diff --git a/.flake8 b/.flake8 new file mode 100644 index 000000000..308e748f4 --- /dev/null +++ b/.flake8 @@ -0,0 +1,2 @@ +[flake8] +max-line-length = 125 \ No newline at end of file diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 000000000..d6547ce30 --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,2 @@ +# change black settings +108955b601e768fd56696be903fc8b471c73ebf7 diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 000000000..4a08579c2 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,12 @@ +# Salesforce Open Source project configuration +# Learn more: https://github.com/salesforce/oss-template +#ECCN:Open Source +#GUSINFO:Open Source,Open Source Workflow + +# @slackapi/slack-platform-python +# are code reviewers for all changes in this repo. +* @slackapi/slack-platform-python + +# @slackapi/developer-education +# are code reviewers for changes in the `/docs` directory. +/docs/ @slackapi/developer-education diff --git a/.github/ISSUE_TEMPLATE/01_question.md b/.github/ISSUE_TEMPLATE/01_question.md new file mode 100644 index 000000000..66971561d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/01_question.md @@ -0,0 +1,51 @@ +--- +name: SDK Question +about: Submit a question about this SDK +title: (Set a clear title describing your question) +labels: "untriaged" +assignees: "" +--- + +(Describe your issue and goal here) + +### Reproducible in: + +```bash +pip freeze | grep slack +python --version +sw_vers && uname -v # or `ver` +``` + +#### The Slack SDK version + +(Paste the output of `pip freeze | grep slack`) + +#### Python runtime version + +(Paste the output of `python --version`) + +#### OS info + +(Paste the output of `sw_vers && uname -v` on macOS/Linux or `ver` on Windows OS) + +#### Steps to reproduce: + +(Share the commands to run, source code, and project settings (e.g., pyproject.toml)) + +1. +2. +3. + +### Expected result: + +(Tell what you expected to happen) + +### Actual result: + +(Tell what actually happened with logs, screenshots) + +### Requirements + +For general questions/issues about Slack API platform or its server-side, could you submit questions at https://my.slack.com/help/requests/new instead. :bow: + +Please read the [Contributing guidelines](https://github.com/slackapi/python-slack-sdk/blob/main/.github/contributing.md) and [Code of Conduct](https://slackhq.github.io/code-of-conduct) before creating this issue or pull request. By submitting, you are agreeing to those rules. diff --git a/.github/ISSUE_TEMPLATE/02_enhancement.md b/.github/ISSUE_TEMPLATE/02_enhancement.md new file mode 100644 index 000000000..33c349f68 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/02_enhancement.md @@ -0,0 +1,25 @@ +--- +name: SDK Enhancement / Feature Request +about: Submit an enhancement/feature request +title: (Set a clear title describing your idea) +labels: "untriaged" +assignees: "" +--- + +(Describe your issue and goal here) + +### Category (place an `x` in each of the `[ ]`) + +- [ ] **slack_sdk.web.WebClient (sync/async)** (Web API client) +- [ ] **slack_sdk.webhook.WebhookClient (sync/async)** (Incoming Webhook, response_url sender) +- [ ] **slack_sdk.models** (UI component builders) +- [ ] **slack_sdk.oauth** (OAuth Flow Utilities) +- [ ] **slack_sdk.socket_mode** (Socket Mode client) +- [ ] **slack_sdk.audit_logs** (Audit Logs API client) +- [ ] **slack_sdk.scim** (SCIM API client) +- [ ] **slack_sdk.rtm** (RTM client) +- [ ] **slack_sdk.signature** (Request Signature Verifier) + +### Requirements + +Please read the [Contributing guidelines](https://github.com/slackapi/python-slack-sdk/blob/main/.github/contributing.md) and [Code of Conduct](https://slackhq.github.io/code-of-conduct) before creating this issue or pull request. By submitting, you are agreeing to those rules. diff --git a/.github/ISSUE_TEMPLATE/03_document.md b/.github/ISSUE_TEMPLATE/03_document.md new file mode 100644 index 000000000..822bc2ee4 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/03_document.md @@ -0,0 +1,17 @@ +--- +name: SDK Document +about: Submit an issue on documents +title: (Set a clear title describing your idea) +labels: "untriaged" +assignees: "" +--- + +(Describe your issue and goal here) + +### The page URLs + +- https://docs.slack.dev/tools/python-slack-sdk/ + +### Requirements + +Please read the [Contributing guidelines](https://github.com/slackapi/python-slack-sdk/blob/main/.github/contributing.md) and [Code of Conduct](https://slackhq.github.io/code-of-conduct) before creating this issue or pull request. By submitting, you are agreeing to those rules. diff --git a/.github/ISSUE_TEMPLATE/04_bug.md b/.github/ISSUE_TEMPLATE/04_bug.md new file mode 100644 index 000000000..fd1fc5718 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/04_bug.md @@ -0,0 +1,51 @@ +--- +name: SDK Bug +about: Report the SDK bug +title: (Set a clear title describing the issue) +labels: "untriaged" +assignees: "" +--- + +(Filling out the following details about bugs will help us solve your issue sooner.) + +### Reproducible in: + +```bash +pip freeze | grep slack +python --version +sw_vers && uname -v # or `ver` +``` + +#### The Slack SDK version + +(Paste the output of `pip freeze | grep slack`) + +#### Python runtime version + +(Paste the output of `python --version`) + +#### OS info + +(Paste the output of `sw_vers && uname -v` on macOS/Linux or `ver` on Windows OS) + +#### Steps to reproduce: + +(Share the commands to run, source code, and project settings (e.g., pyproject.toml)) + +1. +2. +3. + +### Expected result: + +(Tell what you expected to happen) + +### Actual result: + +(Tell what actually happened with logs, screenshots) + +### Requirements + +For general questions/issues about Slack API platform or its server-side, could you submit questions at https://my.slack.com/help/requests/new instead. :bow: + +Please read the [Contributing guidelines](https://github.com/slackapi/python-slack-sdk/blob/main/.github/contributing.md) and [Code of Conduct](https://slackhq.github.io/code-of-conduct) before creating this issue or pull request. By submitting, you are agreeing to those rules. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000..1925abae7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,7 @@ +blank_issues_enabled: false +contact_links: + - name: Slack Platform Customer Support + url: https://my.slack.com/help/requests/new + about: | + This issue tracker is a place to track bugs, feature requests, and questions on this SDK side. + If you have a general question on how to use the Slack platform, please get in touch with our customer support agents first via either /feedback in your Slack workspace or the help page link here. diff --git a/.github/contributing.md b/.github/contributing.md new file mode 100644 index 000000000..f15e0c46a --- /dev/null +++ b/.github/contributing.md @@ -0,0 +1,63 @@ +# Contributors Guide + +Interested in contributing? Awesome! Before you do though, please read our +[Code of Conduct](https://slackhq.github.io/code-of-conduct). We take it very seriously, and expect that you will as +well. + +There are many ways you can contribute! :heart: + +### Bug Reports and Fixes :bug: + +- If you find a bug, please search for it in the [Issues](https://github.com/slackapi/python-slack-sdk/issues), and if it isn't already tracked, + [create a new issue](https://github.com/slackapi/python-slack-sdk/issues/new). Fill out the "Bug Report" section of the issue template. Even if an Issue is closed, feel free to comment and add details, it will still + be reviewed. +- Issues that have already been identified as a bug (note: able to reproduce) will be labelled `bug`. +- If you'd like to submit a fix for a bug, [send a Pull Request](#creating_a_pull_request) and mention the Issue number. +- Include tests that isolate the bug and verifies that it was fixed. + +### New Features :bulb: + +- If you'd like to add new functionality to this project, describe the problem you want to solve in a [new Issue](https://github.com/slackapi/python-slack-sdk/issues/new). +- Issues that have been identified as a feature request will be labelled `enhancement`. +- If you'd like to implement the new feature, please wait for feedback from the project + maintainers before spending too much time writing the code. In some cases, `enhancement`s may + not align well with the project objectives at the time. + +### Tests :mag:, Documentation :books:, Miscellaneous :sparkles: + +- If you'd like to improve the tests, you want to make the documentation clearer, you have an + alternative implementation of something that may have advantages over the way its currently + done, or you have any other change, we would be happy to hear about it! +- If its a trivial change, go ahead and [send a Pull Request](#creating_a_pull_request) with the changes you have in mind. +- If not, [open an Issue](https://github.com/slackapi/python-slack-sdk/issues/new) to discuss the idea first. + +If you're new to our project and looking for some way to make your first contribution, look for +Issues labelled `good first contribution`. + +## Requirements + +For your contribution to be accepted: + +- [x] You must have signed the [Contributor License Agreement (CLA)](https://cla.salesforce.com/sign-cla). +- [x] The test suite must be complete and pass (see the [Maintainer's Guide](./maintainers_guide.md) for details on how to run the tests). +- [x] The changes must be approved by code review. +- [x] Commits should be atomic and messages must be descriptive. Related issues should be mentioned by Issue number. + +If the contribution doesn't meet the above criteria, you may fail our automated checks or a maintainer will discuss it with you. You can continue to improve a Pull Request by adding commits to the branch from which the PR was created. + +[Interested in knowing more about about pull requests at Slack?](https://slack.engineering/on-empathy-pull-requests-979e4257d158#.awxtvmb2z) + +## Creating a Pull Request + +1. :fork_and_knife: Fork the repository on GitHub. +2. :runner: Clone/fetch your fork to your local development machine. It's a good idea to run the tests just + to make sure everything is in order. +3. :herb: Create a new branch and check it out. +4. :crystal_ball: Make your changes and commit them locally. Magic happens here! +5. :arrow_heading_up: Push your new branch to your fork. (e.g. `git push username fix-issue-16`). +6. :inbox_tray: Open a Pull Request on github.com from your new branch on your fork to `main` in this + repository. + +## Maintainers + +There are more details about processes and workflow in the [Maintainer's Guide](./maintainers_guide.md). diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..ac86badc1 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,16 @@ +# Please see the documentation for all configuration options: +# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file + +version: 2 +updates: + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "monthly" + open-pull-requests-limit: 5 + ignore: + - dependency-name: "black" + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "monthly" diff --git a/.github/issue_template.md b/.github/issue_template.md new file mode 100644 index 000000000..33b393830 --- /dev/null +++ b/.github/issue_template.md @@ -0,0 +1,43 @@ +(Describe your issue and goal here) + +### Reproducible in: + +```bash +pip freeze | grep slack +python --version +sw_vers && uname -v # or `ver` +``` + +#### The Slack SDK version + +(Paste the output of `pip freeze | grep slack`) + +#### Python runtime version + +(Paste the output of `python --version`) + +#### OS info + +(Paste the output of `sw_vers && uname -v` on macOS/Linux or `ver` on Windows OS) + +#### Steps to reproduce: + +(Share the commands to run, source code, and project settings (e.g., pyproject.toml)) + +1. +2. +3. + +### Expected result: + +(Tell what you expected to happen) + +### Actual result: + +(Tell what actually happened with logs, screenshots) + +### Requirements + +For general questions/issues about Slack API platform or its server-side, could you submit questions at https://my.slack.com/help/requests/new instead. :bow: + +Please read the [Contributing guidelines](https://github.com/slackapi/python-slack-sdk/blob/main/.github/contributing.md) and [Code of Conduct](https://slackhq.github.io/code-of-conduct) before creating this issue or pull request. By submitting, you are agreeing to those rules. diff --git a/.github/maintainers_guide.md b/.github/maintainers_guide.md new file mode 100644 index 000000000..ccda1607f --- /dev/null +++ b/.github/maintainers_guide.md @@ -0,0 +1,277 @@ +# Maintainers Guide + +This document describes tools, tasks and workflow that one needs to be familiar with in order to effectively maintain +this project. If you use this package within your own software as is but don't plan on modifying it, this guide is +**not** for you. + +## Tools + +### Python (and friends) + +We recommend using [pyenv](https://github.com/pyenv/pyenv) for Python runtime management. If you use macOS, follow the following steps: + +```sh +brew update +brew install pyenv +``` + +You can hook `pyenv` into your shell automatically by running `pyenv init` and following the instructions. + +Install necessary Python runtimes for development/testing. It is not necessary +to install all the various Python versions we test in [continuous integration on +GitHub Actions](https://github.com/slackapi/python-slack-sdk/blob/main/.github/workflows/tests.yml), +but make sure you are running at least one version that we execute our tests in +locally so that you can run the tests yourself. + +```sh +$ pyenv install -l | grep -v "-e[conda|stackless|pypy]" + +$ pyenv install 3.9.6 # select the latest patch version +$ pyenv local 3.9.6 + +$ pyenv versions + system + 3.6.10 + 3.7.7 +* 3.9.6 (set by /path-to-python-slack-sdk/.python-version) + +$ pyenv rehash +``` + +Then, you can create a new [Virtual Environment](https://docs.python.org/3/tutorial/venv.html) specific to the Python version you just installed by running: + +```sh +python -m venv env_3.9.6 +source env_3.9.6/bin/activate +``` + +At this point you have a clean, Python-version-specific environment "activated" for +use just for this project. All `python` and `pip` commands run in your shell +from this point on run in the context of this virtual environment. You can +deactivate the virtual environment by running `deactivate`; it is recommended to +do so after you are done working in this project. To come back to development +work for this project again in the future, `cd` into this project directory and +run `source env_3.9.6/bin/activate` again. + +The last step is to install this project's dependencies and run all unit tests; to do so, you can run + +```sh +./scripts/run_validation.sh +``` + +Also check out [how +we configure GitHub Actions to install dependencies for this project for use in +our continuous integration](https://github.com/slackapi/python-slack-sdk/blob/v3.17.0/.github/workflows/ci-build.yml#L28-L32). + +## Tasks + +### Formatting + +This project uses code formatting tools to maintain consistent style. You can format the codebase by running: + +```sh +./scripts/format.sh +``` + +### Testing + +#### Unit Tests + +When you make changes to this SDK, please write unit tests verifying if the changes work as you expected. You can easily run all the tests and formatting/linter with the below scripts. + +Run all the unit tests, code linter, and code analyzer: + +```sh +./scripts/run_validation.sh +``` + +Run all the unit tests (no linter nor code analyzer): + +```sh +./scripts/run_unit_tests.sh +``` + +Run a specific unit test: + +```sh +./scripts/run_unit_tests.sh tests/web/test_web_client.py +``` + +You can rely on GitHub Actions builds for running the tests on a variety of Python runtimes. + +#### Integration Tests with Real Slack APIs + +This project also has integration tests that verify the SDK works with the Slack API platform. As a preparation, you need to set [the required env variables](https://github.com/slackapi/python-slack-sdk/blob/main/integration_tests/env_variable_names.py) properly. You don't need to setup all of them if you just want to run some of the tests. Commonly, `SLACK_SDK_TEST_BOT_TOKEN` and `SLACK_SDK_TEST_USER_TOKEN` are used for running `WebClient` tests. + +Run all integration tests: + +```sh +./scripts/run_integration_tests.sh +``` + +Run a specific integration test: + +```sh +./scripts/run_integration_tests.sh integration_tests/web/test_async_web_client.py +``` + +#### Develop Locally + +If you want to test the package locally you can. + +1. Build the package locally + - Run + ```sh + scripts/build_pypi_package.sh + ``` + - This will create a `.whl` file in the `./dist` folder +2. Use the built package + - Example `/dist/slack_sdk-1.2.3-py2.py3-none-any.whl` was created + - From anywhere on your machine you can install this package to a project with + ```sh + pip install /dist/slack_sdk-1.2.3-py2.py3-none-any.whl + ``` + - It is also possible to include `slack_sdk @ file:////dist/slack_sdk-1.2.3-py2.py3-none-any.whl` in a [requirements.txt](https://pip.pypa.io/en/stable/user_guide/#requirements-files) file + +### Generating Documentation + +See [`/docs/README`](https://github.com/slackapi/python-slack-sdk/blob/main/docs/README.md) for information on editing documentation pages. + +The API reference is generated from a script. You can generate and preview the **API _reference_ documents for `slack_sdk` package modules** by running: + +```sh +./scripts/generate_api_docs.sh +``` + +### Releasing + +#### test.pypi.org deployment + +[TestPyPI](https://test.pypi.org/) is a separate instance of the Python Package +Index that allows you to try distribution tools and processes without affecting +the real index. This is particularly useful when making changes related to the +package configuration itself, for example, modifications to the `pyproject.toml` file. + +You can deploy this project to TestPyPI using GitHub Actions. + +To deploy using GitHub Actions: + +1. Push your changes to a branch or tag +2. Navigate to +3. Click on "Run workflow" +4. Select your branch or tag from the dropdown +5. Click "Run workflow" to build and deploy your branch to TestPyPI + +Alternatively, you can deploy from your local machine with: + +```sh +./scripts/deploy_to_test_pypi.sh +``` + +#### Development Deployment + +Deploying a new version of this library to PyPI is triggered by publishing a GitHub Release. +Before creating a new release, ensure that everything on a stable branch has +landed, then [run the tests](#unit-tests). + +1. Create the commit for the release + 1. Use the latest supported Python version. Using a [virtual environment](#python-and-friends) is recommended. + 2. In `slack_sdk/version.py` bump the version number in adherence to [Semantic Versioning](http://semver.org/) and [Developmental Release](https://peps.python.org/pep-0440/#developmental-releases). + - Example: if the current version is `1.2.3`, a proper development bump would be `1.2.4.dev0` + - `.dev` will indicate to pip that this is a [Development Release](https://peps.python.org/pep-0440/#developmental-releases) + - Note that the `dev` version can be bumped in development releases: `1.2.4.dev0` -> `1.2.4.dev1` + 3. Build the docs with `./scripts/generate_api_docs.sh`. + 4. Commit with a message including the new version number. For example `1.2.4.dev0` & push the commit to a branch where the development release will live (create it if it does not exist) + 1. `git checkout -b future-release` + 2. `git add --all` (review files with `git status` before committing) + 3. `git commit -m 'chore(release): version 1.2.4.dev0'` + 4. `git push -u origin future-release` +2. Create a new GitHub Release + 1. Navigate to the [Releases page](https://github.com/slackapi/python-slack-sdk/releases). + 2. Click the "Draft a new release" button. + 3. Set the "Target" to the feature branch with the development changes. + 4. Click "Tag: Select tag" + 5. Input a new tag name manually. The tag name must match the version in `slack_sdk/version.py` prefixed with "v" (e.g., if version is `1.2.4.dev0`, enter `v1.2.4.dev0`) + 6. Click the "Create a new tag" button. This won't create your tag immediately. + 7. Click the "Generate release notes" button. + 8. The release name should match the tag name! + 9. Edit the resulting notes to ensure they have decent messaging that is understandable by non-contributors, but each commit should still have its own line. + 10. Set this release as a pre-release. + 11. Publish the release by clicking the "Publish release" button! +3. Navigate to the [release workflow run](https://github.com/slackapi/python-slack-sdk/actions/workflows/pypi-release.yml). You will need to approve the deployment! +4. After a few minutes, the corresponding version will be available on . +5. (Slack Internal) Communicate the release internally + +#### Production Deployment + +Deploying a new version of this library to PyPI is triggered by publishing a GitHub Release. +Before creating a new release, ensure that everything on the `main` branch since +the last tag is in a releasable state! At a minimum, [run the tests](#unit-tests). + +1. Create the commit for the release + 1. Use the latest supported Python version. Using a [virtual environment](#python-and-friends) is recommended. + 2. In `slack_sdk/version.py` bump the version number in adherence to [Semantic Versioning](http://semver.org/) and the [Versioning](#versioning-and-tags) section. + 3. Build the docs with `./scripts/generate_api_docs.sh`. + 4. Commit with a message including the new version number. For example `1.2.3` & push the commit to a branch and create a PR to sanity check. + 1. `git checkout -b 1.2.3-release` + 2. `git add --all` (review files with `git status` before committing) + 3. `git commit -m 'chore(release): version 1.2.3'` + 4. `git push -u origin 1.2.3-release` + 5. Add relevant labels to the PR and add the PR to a GitHub Milestone. + 6. Merge in release PR after getting an approval from at least one maintainer. +2. Create a new GitHub Release + 1. Navigate to the [Releases page](https://github.com/slackapi/python-slack-sdk/releases). + 2. Click the "Draft a new release" button. + 3. Set the "Target" to the `main` branch. + 4. Click "Tag: Select tag" + 5. Input a new tag name manually. The tag name must match the version in `slack_sdk/version.py` prefixed with "v" (e.g., if version is `1.2.3`, enter `v1.2.3`) + 6. Click the "Create a new tag" button. This won't create your tag immediately. + 7. Click the "Generate release notes" button. + 8. The release name should match the tag name! + 9. Edit the resulting notes to ensure they have decent messaging that is understandable by non-contributors, but each commit should still have its own line. + 10. Include a link to the current GitHub Milestone. + 11. Ensure the "latest release" checkbox is checked to mark this as the latest stable release. + 12. Publish the release by clicking the "Publish release" button! +3. Navigate to the [release workflow run](https://github.com/slackapi/python-slack-sdk/actions/workflows/pypi-release.yml). You will need to approve the deployment! +4. After a few minutes, the corresponding version will be available on . +5. Close the current GitHub Milestone and create one for the next patch version. +6. (Slack Internal) Communicate the release internally + - Include a link to the GitHub release +7. (Slack Internal) Tweet by @SlackAPI + - Not necessary for patch updates, might be needed for minor updates, + definitely needed for major updates. Include a link to the GitHub release + +## Workflow + +### Versioning and Tags + +This project uses [Semantic Versioning](http://semver.org/), expressed through the numbering scheme of +[PEP-0440](https://www.python.org/dev/peps/pep-0440/). + +### Branches + +The `main` branch is where active development occurs. Long running named feature branches are occasionally created for +collaboration on a feature that has a large scope (because everyone cannot push commits to another person's open Pull +Request). At some point in the future after a major version increment, there may be maintenance branches for older major +versions. + +### Issue Management + +Labels are used to run issues through an organized workflow. Here are the basic definitions: + +- `bug`: A confirmed bug report. A bug is considered confirmed when reproduction steps have been + documented and the issue has been reproduced. +- `enhancement`: A feature request for something this package might not already do. +- `docs`: An issue that is purely about documentation work. +- `tests`: An issue that is purely about testing work. +- `question`: An issue that is like a support request because the user's usage was not correct. + +**Triage** is the process of taking new issues that aren't yet "seen" and marking them with a basic level of information +with labels. An issue should have **one** of the following labels applied: `bug`, `enhancement`, `question`, `docs`, `tests`, or `discussion`. + +Issues are closed when a resolution has been reached. If for any reason a closed issue seems relevant once again, +reopening is great and better than creating a duplicate issue. + +## Everything else + +When in doubt, find the other maintainers and ask. diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 000000000..ae9cb65ab --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,28 @@ +## Summary + + + +### Testing + + + +### Category + +- [ ] **slack_sdk.web.WebClient (sync/async)** (Web API client) +- [ ] **slack_sdk.webhook.WebhookClient (sync/async)** (Incoming Webhook, response_url sender) +- [ ] **slack_sdk.socket_mode** (Socket Mode client) +- [ ] **slack_sdk.signature** (Request Signature Verifier) +- [ ] **slack_sdk.oauth** (OAuth Flow Utilities) +- [ ] **slack_sdk.models** (UI component builders) +- [ ] **slack_sdk.scim** (SCIM API client) +- [ ] **slack_sdk.audit_logs** (Audit Logs API client) +- [ ] **slack_sdk.rtm_v2** (RTM client) +- [ ] `/docs` (Documents) +- [ ] `/tutorial` (PythOnBoardingBot tutorial) +- [ ] `tests`/`integration_tests` (Automated tests for this library) + +## Requirements + +- [ ] I've read and understood the [Contributing Guidelines](https://github.com/slackapi/python-slack-sdk/blob/main/.github/contributing.md) and have done my best effort to follow them. +- [ ] I've read and agree to the [Code of Conduct](https://slackhq.github.io/code-of-conduct). +- [ ] I've run `python3 -m venv .venv && source .venv/bin/activate && ./scripts/run_validation.sh` after making the changes. diff --git a/.github/release.yml b/.github/release.yml new file mode 100644 index 000000000..b2574b7cc --- /dev/null +++ b/.github/release.yml @@ -0,0 +1,24 @@ +# https://docs.github.com/en/repositories/releasing-projects-on-github/automatically-generated-release-notes#configuring-automatically-generated-release-notes +changelog: + categories: + - title: 🚀 Enhancements + labels: + - enhancement + - title: 🐛 Bug Fixes + labels: + - bug + - title: 📚 Documentation + labels: + - docs + - title: 🤖 Build + labels: + - build + - title: 🧪 Testing/Code Health + labels: + - code health + - title: 🔒 Security + labels: + - security + - title: 📦 Other changes + labels: + - "*" diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml new file mode 100644 index 000000000..c0fd021f0 --- /dev/null +++ b/.github/workflows/ci-build.yml @@ -0,0 +1,134 @@ +name: Python CI + +on: + push: + branches: + - main + pull_request: + schedule: + - cron: "0 0 * * *" + workflow_dispatch: + +env: + LATEST_SUPPORTED_PY: "3.14" + +jobs: + lint: + name: Lint + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: + contents: read + steps: + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + persist-credentials: false + - name: Set up Python ${{ env.LATEST_SUPPORTED_PY }} + uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 + with: + python-version: ${{ env.LATEST_SUPPORTED_PY }} + - name: Run lint verification + run: ./scripts/lint.sh + + typecheck: + name: Typecheck + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: + contents: read + steps: + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + persist-credentials: false + - name: Set up Python ${{ env.LATEST_SUPPORTED_PY }} + uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 + with: + python-version: ${{ env.LATEST_SUPPORTED_PY }} + - name: Run mypy verification + run: ./scripts/run_mypy.sh + + unittest: + name: Unit tests + runs-on: ubuntu-22.04 + timeout-minutes: 15 + strategy: + fail-fast: false + matrix: + python-version: + - "3.14" + - "3.13" + - "3.12" + - "3.11" + - "3.10" + - "3.9" + - "3.8" + - "3.7" + - "pypy3.10" + - "pypy3.11" + permissions: + contents: read + env: + CI_LARGE_SOCKET_MODE_PAYLOAD_TESTING_DISABLED: "1" + FORCE_COLOR: "1" + steps: + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + persist-credentials: false + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 + with: + python-version: ${{ matrix.python-version }} + cache: pip + - name: Install dependencies + run: | + pip install -U pip + pip install -r requirements/testing.txt + pip install -r requirements/optional.txt + - name: Run tests + run: | + PYTHONPATH=$PWD:$PYTHONPATH pytest --cov-report=xml --cov=slack_sdk/ --junitxml=reports/test_report.xml tests/ + - name: Run tests for SQLAlchemy v1.4 (backward-compatibility) + run: | + # Install v1.4 for testing + pip install "SQLAlchemy>=1.4,<2" + PYTHONPATH=$PWD:$PYTHONPATH pytest tests/slack_sdk/oauth/installation_store/test_sqlalchemy.py + PYTHONPATH=$PWD:$PYTHONPATH pytest tests/slack_sdk/oauth/state_store/test_sqlalchemy.py + - name: Upload test results to Codecov + if: ${{ !cancelled() }} + uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2 + with: + directory: ./reports/ + fail_ci_if_error: true + flags: ${{ matrix.python-version }} + report_type: test_results + token: ${{ secrets.CODECOV_TOKEN }} + verbose: true + - name: Upload test coverage to Codecov (only with latest supported version) + if: startsWith(matrix.python-version, env.LATEST_SUPPORTED_PY) + uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5.5.2 + with: + fail_ci_if_error: true + # Run validation generates the coverage file + files: ./coverage.xml + report_type: coverage + token: ${{ secrets.CODECOV_TOKEN }} + verbose: true + + notifications: + name: Regression notifications + runs-on: ubuntu-latest + needs: + - lint + - typecheck + - unittest + if: ${{ !success() && github.ref == 'refs/heads/main' && github.event_name != 'workflow_dispatch' }} + steps: + - name: Send notifications of failing tests + uses: slackapi/slack-github-action@91efab103c0de0a537f72a35f6b8cda0ee76bf0a # v2.1.1 + with: + errors: true + webhook: ${{ secrets.SLACK_REGRESSION_FAILURES_WEBHOOK_URL }} + webhook-type: webhook-trigger + payload: | + action_url: "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" + repository: "${{ github.repository }}" diff --git a/.github/workflows/pypi-release.yml b/.github/workflows/pypi-release.yml new file mode 100644 index 000000000..89a18c827 --- /dev/null +++ b/.github/workflows/pypi-release.yml @@ -0,0 +1,87 @@ +name: Upload A Release to pypi.org or test.pypi.org + +on: + release: + types: + - published + workflow_dispatch: + inputs: + dry_run: + description: "Dry run (build only, do not publish)" + required: false + type: boolean + +jobs: + release-build: + runs-on: ubuntu-latest + permissions: + contents: read + + steps: + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + with: + ref: ${{ github.event.release.tag_name || github.ref }} + persist-credentials: false + + - name: Set up Python + uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v6.1.0 + with: + python-version: "3.x" + + - name: Build release distributions + run: | + scripts/build_pypi_package.sh + + - name: Persist dist folder + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + with: + name: release-dist + path: dist/ + + test-pypi-publish: + runs-on: ubuntu-latest + needs: + - release-build + # Run this job for workflow_dispatch events when dry_run input is not 'true' + # Note: The comparison is against a string value 'true' since GitHub Actions inputs are strings + if: github.event_name == 'workflow_dispatch' && github.event.inputs.dry_run != 'true' + environment: + name: testpypi + permissions: + id-token: write + + steps: + - name: Retrieve dist folder + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 + with: + name: release-dist + path: dist/ + + - name: Publish release distributions to test.pypi.org + # Using OIDC for PyPI publishing (no API tokens needed) + # See: https://docs.github.com/en/actions/how-tos/secure-your-work/security-harden-deployments/oidc-in-pypi + uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 + with: + repository-url: https://test.pypi.org/legacy/ + + pypi-publish: + runs-on: ubuntu-latest + needs: + - release-build + if: github.event_name == 'release' + environment: + name: pypi + permissions: + id-token: write + + steps: + - name: Retrieve dist folder + uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 + with: + name: release-dist + path: dist/ + + - name: Publish release distributions to pypi.org + # Using OIDC for PyPI publishing (no API tokens needed) + # See: https://docs.github.com/en/actions/how-tos/secure-your-work/security-harden-deployments/oidc-in-pypi + uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 diff --git a/.github/workflows/triage-issues.yml b/.github/workflows/triage-issues.yml new file mode 100644 index 000000000..cf13d3afc --- /dev/null +++ b/.github/workflows/triage-issues.yml @@ -0,0 +1,32 @@ +# This workflow uses the following github action to automate +# management of stale issues and prs in this repo: +# https://github.com/marketplace/actions/close-stale-issues + +name: Close stale issues and PRs + +on: + workflow_dispatch: + schedule: + - cron: "0 0 * * 1" + +jobs: + stale: + runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: write + steps: + - uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1 + with: + days-before-issue-stale: 30 + days-before-issue-close: 10 + days-before-pr-stale: -1 + days-before-pr-close: -1 + stale-issue-label: auto-triage-stale + stale-issue-message: 👋 It looks like this issue has been open for 30 days with no activity. We'll mark this as stale for now, and wait 10 days for an update or for further comment before closing this issue out. If you think this issue needs to be prioritized, please comment to get the thread going again! Maintainers also review issues marked as stale on a regular basis and comment or adjust status if the issue needs to be reprioritized. + close-issue-message: As this issue has been inactive for more than one month, we will be closing it. Thank you to all the participants! If you would like to raise a related issue, please create a new issue which includes your specific details and references this issue number. + exempt-issue-labels: auto-triage-skip + exempt-all-milestones: true + remove-stale-when-updated: true + enable-statistics: true + operations-per-run: 60 diff --git a/.gitignore b/.gitignore index 0d20b6487..5f316a341 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,41 @@ -*.pyc +# general things to ignore +build/ +dist/ +.eggs/ +*.egg-info/ +*.egg +*.py[cod] +__pycache__/ +*.so +*~ + +# virtualenv +env*/ +venv*/ +.venv*/ +.env*/ + +# codecov / coverage +.coverage* +cov_* +coverage.xml +reports/ + +# due to using tox and pytest +.tox +.cache +.pytest_cache/ +.python-version +pip +.mypy_cache/ + +# JetBrains PyCharm settings +.idea/ + +tmp.txt +.DS_Store +logs/ + +.pytype/ +*.db +.env* diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..08726984d --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,9 @@ +// Place your settings in this file to overwrite default and user settings. +{ + "python.linting.pylintEnabled": false, + "python.linting.flake8Enabled": true, + "python.venvPath": "${workspaceFolder}/env", + "python.formatting.provider": "black", + "editor.formatOnSave": true, + "python.linting.enabled": true, +} \ No newline at end of file diff --git a/LICENSE.txt b/LICENSE similarity index 93% rename from LICENSE.txt rename to LICENSE index 89de35479..a63845785 100644 --- a/LICENSE.txt +++ b/LICENSE @@ -1,3 +1,7 @@ +The MIT License (MIT) + +Copyright (c) 2015- Slack Technologies, LLC + Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 000000000..4c24f8ecf --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1 @@ +include README.md LICENSE slack/py.typed slack_sdk/py.typed diff --git a/README.md b/README.md index b7ad2b940..2d0638e7d 100644 --- a/README.md +++ b/README.md @@ -1,93 +1,305 @@ -python-slackclient -================ -A basic client for Slack.com, which can optionally connect to the Slack Real Time Messaging (RTM) API. - -Overview ---------- -This plugin is a light wrapper around the [Slack API](https://api.slack.com/). In its basic form, it can be used to call any API method and be expected to return a dict of the JSON reply. - -The optional RTM connection allows you to create a persistent websocket connection, from which you can read events just like an official Slack client. This allows you to respond to events in real time without polling and send messages without making a full HTTPS request. - -See [python-rtmbot](https://github.com/slackhq/python-rtmbot/) for an active project utilizing this library. - -Installation ----------- - -#### Automatic w/ PyPI ([virtualenv](http://virtualenv.readthedocs.org/en/latest/) is recommended.) - - pip install slackclient - -#### Manual - - git clone https://github.com/slackhq/python-slackclient.git - pip install -r requirements.txt - -Usage ------ -See examples in [doc/examples](doc/examples/) - -_Note:_ You must obtain a token for the user/bot. You can find or generate these at the [Slack API](https://api.slack.com/web) page. - -###Basic API methods - -```python -import time -from slackclient import SlackClient - -token = "xoxp-28192348123947234198234" # found at https://api.slack.com/#auth) -sc = SlackClient(token) -print sc.api_call("api.test") -print sc.api_call("channels.info", channel="1234567890") -``` - -### Real Time Messaging ---------- -```python -import time -from slackclient import SlackClient - -token = "xoxp-28192348123947234198234"# found at https://api.slack.com/#auth) -sc = SlackClient(token) -if sc.rtm_connect(): - while True: - print sc.rtm_read() - time.sleep(1) -else: - print "Connection Failed, invalid token?" -``` - -####Objects ------------ - -[SlackClient.**server**] -Server object owns the websocket and all nested channel information. - -[SlackClient.server.**channels**] -A searchable list of all known channels within the parent server. Call `print (sc instance)` to see the entire list. - -####Methods ------------ - -SlackClient.**rtm_connect()** -Connect to a Slack RTM websocket. This is a persistent connection from which you can read events. - -SlackClient.**rtm_read()** -Read all data from the RTM websocket. Multiple events may be returned, always returns a list [], which is empty if there are no incoming messages. - -SlackClient.**rtm_send_message([channel, message])** -Sends the text in [message] to [channel], which can be a name or identifier i.e. "#general" or "C182391" - -SlackClient.**api_call([method, params])** -Call the Slack method [method] with the a dict of params in [params] - -SlackClient.server.**send_to_websocket([data])** -Send a JSON message directly to the websocket. See RTM documentation for allowed types. - -SlackClient.server.**channels.find([identifier])** -The identifier can be either name or Slack channel ID. See above for examples. - -SlackClient.server.**channnels[int].send_message([text])** -Send message [text] to [int] channel in the channels list. - -SlackClient.server.**channnels.find([identifier]).send_message([text])** -Send message [text] to channel [identifier], which can be either channel name or ID. Ex "#general" or "C182391" +

Python Slack SDK

+ +

+ + Tests + + Codecov + + Pepy Total Downloads +
+ + PyPI - Version + + Python Versions + + Documentation +

+ +The Slack platform offers several APIs to build apps. Each Slack API delivers part of the capabilities from the platform, so that you can pick just those that fit for your needs. This SDK offers a corresponding package for each of Slack’s APIs. They are small and powerful when used independently, and work seamlessly when used together, too. + +**Comprehensive documentation on using the Slack Python can be found at [https://docs.slack.dev/tools/python-slack-sdk/](https://docs.slack.dev/tools/python-slack-sdk/)** + +--- + +Whether you're building a custom app for your team, or integrating a third party service into your Slack workflows, Slack Developer Kit for Python allows you to leverage the flexibility of Python to get your project up and running as quickly as possible. + +The **Python Slack SDK** allows interaction with: + +- `slack_sdk.web`: for calling the [Web API methods][api-methods] +- `slack_sdk.webhook`: for utilizing the [Incoming Webhooks](https://docs.slack.dev/messaging/sending-messages-using-incoming-webhooks/) and [`response_url`s in payloads](https://docs.slack.dev/interactivity/handling-user-interaction/#message_responses) +- `slack_sdk.signature`: for [verifying incoming requests from the Slack API server](https://docs.slack.dev/authentication/verifying-requests-from-slack/) +- `slack_sdk.socket_mode`: for receiving and sending messages over [Socket Mode](https://docs.slack.dev/apis/events-api/using-socket-mode/) connections +- `slack_sdk.audit_logs`: for utilizing [Audit Logs APIs](https://docs.slack.dev/admins/audit-logs-api/) +- `slack_sdk.scim`: for utilizing [SCIM APIs](https://docs.slack.dev/admins/scim-api/) +- `slack_sdk.oauth`: for implementing the [Slack OAuth flow](https://docs.slack.dev/authentication/installing-with-oauth/) +- `slack_sdk.models`: for constructing [Block Kit](https://docs.slack.dev/block-kit/) UI components using easy-to-use builders +- `slack_sdk.rtm`: for utilizing the [RTM API][rtm-docs] + +If you want to use our [Events API][events-docs] and Interactivity features, please check the [Bolt for Python][bolt-python] library. Details on the Tokens and Authentication can be found in our [Auth Guide](https://docs.slack.dev/tools/python-slack-sdk/installation/). + +## slackclient is in maintenance mode + +Are you looking for [slackclient](https://pypi.org/project/slackclient/)? The slackclient project is in maintenance mode now and this [`slack_sdk`](https://pypi.org/project/slack-sdk/) is the successor. If you have time to make a migration to slack_sdk v3, please follow [our migration guide](https://docs.slack.dev/tools/python-slack-sdk/v3-migration/) to ensure your app continues working after updating. + +## Table of contents + +* [Requirements](#requirements) +* [Installation](#installation) +* [Getting started tutorial](#getting-started-tutorial) +* [Basic Usage of the Web Client](#basic-usage-of-the-web-client) + * [Sending a message to Slack](#sending-a-message-to-slack) + * [Uploading files to Slack](#uploading-files-to-slack) +* [Async usage](#async-usage) + * [WebClient as a script](#asyncwebclient-in-a-script) + * [WebClient in a framework](#asyncwebclient-in-a-framework) +* [Advanced Options](#advanced-options) + * [SSL](#ssl) + * [Proxy](#proxy) + * [DNS performance](#dns-performance) + * [Example](#example) +* [Migrating from v1](#migrating-from-v1) +* [Support](#support) +* [Development](#development) + +### Requirements + +--- + +This library requires Python 3.7 and above. If you're unsure how to check what version of Python you're on, you can check it using the following: + +> **Note:** You may need to use `python3` before your commands to ensure you use the correct Python path. e.g. `python3 --version` + +```bash +python --version + +-- or -- + +python3 --version +``` + +### Installation + +We recommend using [PyPI][pypi] to install the Slack Developer Kit for Python. + +```bash +$ pip install slack_sdk +``` + +### Getting started tutorial + +--- + +We've created this [tutorial](https://github.com/slackapi/python-slack-sdk/tree/main/tutorial) to build a basic Slack app in less than 10 minutes. It requires some general programming knowledge, and Python basics. It focuses on the interacting with the Slack Web API and RTM API. Use it to give you an idea of how to use this SDK. + +**[Read the tutorial to get started!](https://github.com/slackapi/python-slack-sdk/tree/main/tutorial)** + +### Basic Usage of the Web Client + +--- + +Slack provide a Web API that gives you the ability to build applications that interact with Slack in a variety of ways. This Development Kit is a module based wrapper that makes interaction with that API easier. We have a basic example here with some of the more common uses but a full list of the available methods are available [here][api-methods]. More detailed examples can be found in [our guide](https://docs.slack.dev/tools/python-slack-sdk/web/). + +#### Sending a message to Slack + +One of the most common use-cases is sending a message to Slack. If you want to send a message as your app, or as a user, this method can do both. In our examples, we specify the channel name, however it is recommended to use the `channel_id` where possible. Also, if your app's bot user is not in a channel yet, invite the bot user before running the code snippet (or add `chat:write.public` to Bot Token Scopes for posting in any public channels). + +```python +import os +from slack_sdk import WebClient +from slack_sdk.errors import SlackApiError + +client = WebClient(token=os.environ['SLACK_BOT_TOKEN']) + +try: + response = client.chat_postMessage(channel='#random', text="Hello world!") + assert response["message"]["text"] == "Hello world!" +except SlackApiError as e: + # You will get a SlackApiError if "ok" is False + assert e.response["ok"] is False + assert e.response["error"] # str like 'invalid_auth', 'channel_not_found' + print(f"Got an error: {e.response['error']}") + # Also receive a corresponding status_code + assert isinstance(e.response.status_code, int) + print(f"Received a response status_code: {e.response.status_code}") +``` + +Here we also ensure that the response back from Slack is a successful one and that the message is the one we sent by using the `assert` statement. + +#### Uploading files to Slack + +We've changed the process for uploading files to Slack to be much easier and straight forward. You can now just include a path to the file directly in the API call and upload it that way. + +```python +import os +from slack_sdk import WebClient +from slack_sdk.errors import SlackApiError + +client = WebClient(token=os.environ['SLACK_BOT_TOKEN']) + +try: + filepath="./tmp.txt" + response = client.files_upload_v2(channel='C0123456789', file=filepath) + assert response["file"] # the uploaded file +except SlackApiError as e: + # You will get a SlackApiError if "ok" is False + assert e.response["ok"] is False + assert e.response["error"] # str like 'invalid_auth', 'channel_not_found' + print(f"Got an error: {e.response['error']}") +``` + +More details on the `files_upload_v2` method can be found [here][files_upload_v2]. + +### Async usage + +`AsyncWebClient` in this SDK requires [AIOHttp][aiohttp] under the hood for asynchronous requests. + +#### AsyncWebClient in a script + +```python +import asyncio +import os +from slack_sdk.web.async_client import AsyncWebClient +from slack_sdk.errors import SlackApiError + +client = AsyncWebClient(token=os.environ['SLACK_BOT_TOKEN']) + +async def post_message(): + try: + response = await client.chat_postMessage(channel='#random', text="Hello world!") + assert response["message"]["text"] == "Hello world!" + except SlackApiError as e: + assert e.response["ok"] is False + assert e.response["error"] # str like 'invalid_auth', 'channel_not_found' + print(f"Got an error: {e.response['error']}") + +asyncio.run(post_message()) +``` + +#### AsyncWebClient in a framework + +If you are using a framework invoking the asyncio event loop like : sanic/jupyter notebook/etc. + +```python +import os +from slack_sdk.web.async_client import AsyncWebClient +from slack_sdk.errors import SlackApiError + +client = AsyncWebClient(token=os.environ['SLACK_BOT_TOKEN']) +# Define this as an async function +async def send_to_slack(channel, text): + try: + # Don't forget to have await as the client returns asyncio.Future + response = await client.chat_postMessage(channel=channel, text=text) + assert response["message"]["text"] == text + except SlackApiError as e: + assert e.response["ok"] is False + assert e.response["error"] # str like 'invalid_auth', 'channel_not_found' + raise e + +from aiohttp import web + +async def handle_requests(request: web.Request) -> web.Response: + text = 'Hello World!' + if 'text' in request.query: + text = "\t".join(request.query.getall("text")) + try: + await send_to_slack(channel="#random", text=text) + return web.json_response(data={'message': 'Done!'}) + except SlackApiError as e: + return web.json_response(data={'message': f"Failed due to {e.response['error']}"}) + + +if __name__ == "__main__": + app = web.Application() + app.add_routes([web.get("/", handle_requests)]) + # e.g., http://localhost:3000/?text=foo&text=bar + web.run_app(app, host="0.0.0.0", port=3000) +``` + +### Advanced Options + +#### SSL + +You can provide a custom SSL context or disable verification by passing the `ssl` option, supported by both the RTM and the Web client. + +For async requests, see the [AIOHttp SSL documentation](https://docs.aiohttp.org/en/stable/client_advanced.html#ssl-control-for-tcp-sockets). + +For sync requests, see the [urllib SSL documentation](https://docs.python.org/3/library/urllib.request.html#urllib.request.urlopen). + +#### Proxy + +A proxy is supported when making async requests, pass the `proxy` option, supported by both the RTM and the Web client. + +For async requests, see [AIOHttp Proxy documentation](https://docs.aiohttp.org/en/stable/client_advanced.html#proxy-support). + +For sync requests, setting either `HTTPS_PROXY` env variable or the `proxy` option works. + +#### DNS performance + +Using the async client and looking for a performance boost? Installing the optional dependencies (aiodns) may help speed up DNS resolving by the client. We've included it as an extra called "optional": +```bash +$ pip install slack_sdk[optional] +``` + +#### Example + +```python +import os +from slack_sdk import WebClient +from ssl import SSLContext + +sslcert = SSLContext() +# pip3 install proxy.py +# proxy --port 9000 --log-level d +proxyinfo = "http://localhost:9000" + +client = WebClient( + token=os.environ['SLACK_BOT_TOKEN'], + ssl=sslcert, + proxy=proxyinfo +) +response = client.chat_postMessage(channel="#random", text="Hello World!") +print(response) +``` + +### Migrating from v2 + +If you're migrating from slackclient v2.x of slack_sdk to v3.x, Please follow our migration guide to ensure your app continues working after updating. + +**[Check out the Migration Guide here!](https://docs.slack.dev/tools/python-slack-sdk/v3-migration/)** + +### Migrating from v1 + +If you're migrating from v1.x of slackclient to v2.x, Please follow our migration guide to ensure your app continues working after updating. + +**[Check out the Migration Guide here!](https://github.com/slackapi/python-slack-sdk/wiki/Migrating-to-2.x)** + +### Support + +--- + +If you get stuck, we’re here to help. The following are the best ways to get assistance working through your issue: + +Use our [Github Issue Tracker][gh-issues] for reporting bugs or requesting features. +Visit the [Slack Community][slack-community] for getting help using Slack Developer Kit for Python or just generally bond with your fellow Slack developers. + +### Contributing + +We welcome contributions from everyone! Please check out our +[Contributor's Guide](.github/contributing.md) for how to contribute in a +helpful and collaborative way. + + + +[slackclientv1]: https://github.com/slackapi/python-slackclient/tree/v1 +[api-methods]: https://docs.slack.dev/reference/methods +[rtm-docs]: https://docs.slack.dev/legacy/legacy-rtm-api/ +[events-docs]: https://docs.slack.dev/apis/events-api/ +[bolt-python]: https://github.com/slackapi/bolt-python +[pypi]: https://pypi.org/ +[gh-issues]: https://github.com/slackapi/python-slack-sdk/issues +[slack-community]: https://slackcommunity.com/ +[files_upload_v2]: https://github.com/slackapi/python-slack-sdk/releases/tag/v3.19.0 +[aiohttp]: https://aiohttp.readthedocs.io/ + diff --git a/__init__.py b/__init__.py deleted file mode 100644 index 8b1378917..000000000 --- a/__init__.py +++ /dev/null @@ -1 +0,0 @@ - diff --git a/_pytest/conftest.py b/_pytest/conftest.py deleted file mode 100644 index 13625385e..000000000 --- a/_pytest/conftest.py +++ /dev/null @@ -1,20 +0,0 @@ -import pytest -from slackclient._channel import Channel -from slackclient._server import Server -from slackclient._client import SlackClient - -@pytest.fixture -def server(monkeypatch): - myserver = Server('xoxp-1234123412341234-12341234-1234', False) - return myserver - -@pytest.fixture -def slackclient(server): - myslackclient = SlackClient('xoxp-1234123412341234-12341234-1234') - return myslackclient - -@pytest.fixture -def channel(server): - mychannel = Channel(server, "somechannel", "C12341234", ["user"]) - return mychannel - diff --git a/_pytest/test_channel.py b/_pytest/test_channel.py deleted file mode 100644 index 4ecf4c573..000000000 --- a/_pytest/test_channel.py +++ /dev/null @@ -1,9 +0,0 @@ -from slackclient._channel import Channel -import pytest - -def test_Channel(channel): - assert type(channel) == Channel - -@pytest.mark.xfail -def test_Channel_send_message(channel): - channel.send_message('hi') diff --git a/_pytest/test_server.py b/_pytest/test_server.py deleted file mode 100644 index 5989df9b9..000000000 --- a/_pytest/test_server.py +++ /dev/null @@ -1,33 +0,0 @@ -from slackclient._user import User -from slackclient._server import Server, SlackLoginError -from slackclient._channel import Channel -import json -import pytest - -@pytest.fixture -def login_data(): - login_data = open('_pytest/data/rtm.start.json','r').read() - login_data = json.loads(login_data) - return login_data - -def test_Server(server): - assert type(server) == Server - - -def test_Server_parse_channel_data(server, login_data): - server.parse_channel_data(login_data["channels"]) - assert type(server.channels.find('general')) == Channel - -def test_Server_parse_user_data(server, login_data): - server.parse_user_data(login_data["users"]) - assert type(server.users.find('fakeuser')) == User - -def test_Server_cantconnect(server): - with pytest.raises(SlackLoginError): - reply = server.ping() - -@pytest.mark.xfail -def test_Server_ping(server, monkeypatch): - #monkeypatch.setattr("", lambda: True) - monkeypatch.setattr("websocket.create_connection", lambda: True) - reply = server.ping() diff --git a/_pytest/test_slackclient.py b/_pytest/test_slackclient.py deleted file mode 100644 index 218bfbae1..000000000 --- a/_pytest/test_slackclient.py +++ /dev/null @@ -1,26 +0,0 @@ -from slackclient._client import SlackClient -from slackclient._channel import Channel -import json -import pytest - -@pytest.fixture -def channel_created(): - channel_created = open('_pytest/data/channel.created.json', 'r').read() - channel_created = json.loads(channel_created) - return channel_created - -@pytest.fixture -def im_created(): - channel_created = open('_pytest/data/im.created.json', 'r').read() - channel_created = json.loads(channel_created) - return channel_created - -def test_SlackClient(slackclient): - assert type(slackclient) == SlackClient - -def test_SlackClient_process_changes(slackclient, channel_created, im_created): - slackclient.process_changes(channel_created) - assert type(slackclient.server.channels.find('fun')) == Channel - slackclient.process_changes(im_created) - assert type(slackclient.server.channels.find('U123BL234')) == Channel - diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 000000000..5568e5e6b --- /dev/null +++ b/codecov.yml @@ -0,0 +1,8 @@ +coverage: + status: + project: + default: + threshold: 2.0% + patch: + default: + target: 50% diff --git a/doc/examples/README.md b/doc/examples/README.md deleted file mode 100644 index a9e8fc02f..000000000 --- a/doc/examples/README.md +++ /dev/null @@ -1 +0,0 @@ -need to add examples. in the meantime check out [python-rtmbot](https://github.com/slackhq/python-rtmbot/) diff --git a/docs/english/_sidebar.json b/docs/english/_sidebar.json new file mode 100644 index 000000000..6e5aa7358 --- /dev/null +++ b/docs/english/_sidebar.json @@ -0,0 +1,66 @@ +[ + { + "type": "doc", + "id": "tools/python-slack-sdk/index", + "label": "Python Slack SDK", + "className": "sidebar-title" + }, + { + "type": "html", + "value": "
" + }, + "tools/python-slack-sdk/installation", + "tools/python-slack-sdk/web", + "tools/python-slack-sdk/webhook", + "tools/python-slack-sdk/socket-mode", + "tools/python-slack-sdk/oauth", + "tools/python-slack-sdk/audit-logs", + "tools/python-slack-sdk/rtm", + "tools/python-slack-sdk/scim", + { "type": "html", "value": "
" }, + { + "type": "category", + "label": "Legacy slackclient v2", + "items": [ + "tools/python-slack-sdk/legacy/index", + "tools/python-slack-sdk/legacy/auth", + "tools/python-slack-sdk/legacy/basic_usage", + "tools/python-slack-sdk/legacy/conversations", + "tools/python-slack-sdk/legacy/real_time_messaging", + "tools/python-slack-sdk/legacy/faq", + "tools/python-slack-sdk/legacy/changelog" + ] + }, + "tools/python-slack-sdk/v3-migration", + { "type": "html", "value": "
" }, + { + "type": "category", + "label": "Tutorials", + "items": [ + "tools/python-slack-sdk/tutorial/uploading-files", + "tools/python-slack-sdk/tutorial/understanding-oauth-scopes" + ] + }, + { "type": "html", "value": "
" }, + { + "type": "link", + "label": "Reference", + "href": "https://docs.slack.dev/tools/python-slack-sdk/reference/index.html" + }, + { "type": "html", "value": "
" }, + { + "type": "link", + "label": "Release notes", + "href": "https://github.com/slackapi/python-slack-sdk/releases" + }, + { + "type": "link", + "label": "Code on GitHub", + "href": "https://github.com/SlackAPI/python-slack-sdk" + }, + { + "type": "link", + "label": "Contributors Guide", + "href": "https://github.com/SlackAPI/python-slack-sdk/blob/main/.github/contributing.md" + } +] diff --git a/docs/english/audit-logs.md b/docs/english/audit-logs.md new file mode 100644 index 000000000..1d8b930ce --- /dev/null +++ b/docs/english/audit-logs.md @@ -0,0 +1,103 @@ +# Audit Logs API client + +The [Audit Logs API](/admins/audit-logs-api) is a set of APIs that you can use to monitor what's happening in your [Enterprise Grid](/enterprise) organization. + +The Audit Logs API can be used by Security Information and Event Management (SIEM) tools to provide an analysis of how your Slack organization is being accessed. You can also use this API to write your own apps to see how members of your organization are using Slack. + +You'll need a valid token in order to use the Audit Logs API. In addition, the Slack app using the Audit Logs API needs to be installed in the Enterprise Grid organization, not an individual workspace within the organization. + +--- + +## AuditLogsClient {#auditlogsclient} + +An OAuth token with [the admin scope](/reference/scopes/admin) is required to access this API. + +You'll likely use the `/logs` endpoint as it's the essential part of this API. + +To learn about the available parameters for this endpoint, check out [using the Audit Logs API](/admins/audit-logs-api). You can also learn more about the data structure of `api_response.typed_body` from [the class source code](https://github.com/slackapi/python-slack-sdk/blob/main/slack_sdk/audit_logs/v1/logs.py). + +``` python +import os +from slack_sdk.audit_logs import AuditLogsClient + +client = AuditLogsClient(token=os.environ["SLACK_ORG_ADMIN_USER_TOKEN"]) + +api_response = client.logs(action="user_login", limit=1) +api_response.typed_body # slack_sdk.audit_logs.v1.LogsResponse +``` + +If you would like to access `/schemes` or `/actions`, you can use the +following methods: + +``` python +api_response = client.schemas() +api_response = client.actions() +``` + +## AsyncAuditLogsClient {#asyncauditlogsclient} + +If you are keen to use asyncio for SCIM API calls, we offer AsyncSCIMClient for it. This client relies on aiohttp library. + +``` python +from slack_sdk.audit_logs.async_client import AsyncAuditLogsClient +client = AsyncAuditLogsClient(token=os.environ["SLACK_ORG_ADMIN_USER_TOKEN"]) + +api_response = await client.logs(action="user_login", limit=1) +api_response.typed_body # slack_sdk.audit_logs.v1.LogsResponse +``` + +--- + +## RetryHandler {#retryhandler} + +With the default settings, only `ConnectionErrorRetryHandler` with its default configuration (=only one retry in the manner of [exponential backoff and jitter](https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/)) is enabled. The retry handler retries if an API client encounters a connectivity-related failure (e.g., connection reset by peer). + +To use other retry handlers, you can pass a list of `RetryHandler` to the client constructor. For instance, you can add the built-in `RateLimitErrorRetryHandler` this way: + +``` python +import os +from slack_sdk.audit_logs import AuditLogsClient +client = AuditLogsClient(token=os.environ["SLACK_ORG_ADMIN_USER_TOKEN"]) + +# This handler does retries when HTTP status 429 is returned +from slack_sdk.http_retry.builtin_handlers import RateLimitErrorRetryHandler +rate_limit_handler = RateLimitErrorRetryHandler(max_retry_count=1) + +# Enable rate limited error retries as well +client.retry_handlers.append(rate_limit_handler) +``` + +You can also create one on your own by defining a new class that inherits `slack_sdk.http_retry RetryHandler` (`AsyncRetryHandler` for asyncio apps) and implements required methods (internals of `can_retry` / `prepare_for_next_retry`). Check out the source code for the ones that are built in to learn how to properly implement them. + +``` python +import socket +from typing import Optional +from slack_sdk.http_retry import (RetryHandler, RetryState, HttpRequest, HttpResponse) +from slack_sdk.http_retry.builtin_interval_calculators import BackoffRetryIntervalCalculator +from slack_sdk.http_retry.jitter import RandomJitter + +class MyRetryHandler(RetryHandler): + def _can_retry( + self, + *, + state: RetryState, + request: HttpRequest, + response: Optional[HttpResponse] = None, + error: Optional[Exception] = None + ) -> bool: + # [Errno 104] Connection reset by peer + return error is not None and isinstance(error, socket.error) and error.errno == 104 + +client = AuditLogsClient( + token=os.environ["SLACK_ORG_ADMIN_USER_TOKEN"], + retry_handlers=[MyRetryHandler( + max_retry_count=1, + interval_calculator=BackoffRetryIntervalCalculator( + backoff_factor=0.5, + jitter=RandomJitter(), + ), + )], +) +``` + +For asyncio apps, `Async` prefixed corresponding modules are available. All the methods in those methods are async/await compatible. Check [the source code](https://github.com/slackapi/python-slack-sdk/blob/main/slack_sdk/http_retry/async_handler.py) for more details. diff --git a/docs/english/index.md b/docs/english/index.md new file mode 100644 index 000000000..7fdbcd6e4 --- /dev/null +++ b/docs/english/index.md @@ -0,0 +1,36 @@ +# Python Slack SDK + +The Slack Python SDK has corresponding packages for Slack APIs. They are small and powerful when used independently, and work seamlessly when used together, too. + +The Slack platform offers several APIs to build apps. Each Slack API delivers part of the capabilities from the platform, so that you can pick just those that fit your needs. + +## Features {#features} + +| Feature | Use | Package | +|---|---|---| +| [Web API](/tools/python-slack-sdk/web) | Send data to or query data from Slack using any of over 200 methods. | `slack_sdk.web`, `slack_sdk.web.async_client` | +| [Webhooks](/tools/python-slack-sdk/webhook) / `response_url` | Send a message using Incoming Webhooks or `response_url` | `slack_sdk.webhook`, `slack_sdk.webhook.async_client` | +| [Socket Mode](/tools/python-slack-sdk/socket-mode) | Receive and send messages over Socket Mode connections. | `slack_sdk.socket_mode` | +| [OAuth](/tools/python-slack-sdk/oauth) | Setup the authentication flow using V2 OAuth, OpenID Connect for Slack apps. | `slack_sdk.oauth` | +| [Audit Logs API](/tools/python-slack-sdk/audit-logs) | Receive audit logs API data. | `slack_sdk.audit_logs` | +| [SCIM API](/tools/python-slack-sdk/scim) | Utilize the SCIM APIs for provisioning and managing user accounts and groups. | `slack_sdk.scim` | +| [RTM API](/tools/python-slack-sdk/rtm) | Listen for incoming messages and a limited set of events happening in Slack, using WebSocket. | `slack_sdk.rtm_v2` | +| Request Signature Verification | Verify incoming requests from the Slack API servers. | `slack_sdk.signature` | +| UI Builders | Construct UI components using easy-to-use builders. | `slack_sdk.models` | + +You can also view the [Python module documents](https://docs.slack.dev/tools/python-slack-sdk/reference)! + +## Getting help {#getting-help} + +These docs have lots of information on the Python Slack SDK. There's also an in-depth Reference section. Please explore! + +If you get stuck, we're here to help. The following are the best ways to get assistance working through your issue: + +* [Issue Tracker](http://github.com/slackapi/python-slack-sdk/issues) for questions, bug reports, feature requests, and general discussion related to the Python Slack SDK. Try searching for an existing issue before creating a new one. +* [Email](mailto:support@slack.com) our developer support team: `support@slack.com`. + +## Contributing {#contributing} + +These docs live within the [Python Slack SDK](https://github.com/slackapi/python-slack-sdk) repository and are open source. + +We welcome contributions from everyone! Please check out our [Contributor's Guide](https://github.com/slackapi/python-slack-sdk/blob/main/.github/contributing.md) for how to contribute in a helpful and collaborative way. diff --git a/docs/english/installation.md b/docs/english/installation.md new file mode 100644 index 000000000..17bae95d2 --- /dev/null +++ b/docs/english/installation.md @@ -0,0 +1,205 @@ +# Installation + +This package supports Python 3.7 and higher. We recommend using [PyPI](https://pypi.python.org/pypi) for installation. Run the following command: + +```bash +pip install slack-sdk +``` + +Alternatively, you can always pull the source code directly into your project: + +```bash +git clone https://github.com/slackapi/python-slack-sdk.git +cd python-slack-sdk +python3 -m venv .venv +source .venv/bin/activate +pip install -U pip +pip install -e . # install the SDK project into the virtual env +``` + +Create a `./test.py` file with the following: + +```python title="test.py" +# test.py +import sys +# Enable debug logging +import logging +logging.basicConfig(level=logging.DEBUG) +# Verify it works +from slack_sdk import WebClient +client = WebClient() +api_response = client.api_test() +``` + +Then, run the script: + +```bash +python test.py +``` + +It's also good to try on the Python REPL. + +## Access tokens {#handling-tokens} + +Making calls to the Slack API often requires a [token](/authentication/tokens) with associated scopes that grant access to resources. Collecting a token can be done from app settings or with an OAuth installation depending on your app's requirements. + +**Always keep your access tokens safe.** + +The OAuth token you use to call the Slack Web API has access to the data on the workspace where it is installed. Depending on the scopes granted to the token, it potentially has the ability to read and write data. Treat these tokens just as you would a password — don't publish them, don't check them into source code, and don't share them with others. + +:::danger[Never do the following] + +```python +# don't do this! +token = 'xoxb-111-222-xxxxx' +``` + +::: + +We recommend you pass tokens in as environment variables, or store them in a database that is accessed at runtime. You can add a token to the environment by starting your app as follows: + +```python +SLACK_BOT_TOKEN="xoxb-111-222-xxxxx" python myapp.py +``` + +Then, retrieve the key as follows: + +```python +import os +SLACK_BOT_TOKEN = os.environ["SLACK_BOT_TOKEN"] +``` + +Refer to our [best practices for security](/security) page for more information. + +## Installing on a single workspace {#single-workspace} + +If you're building an application for a single Slack workspace, there's no need to build out the entire OAuth flow. Once you've set up your features, click the **Install App to Team** button on the **Install App** page. If you add new permission scopes or Slack app features after an app has been installed, you must reinstall the app to your workspace for the changes to take effect. + +Refer to the [Slack quickstart](/quickstart) guide for more details. + +## Installing on multiple workspaces {#multi-workspace} + +If you intend for an app to be installed on multiple Slack workspaces, you will need to handle this installation via the industry-standard OAuth protocol. Read more about [installing with OAuth](/authentication/installing-with-oauth). + +The OAuth exchange is facilitated via HTTP and requires a webserver; in this example, we'll use [Flask](https://flask.palletsprojects.com/). + +To configure your app for OAuth, you'll need a client ID, a client secret, and a set of one or more scopes that will be applied to the token once it is granted. The client ID and client secret are available from the [app page](https://api.slack.com/apps). The scopes are determined by the functionality of the app — every method you wish to access has a corresponding scope, and your app will need to request that scope in order to be able to access the method. Review the full list of [OAuth scopes](/reference/scopes). + +```python +import os +from slack_sdk import WebClient +from flask import Flask, request + +client_id = os.environ["SLACK_CLIENT_ID"] +client_secret = os.environ["SLACK_CLIENT_SECRET"] +oauth_scope = os.environ["SLACK_SCOPES"] + +app = Flask(__name__) +``` + +### The OAuth initiation link {#oauth-link} + +To begin the OAuth flow that will install your app on a workspace, you'll need to provide the user with a link to the Slack OAuth page. This can be a simple link to `https://slack.com/oauth/v2/authorize` with the +`scope` and `client_id` query parameters. + +This link directs the user to the OAuth acceptance page, where the user will review and accept or decline the permissions your app is requesting as defined by the scope(s). + +```python +@app.route("/slack/install", methods=["GET"]) +def pre_install(): + state = "randomly-generated-one-time-value" + return '' \ + 'Add to Slack' +``` + +### The OAuth completion page {#oauth-completion} + +Once the user has agreed to the permissions you've requested, Slack will redirect the user to your auth completion page, which includes a `code` query string parameter. You'll use the `code` parameter to call the [`oauth.v2.access`](/reference/methods/oauth.v2.access) API method that will grant you the token. + +```python +@app.route("/slack/oauth_redirect", methods=["GET"]) +def post_install(): + # Verify the "state" parameter + + # Retrieve the auth code from the request params + code_param = request.args['code'] + + # An empty string is a valid token for this request + client = WebClient() + + # Request the auth tokens from Slack + response = client.oauth_v2_access( + client_id=client_id, + client_secret=client_secret, + code=code_param + ) +``` + +A successful request to the `oauth.v2.access` API method will yield a JSON payload with at least one token: a bot token that begins with `xoxb`. + +```python +@app.route("/slack/oauth_redirect", methods=["GET"]) +def post_install(): + # Verify the "state" parameter + + # Retrieve the auth code from the request params + code_param = request.args['code'] + + # An empty string is a valid token for this request + client = WebClient() + + # Request the auth tokens from Slack + response = client.oauth_v2_access( + client_id=client_id, + client_secret=client_secret, + code=code_param + ) + print(response) + + # Save the bot token to an environmental variable or to your data store + # for later use + os.environ["SLACK_BOT_TOKEN"] = response['access_token'] + + # Don't forget to let the user know that OAuth has succeeded! + return "Installation is completed!" + +if __name__ == "__main__": + app.run("localhost", 3000) +``` + +Once your user has completed the OAuth flow, you'll be able to use the provided tokens to call any of the Slack Web API methods that require an access token. + +Refer to the [basic usage](/tools/python-slack-sdk/legacy/basic_usage) page for more examples. + +## Installation troubleshooting {#troubleshooting} + +We recommend using [virtualenv (venv)](https://docs.python.org/3/tutorial/venv.html) to set up your +Python runtime. + +```bash +# Create a dedicated virtual env for running your Python scripts +python -m venv .venv + +# Run .venv\Scripts\activate on Windows OS +source .venv/bin/activate + +# Install slack_sdk PyPI package +pip install "slack_sdk>=3.0" + +# Set your token as an env variable (`set` command for Windows OS) +export SLACK_BOT_TOKEN=xoxb-*** +``` + +Then, verify the following code works on the Python REPL: + +```python +import os +import logging +from slack_sdk import WebClient +logging.basicConfig(level=logging.DEBUG) +client = WebClient(token=os.environ["SLACK_BOT_TOKEN"]) +res = client.api_test() +``` + +As the `slack` package is deprecated, we recommend switching to `slack_sdk` package. That being said, the code you're working on may be still using the old package. If you encounter an error saying `AttributeError: module 'slack' has no attribute 'WebClient'`, run `pip list`. If you find both `slack_sdk` and `slack` in the output, try removing `slack` by `pip uninstall slack` and reinstalling `slack_sdk`. diff --git a/docs/english/legacy/auth.md b/docs/english/legacy/auth.md new file mode 100644 index 000000000..23fe0aa23 --- /dev/null +++ b/docs/english/legacy/auth.md @@ -0,0 +1,135 @@ +# Tokens & installation + +:::danger[The [`slackclient`](https://pypi.org/project/slackclient/) PyPI project is in maintenance mode and the [slack-sdk](https://pypi.org/project/slack-sdk/) project is its successor.] + +The v3 SDK provides additional features such as Socket Mode, OAuth flow, SCIM API, Audit Logs API, better asyncio support, retry handlers, and more. + +::: + +## Access tokens {#handling-tokens} + +**Always keep your access tokens safe.** + +The OAuth token you use to call the Slack Web API has access to the data on the workspace where it is installed. Depending on the scopes granted to the token, it potentially has the ability to read and write data. Treat these tokens just as you would a password — don't publish them, don't check them into source code, and don't share them with others. + +Never do the following: + +``` python +token = 'xoxb-111-222-xxxxx' +``` + +We recommend you pass tokens in as environment variables, or store them in a database that is accessed at runtime. You can add a token to the environment by starting your app as follows: + +``` python +SLACK_BOT_TOKEN="xoxb-111-222-xxxxx" python myapp.py +``` + +Then, retrieve the key as follows: + +``` python +import os +SLACK_BOT_TOKEN = os.environ["SLACK_BOT_TOKEN"] +``` + +Refer to our [best practices for security](/security) page for more information. + +## Installing on a single workspace {#single-workspace} + +If you're building an application for a single Slack workspace, there's no need to build out the entire OAuth flow. Once you've set up your features, click the **Install App to Team** button on the **Install App** page. If you add new permission scopes or Slack app features after an app has been installed, you must reinstall the app to your workspace for the changes to take effect. + +Refer to the [quickstart](/quickstart) guide for more details. + +## Installing on multiple workspaces {#multi-workspace} + +If you intend for an app to be installed on multiple Slack workspaces, you will need to handle this installation via the industry-standard OAuth protocol. Read more about [installing with OAuth](/authentication/installing-with-oauth). + +The OAuth exchange is facilitated via HTTP and requires a webserver; in this example, we'll use [Flask](https://flask.palletsprojects.com/). + +To configure your app for OAuth, you'll need a client ID, a client secret, and a set of one or more scopes that will be applied to the token once it is granted. The client ID and client secret are available from the [app page](https://api.slack.com/apps). The scopes are determined by the functionality of the app — every method you wish to access has a corresponding scope, and your app will need to request that scope in order to be able to access the method. Review the full list of [OAuth scopes](/reference/scopes). + +``` python +import os +from slack import WebClient +from flask import Flask, request + +client_id = os.environ["SLACK_CLIENT_ID"] +client_secret = os.environ["SLACK_CLIENT_SECRET"] +oauth_scope = os.environ["SLACK_SCOPES"] + +app = Flask(__name__) +``` + +### The OAuth initiation link {#oauth-link} + +To begin the OAuth flow that will install your app on a workspace, you'll need to provide the user with a link to the Slack OAuth page. This can be a simple link to `https://slack.com/oauth/v2/authorize` with the +`scope` and `client_id` query parameters. + +This link directs the user to the OAuth acceptance page, where the user will review and accept or decline the permissions your app is requesting as defined by the scope(s). + +``` python +@app.route("/slack/install", methods=["GET"]) +def pre_install(): + state = "randomly-generated-one-time-value" + return '' \ + 'Add to Slack' +``` + +### The OAuth completion page {#oauth-completion} + +Once the user has agreed to the permissions you've requested, Slack will redirect the user to your auth completion page, which includes a `code` query string parameter. You'll use the `code` parameter to call the [`oauth.v2.access`](/reference/methods/oauth.v2.access) API method that will grant you the token. + +``` python +@app.route("/slack/oauth_redirect", methods=["GET"]) +def post_install(): + # Verify the "state" parameter + + # Retrieve the auth code from the request params + code_param = request.args['code'] + + # An empty string is a valid token for this request + client = WebClient() + + # Request the auth tokens from Slack + response = client.oauth_v2_access( + client_id=client_id, + client_secret=client_secret, + code=code_param + ) +``` + +A successful request to the `oauth.v2.access` API method will yield a JSON payload with at least one token: a bot token that begins with `xoxb`. + +``` python +@app.route("/slack/oauth_redirect", methods=["GET"]) +def post_install(): + # Verify the "state" parameter + + # Retrieve the auth code from the request params + code_param = request.args['code'] + + # An empty string is a valid token for this request + client = WebClient() + + # Request the auth tokens from Slack + response = client.oauth_v2_access( + client_id=client_id, + client_secret=client_secret, + code=code_param + ) + print(response) + + # Save the bot token to an environmental variable or to your data store + # for later use + os.environ["SLACK_BOT_TOKEN"] = response['access_token'] + + # Don't forget to let the user know that OAuth has succeeded! + return "Installation is completed!" + +if __name__ == "__main__": + app.run("localhost", 3000) +``` + +Once your user has completed the OAuth flow, you'll be able to use the provided tokens to call any of the Slack Web API methods that require an access token. + +Refer to the [basic usage](/tools/python-slack-sdk/legacy/basic_usage) page for more examples. diff --git a/docs/english/legacy/basic_usage.md b/docs/english/legacy/basic_usage.md new file mode 100644 index 000000000..75094dd9f --- /dev/null +++ b/docs/english/legacy/basic_usage.md @@ -0,0 +1,457 @@ +# Basic usage + +:::danger[The [`slackclient`](https://pypi.org/project/slackclient/) PyPI project is in maintenance mode and the [slack-sdk](https://pypi.org/project/slack-sdk/) project is its successor.] + +The v3 SDK provides additional features such as Socket Mode, OAuth flow, SCIM API, Audit Logs API, better async support, retry handlers, and more. + +::: + +The Slack Web API allows you to build applications that interact with Slack in more complex ways than the integrations we provide out of the box. + +Accessing Slack API methods requires an OAuth token — read more about [installing with OAuth](/authentication/installing-with-oauth). + +Each of these [API methods](/reference/methods) is fully documented on our developer site at [docs.slack.dev](/). + +## Sending a message {#sending-messages} + +One of the primary uses of Slack is posting messages to a channel using the channel ID, or as a DM to another person using their user ID. This method will handle either a channel ID or a user ID passed to the `channel` parameter. + +``` python +import logging +logging.basicConfig(level=logging.DEBUG) + +import os +from slack import WebClient +from slack.errors import SlackApiError + +slack_token = os.environ["SLACK_API_TOKEN"] +client = WebClient(token=slack_token) + +try: + response = client.chat_postMessage( + channel="C0XXXXXX", + text="Hello from your app! :tada:" + ) +except SlackApiError as e: + # You will get a SlackApiError if "ok" is False + assert e.response["error"] # str like 'invalid_auth', 'channel_not_found' +``` + +Sending an ephemeral message, which is only visible to an assigned user in a specified channel, is nearly the same as sending a regular message but with an additional `user` parameter. + +``` python +import os +from slack import WebClient + +slack_token = os.environ["SLACK_API_TOKEN"] +client = WebClient(token=slack_token) + +response = client.chat_postEphemeral( + channel="C0XXXXXX", + text="Hello silently from your app! :tada:", + user="U0XXXXXXX" +) +``` + +See the [`chat.postEphemeral`](/reference/methods/chat.postEphemeral) API method for more details. + +## Formatting messages with Block Kit {#block-kit} + +Messages posted from apps can contain more than just text; they can also include full user interfaces composed of blocks using [Block Kit](/block-kit). + +The [`chat.postMessage method`](/reference/methods/chat.postMessage) takes an optional blocks argument that allows you to customize the layout of a message. Blocks are specified in a single object literal, so just add additional keys for any optional argument. + +To send a message to a channel, use the channel's ID. For DMs, use the user's ID. + +``` python +client.chat_postMessage( + channel="C0XXXXXX", + blocks=[ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "Danny Torrence left the following review for your property:" + } + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": " \n :star: \n Doors had too many axe holes, guest in room " + + "237 was far too rowdy, whole place felt stuck in the 1920s." + }, + "accessory": { + "type": "image", + "image_url": "https://images.pexels.com/photos/750319/pexels-photo-750319.jpeg", + "alt_text": "Haunted hotel image" + } + }, + { + "type": "section", + "fields": [ + { + "type": "mrkdwn", + "text": "*Average Rating*\n1.0" + } + ] + } + ] +) +``` + +:::tip[You can use [Block Kit Builder](https://app.slack.com/block-kit-builder/) to prototype your message's look and feel.] + +::: + +## Threading messages {#threading-messages} + +Threaded messages are a way of grouping messages together to provide greater context. You can reply to a thread or start a new threaded conversation by simply passing the original message's `ts` ID in the `thread_ts` attribute when posting a message. If you're replying to a threaded message, you'll pass the `thread_ts` ID of the message you're replying to. + +A channel or DM conversation is a nearly linear timeline of messages exchanged between people, bots, and apps. When one of these messages is replied to, it becomes the parent of a thread. By default, threaded replies do not appear directly in the channel, but are instead relegated to a kind of forked timeline descending from the parent message. + +``` python +response = client.chat_postMessage( + channel="C0XXXXXX", + thread_ts="1476746830.000003", + text="Hello from your app! :tada:" +) +``` + +By default, the `reply_broadcast` parameter is set to `False`. To indicate your reply is germane to all members of a channel and therefore a notification of the reply should be posted in-channel, set the `reply_broadcast` parameter to `True`. + +``` python +response = client.chat_postMessage( + channel="C0XXXXXX", + thread_ts="1476746830.000003", + text="Hello from your app! :tada:", + reply_broadcast=True +) +``` + +:::info[While threaded messages may contain attachments and message buttons, when your reply is broadcast to the channel, it'll actually be a reference to your reply and not the reply itself.] + +When appearing in the channel, it won't contain any attachments or message buttons. Updates and deletion of threaded replies works the same as regular messages. + +::: + +Refer to the [threading messages](/messaging#threading) page for more information. + +## Updating a message {#updating-messages} + +Let's say you have a bot that posts the status of a request. When that request changes, you'll want to update the message to reflect it's state. + +``` python +response = client.chat_update( + channel="C0XXXXXX", + ts="1476746830.000003", + text="updates from your app! :tada:" +) +``` + +See the [`chat.update`](/reference/methods/chat.update) API method for formatting options and some special considerations when calling this with a bot user. + +## Deleting a message {#deleting-messages} + +Sometimes you need to delete things. + +``` python +response = client.chat_delete( + channel="C0XXXXXX", + ts="1476745373.000002" +) +``` + +See the [`chat.delete`](/reference/methods/chat.delete) API method for more +details. + +## Opening a modal {#opening-modals} + +Modals allow you to collect data from users and display dynamic information in a focused surface. Modals use the same blocks that compose messages, with the addition of an `input` block. + +``` python +# This module is available since v2.6 +from slack.signature import SignatureVerifier +signature_verifier = SignatureVerifier(os.environ["SLACK_SIGNING_SECRET"]) + +from flask import Flask, request, make_response +app = Flask(__name__) + +@app.route("/slack/events", methods=["POST"]) +def slack_app(): + if not signature_verifier.is_valid_request(request.get_data(), request.headers): + return make_response("invalid request", 403) + + if "payload" in request.form: + payload = json.loads(request.form["payload"]) + + if payload["type"] == "shortcut" \ + and payload["callback_id"] == "open-modal-shortcut": + # Open a new modal by a global shortcut + try: + api_response = client.views_open( + trigger_id=payload["trigger_id"], + view={ + "type": "modal", + "callback_id": "modal-id", + "title": { + "type": "plain_text", + "text": "Awesome Modal" + }, + "submit": { + "type": "plain_text", + "text": "Submit" + }, + "close": { + "type": "plain_text", + "text": "Cancel" + }, + "blocks": [ + { + "type": "input", + "block_id": "b-id", + "label": { + "type": "plain_text", + "text": "Input label", + }, + "element": { + "action_id": "a-id", + "type": "plain_text_input", + } + } + ] + } + ) + return make_response("", 200) + except SlackApiError as e: + code = e.response["error"] + return make_response(f"Failed to open a modal due to {code}", 200) + + if payload["type"] == "view_submission" \ + and payload["view"]["callback_id"] == "modal-id": + # Handle a data submission request from the modal + submitted_data = payload["view"]["state"]["values"] + print(submitted_data) # {'b-id': {'a-id': {'type': 'plain_text_input', 'value': 'your input'}}} + return make_response("", 200) + + return make_response("", 404) + +if __name__ == "__main__": + # export SLACK_SIGNING_SECRET=*** + # export SLACK_API_TOKEN=xoxb-*** + # export FLASK_ENV=development + # python3 app.py + app.run("localhost", 3000) +``` + +See the [`views.open`](/reference/methods/views.open) API method more details and additional parameters. + +To run the above example, the following [app configurations](https://api.slack.com/apps) are required: + +* Enable **Interactivity** with a valid Request URL: `https://{your-public-domain}/slack/events` +* Add a global shortcut with the callback ID: `open-modal-shortcut` + +## Updating and pushing modals {#updating-pushing-modals} + +You can dynamically update a view inside of a modal by calling the `views.update` API method and passing the view ID returned in the previous `views.open` API method call. + +``` python +private_metadata = "any str data you want to store" +response = client.views_update( + view_id=payload["view"]["id"], + hash=payload["view"]["hash"], + view={ + "type": "modal", + "callback_id": "modal-id", + "private_metadata": private_metadata, + "title": { + "type": "plain_text", + "text": "Awesome Modal" + }, + "submit": { + "type": "plain_text", + "text": "Submit" + }, + "close": { + "type": "plain_text", + "text": "Cancel" + }, + "blocks": [ + { + "type": "input", + "block_id": "b-id", + "label": { + "type": "plain_text", + "text": "Input label", + }, + "element": { + "action_id": "a-id", + "type": "plain_text_input", + } + } + ] + } +) +``` + +See the [`views.update`](/reference/methods/views.update) API method for more details. + +If you want to push a new view onto the modal instead of updating an existing view, see the [`views.push`](/reference/methods/views.push) API method. + +## Emoji reactions {#emoji} + +You can quickly respond to any message on Slack with an emoji reaction. Reactions can be used for any purpose: voting, checking off to-do items, showing excitement, or just for fun. + +This method adds a reaction (emoji) to an item (`file`, `file comment`, `channel message`, `group message`, or `direct message`). One of `file`, `file_comment`, or the combination of `channel` and `timestamp` must be specified. + +``` python +response = client.reactions_add( + channel="C0XXXXXXX", + name="thumbsup", + timestamp="1234567890.123456" +) +``` + +Removing an emoji reaction is basically the same format, but you'll use the `reactions.remove` API method instead of the `reactions.add` API method. + +``` python +response = client.reactions_remove( + channel="C0XXXXXXX", + name="thumbsup", + timestamp="1234567890.123456" +) +``` + +See the [`reactions.add`](/reference/methods/reactions.add) and [`reactions.remove`](/reference/methods/reactions.remove) API methods for more details. + +## Listing public channels {#listing-public-channels} + +At some point, you'll want to find out what channels are available to your app. This is how you get that list. + +``` python +response = client.conversations_list(types="public_channel") +``` + +Archived channels are included by default. You can exclude them by passing `exclude_archived=1` to your request. + +``` python +response = client.conversations_list(exclude_archived=1) +``` + +See the [`conversations.list`](/reference/methods/conversations.list) API method for more details. + +## Getting a channel's info {#get-channel-info} + +Once you have the ID for a specific channel, you can fetch information about that channel. + +``` python +response = client.conversations_info(channel="C0XXXXXXX") +``` + +See the [`conversations.info`](/reference/methods/conversations.info) API method for more details. + +## Joining a channel {#join-channel} + +Channels are the social hub of most Slack teams. Here's how you hop into one: + +``` python +response = client.conversations_join(channel="C0XXXXXXY") +``` + +If you are already in the channel, the response is slightly different. The `already_in_channel` attribute will be true, and a limited `channel` object will be returned. Bot users cannot join a channel on their own, they need to be invited by another user. + +See the [`conversations.join`](/reference/methods/conversations.join) API method for more details. + +------------------------------------------------------------------------ + +## Leaving a channel {#leave-channel} + +Maybe you've finished up all the business you had in a channel, or maybe you joined one by accident. This is how you leave a channel. + +``` python +response = client.conversations_leave(channel="C0XXXXXXX") +``` + +See the [`conversations.leave`](/reference/methods/conversations.leave) API method for more details. + +## Listing team members {#list-team-members} + +``` python +response = client.users_list() +users = response["members"] +user_ids = list(map(lambda u: u["id"], users)) +``` + +See the [`users.list`](/reference/methods/users.list) API method for more details. + +## Uploading files {#uploading-files} + +``` python +response = client.files_upload_v2( + channel="C3UKJTQAC", + file="./files.pdf", + title="Test upload" +) +``` + +See the [`files.upload`](/reference/methods/files.upload) API method for more details. + +## Calling API methods {#calling-API-methods} + +This library covers all the public endpoints as the methods in `WebClient`. That said, you may see a bit of a delay with the library release. When you're in a hurry, you can directly use the `api_call` method as below. + +``` python +import os +from slack import WebClient + +client = WebClient(token=os.environ['SLACK_API_TOKEN']) +response = client.api_call( + api_method='chat.postMessage', + json={'channel': '#random','text': "Hello world!"} +) +assert response["message"]["text"] == "Hello world!" +``` + +## Rate limits {#rate-limits} + +When posting messages to a channel, Slack allows apps to send no more than one message per channel per second. We allow bursts over that limit for short periods; however, if your app continues to exceed the limit over a longer period of time, it will be rate limited. Different API methods have other limits — be sure to check the [rate limits](/apis/web-api/rate-limits) and test that your app has a graceful fallback if it should hit those limits. + +If you go over these limits, Slack will begin returning *HTTP 429 Too Many Requests* errors, a JSON object containing the number of calls you have been making, and a *Retry-After* header containing the number of seconds until you can retry. + +Here's an example of how you might handle rate limited requests: + +``` python +import os +import time +from slack import WebClient +from slack.errors import SlackApiError + +client = WebClient(token=os.environ["SLACK_API_TOKEN"]) + +# Simple wrapper for sending a Slack message +def send_slack_message(channel, message): + return client.chat_postMessage( + channel=channel, + text=message + ) + +# Make the API call and save results to `response` +channel = "#random" +message = "Hello, from Python!" +# Do until being rate limited +while True: + try: + response = send_slack_message(channel, message) + except SlackApiError as e: + if e.response.status_code == 429: + # The `Retry-After` header will tell you how long to wait before retrying + delay = int(e.response.headers['Retry-After']) + print(f"Rate limited. Retrying in {delay} seconds") + time.sleep(delay) + response = send_slack_message(channel, message) + else: + # other errors + raise e +``` + +Refer to the [rate limits](/apis/web-api/rate-limits) page for more information. diff --git a/docs/english/legacy/changelog.md b/docs/english/legacy/changelog.md new file mode 100644 index 000000000..d58f0bf42 --- /dev/null +++ b/docs/english/legacy/changelog.md @@ -0,0 +1,399 @@ +# Changelog + +## v3.0.0 (2020-11-09) + +This is the first stable version of [slack_sdk](https://pypi.org/project/slack-sdk/) v3. The remarkable updates in this major version are: +- Newly added OAuth flow support +- Better async/sync separation for `WebClient` and `WebhookClient` +- Renamed packages (from `slack` to `slack_sdk`) with deprecation warnings + +Refer to [v3.0.0 milestone](https://github.com/slackapi/python-slack-sdk/milestone/10?closed=1) and [the docs website](/tools/python-slack-sdk/) for details. If you're a `slackclient` user, the migration guide for `slackclient` v2.x users is available at http://localhost:3000/python-slack-sdk/v3-migration. + +## v2.9.3 (2020-10-20) + +Refer to [v2.9.3 milestone](https://github.com/slackapi/python-slackclient/milestone/20?closed=1) to know the complete list of the issues resolved by this release. + +**Updates** + +- \[Block Kit\] #851 #852 Set default_type for HeaderBlock text - Thanks \@fwump38 +- \[Block Kit\] #853 #854 Enable to use input blocks in Home tab views - Thanks \@fwump38 +- \[RTMClient\] #857 #846 RTMClient does not pass timeout value to WebClient - Thanks \@Luden \@seratch + +## v2.9.2 (2020-10-09) + +Refer to [v2.9.2 milestone](https://github.com/slackapi/python-slackclient/milestone/19?closed=1) to know the complete list of the issues resolved by this release. + +**Updates** + +- \[Block Kit\] #841 Dispatch Action in Input blocks - Thanks \@seratch +- \[WebClient\] #838 Add apps.event.authorizations.list and other APIs - Thanks \@seratch +- \[WebClient\]\[WebhookClient\] #829 Improve error body parser to handle no charset responses - Thanks \@adamchainz \@seratch +- \[Block Kit\] #824 Correct text field validation in Header blocks - Thanks \@seratch + +## v2.9.1 (2020-09-23) + +Refer to [v2.9.1 milestone](https://github.com/slackapi/python-slackclient/milestone/18?closed=1) to know the complete list of the issues resolved by this release. + +**Updates** + +- \[WebClient\]\[WebhookClient\] #820 #821 #822 The proxy option in WebClient/WebhookClient no longer works - Thanks \@seratch + +## v2.9.0 (2020-09-17) + +Refer to [v2.9.0 milestone](https://github.com/slackapi/python-slackclient/milestone/17?closed=1) to know the complete list of the issues resolved by this release. + +**Updates** + +- \[WebClient\] #811 Add workflows.\* API support - Thanks \@misscoded +- \[WebClient\] #810 #809 Only set default filename in files_upload if file is an instance of str - Thanks \@csaska + +## v2.8.2 (2020-09-04) + +Refer to [v2.8.2 milestone](https://github.com/slackapi/python-slackclient/milestone/16?closed=1) to know the complete list of the issues resolved by this release. + +**Updates** + +- \[WebClient\] #795 #794 Add admin.conversations.\* API methods in WebClient/AsyncWebClient - Thanks \@ruberVulpes +- \[WebClient\] #796 Fix a link to the Static options documentation - Thanks \@Jamim + +## v2.8.1 (2020-08-28) + +Refer to [v2.8.1 milestone](https://github.com/slackapi/python-slackclient/milestone/15?closed=1) to know the complete list of the issues resolved by this release. + +**Updates** + +- \[WebClient\] #778 #779 Adding support for View objects for views.push/update/publish - Thanks \@ruberVulpes +- \[WebClient\] #786 Fix admin.conversations.restrictAccess.\* methods to match documentation - Thanks \@ruberVulpes + +## v2.8.0 (2020-08-06) + +Refer to [v2.8.0 milestone](https://github.com/slackapi/python-slackclient/milestone/14?closed=1) to know the complete list of the issues resolved by this release. + +**New Features** + +- \[WebClient\] #765 #766 Introduce AsyncWebClient/AsyncWebhookClient providing coroutines - Thanks \@seratch +- \[Block Kit\] #767 #768 Add \"header\" block support - Thanks \@mwbrooks + +**Updates** + +- \[WebClient\] #738 Add HTTP_PROXY, HTTPS_PROXY env variable support in async WebClient - Thanks \@iamtofr \@seratch +- \[WebClient\] #769 #773 Enable User-Agent to have additional info part - Thanks \@seratch +- \[WebClient\] #770 #771 Fix a bug where `files.upload`\'s file param doesn\'t accept bytes data - Thanks \@seratch + +## v2.7.3 (2020-07-20) + +Refer to [v2.7.3 milestone](https://github.com/slackapi/python-slackclient/milestone/13?closed=1) to know the complete list of the issues resolved by this release. + +**Updates** + +- \[WebClient\] #754 Fix #729 Add admin.conversations.restrictAccess.\*, conversations.mark API - Thanks \@ruberVulpes \@kian2attari +- \[WebClient\] #758 Fix #757 Add admin.usergroups.addTeams, calls.participants.remove API - Thanks \@seratch +- \[WebClient\] #727 Fix #645 Unclosed client session - Thanks \@NoAnyLove \@jourdanrodrigues +- \[WebClient\] #745 Fix #744 a validation logic bug in DatePickerElement - Thanks \@dzudi941 +- \[WebClient\] #752 Fix #733 Better error handling when getting TimeoutError in RTMClient#start() - Thanks \@liorblob \@seratch +- \[WebClient\] #751 Fix #718 by handling unexpected response body format - Thanks \@jeffbuswell \@seratch + +## v2.7.2 (2020-06-23) + +Refer to [v2.7.2 milestone](https://github.com/slackapi/python-slackclient/milestone/12?closed=1) to know the complete list of the issues resolved by this release. + +**Updates** + +- \[WebClient\] Fix #728 by adding bytearray support in files_upload (sync mode) - Thanks \@sofya-salmanova \@seratch +- \[WebClient\] #726 Fix InputBlock.hint validation failure - Thanks \@jourdanrodrigues +- \[WebClient\] #723 Correct the default value of InputBlock.label, hint - Thanks \@jourdanrodrigues + +## v2.7.1 (2020-06-04) + +This release includes the fixes for regression bugs in `WebClient` since v2.6.0. Refer to [v2.7.1 milestone](https://github.com/slackapi/python-slackclient/milestone/11?closed=1) to know the complete list of the issues resolved by this release. + +**Updates** + +- \[WebClient\] #716 #712 Support timeout in sync sync web clients - Thanks \@DanialErfanian \@seratch +- \[WebClient\] #713 Support custom SSL context in sync sync web clients - Thanks \@austinbutler +- \[WebClient\] #715 #714 Support proxy in sync sync web clients - Thanks \@austinbutler \@seratch + +## v2.7.0 (2020-06-02) + +Refer to [v2.7.0 milestone](https://github.com/slackapi/python-slackclient/milestone/6?closed=1) to know the complete list of the issues resolved by this release. + +**New Features** + +- \[WebhookClient\] #707 #270 #531 Add `WebhookClient` for Incoming Webhooks & response_url - Thanks \@seratch \@chubz \@Ambro17 + +**Updates** + +- \[WebClient\] #704 #695 Add `calls\_\*` methods to `WebClient` and `CallBlock` in Block Kit classes - Thanks \@seratch +- \[WebClient\] #710 #536 Allow Tokens to be specified per request - Thanks \@seratch +- \[WebClient\] #709 #708 Add default_to_current_conversation in conversations_select elements - Thanks \@seratch + +## v2.6.2 (2020-05-28) + +Refer to [v2.6.2 milestone](https://github.com/slackapi/python-slackclient/milestone/9?closed=1) to know the complete details of this release. + +**Updates** + +- \[WebClient\] #705 WebClient\'s paginated API calls may fail with no params - Thanks \@seratch + +## v2.6.1 (2020-05-24) + +This patch release is a quick fix for #701, a major issue that affected RTMClient users in v2.6.0. The malfunction was introduced by #667 trying to address #558 #619. Those issues were reopened and will be resolved by another approach. Refer to [v2.6.1 milestone](https://github.com/slackapi/python-slackclient/milestone/8) to know the complete list of the issues resolved by this release. + +**Updates** + +- \[RTMClient\] #701 RTMClient drops some messages when they come in rapid succession - Thanks \@pbrackin \@seratch + +## v2.6.0 (2020-05-21) + +Refer to [v2.6.0 milestone](https://github.com/slackapi/python-slackclient/milestone/5?closed=1) to know the complete list of the issues resolved by this release. + +**New Features** + +- \[Block Kit\] #659 Add complete supports for Block Kit components and fixed a few existing bugs as well (#500 #519 #623 #632 #635 #639 #676 #699) - Thanks \@seratch \@diurnalist \@ruberVulpes \@jeremyschulman \@e271828- \@RodneyU215 +- \[Signature\] #686 Add slack.signature.SignatureVerifier for request verification - Thanks \@seratch +- \[WebClient\] #682 Add missing Grid admin APIs (`admin.usergroups.\*`, `admin.users.\*`, `admin.apps.\*`) - Thanks \@stevengill \@seratch + +**Updates** + +- \[WebClient\]\[RTMClient\] Fixed a bunch of the currency issues this SDK had (#429 #463 #492 #497 #530 #569 #605 #613 #626 #630 #631 #633 #669) - Thanks \@seratch \@aaguilartablada \@aoberoi \@stevengill \@marshallino16 +- \[WebClient\] #681 #560 Enable using bool values for request parameters - Thanks \@roman-kachanovsky \@seratch +- \[WebClient\] #661 #678 Improve handling of required \"ids\" parameters (e.g., channel_ids, users) - Thanks \@seratch +- \[WebClient\] #680 Add non-conversation API deprecation warnings - Thanks \@seratch +- \[WebClient\] #671 #670 Enable passing None values for request parameters (they used to result in errors) - Thanks \@yuji38kwmt \@seratch +- \[WebClient\] #673 Fix #672 files.upload fails with a filepath containing multi byte chars - Thanks \@yuji38kwmt \@seratch +- \[WebClient\] #656 Fix #594 preview_image for files.remote.add API method is not properly supported - Thanks \@Eothred \@seratch +- \[Maintenance\] #618 Add py.typed file to package distribution - Thanks \@JKillian +- \[WebClient\] #599 Strip token string parameters of whitespace - Thanks \@TheFrozenFire +- \[WebClient\] #692 Fix superfluous_charset warnings since v2.4.0 - Thanks \@seratch +- \[WebClient\] #652 Update oauth_v2_access to include redirect_uri (as optional) - Thanks \@tomasreimers + +## v2.5.0 (2019-12-09) + +**New Features** + +- \[WebClient\] Adding new oauth.v2.access Web API method. #577 + +## v2.4.0 (2019-11-27) + +**New Features** + +- \[WebClient\] Adding new admin.\* Web API methods. #571 + +**Updates** + +- \[WebClient\] We\'re no longer validating token types for Web API methods. Improves compatibility with granular bot permissions. #568 (Thanks \@Smotko) +- \[WebClient\] Correcting typos in descriptions #554 (Thanks \@phamk) +- \[WebClient\] Fixed \'iteracting\' typo in library file headers #564 (Thanks \@acabey) +- \[Message Builders\] Remove value from LinkButtonElement #563 (Thanks \@pedroma) + +## v2.3.1 (2019-10-29) + +**Updates** + +- \[WebClient\] Fixing a regression that causes the client to close sessions prematurely. #544 (Thanks \@fatih-acar!) +- \[WebClient\] Adding required missing view param to views.update Web API method. #542 + +## v2.3.0 (2019-10-22) + +**New Features** + +- \[WebClient\] Adding new views.publish Web API method. #540 + +**Updates** + +- \[WebClient\] Some server responses don\'t return json. Correcting initial assumption. #540 +- \[Maintenance\] Add `py.typed` to mark the library to support type hinting #524s + +## v2.2.1 (2019-10-08) + +**Updates** + +- \[Docs\] Fix Indentation of Code Snippets in README.md #525 (Thanks \@abhishekjiitr) +- \[WebClient\] Fix Web Client custom iterator #521 (Thanks \@smaeda-ks) +- \[WebClient\] Oauth previously failed to pass along credentials properly. This is fixed now. #527 +- \[WebClient\] When a SlackApiError occurs we\'re now passing the entire SlackResponse into the exception. #527 + +## v2.2.0 (2019-09-25) + +**New Features** + +- \[WebClient\] Adding new admin and remote files API methods. #501 +- \[WebClient\] Adding new view API methods. #517 + +**Updates** + +- \[Message Builders\] Update BlockAttachment to not send invalid JSON due to fields attribute #473 (Thanks \@paul-griffith) +- \[Docs\] Add RTM section for docs v2 #477 (Thanks \@shanedewael) +- \[Docs\] Fix typo; recieved -\> received #478 (Thanks \@joakimnordling) +- \[Docs\] Fix block kit link & update docs #484 (Thanks \@clavin) +- \[RTMClient\] Return callback from `RTMClient.run_on` #490 (Thanks \@clavin) +- \[Docs\] Fix link to Auth Guide in readme #498 (Thanks \@asherf) +- \[Docs\] Fix missing word and typo #512 (Thanks \@marks) +- \[Message Builders\] bugfix for value length in button elements #514 (Thanks \@avanderm) +- \[Docs\] Fixes formatting #515 (Thanks \@vpetersson) +- \[Docs\] Improve a code snippet on README #516 (Thanks \@seratch) +- \[WebClient\] Fixed an OAuth Headers bug and made the `token` param optional. #517 + +## v2.1.0 (2019-07-01) + +**New Features** + +- Type-hinted helper classes for building messages in v2 #400 (Thanks \@paul-griffith) + +**Breaking Changes** + +- \[RTMClient\] Converted the `RTMClient#typing()` function to async #446 + +**Updates** + +- \[RTMClient\] Handle case in which aiohttp closes the websocket due to lack of ping responses. #453 (Thanks \@flyte) +- Modify package identifier in user agent to match v1.x identifier #418 (Thanks \@aoberoi) +- \[WebClient\] Fixed typo in Scheduled message #428 & #435 (Thanks \@splinterific) +- Transform install_requires of \'aiodns\' into extras_require. #440 (Thanks \@staticdev) + +**Thank you!** To everyone who has opened, commented or reacted to an issue; this project is better because of you! Thank you for helping the Slack community! + +## v2.0.0 (2019-04-29) + +[Original RFC](https://github.com/slackapi/python-slackclient/issues/384) + +[v2 PR](https://github.com/slackapi/python-slackclient/pull/394) + +**New Features** + +- Client Decomposition: We've split the client into two. + - WebClient: A HTTP client focused on Slack\'s Web API. + - RTMClient: A websocket client focused on Slack\'s RTM API. +- RTMClient: Completely redesigned, this client allows you to link your application\'s callbacks to corresponding Slack events. +- WebClient: The WebClient now provides built-in methods for Slack\'s Web API. These methods act as helpers enabling you to focus less on how the request is constructed. Here are a few things this provides: + - Basic information about each method through the docstring. + - Easy File Uploads: You can now pass in the location of a file and the library will handle opening and retrieving the file object to be transmitted. + - Token type validation: This gives you better error messaging when you\'re attempting to consume an API method that your token doesn\'t have access to. + - Constructs requests using Slack\'s preferred HTTP methods and content-types. + +**Breaking Changes:** If you\'re migrating from v1.x of slackclient to v2.x, Please follow our [migration guide](https://github.com/slackapi/python-slackclient/wiki/Migrating-to-2.x) to ensure your app continues working after updating. + +**Thank you!** This release would not have been possible without the support of our community. Thank you to everyone who has contributed to this release. + +## v1.3.1 (2019-02-28) + +- Lock websocket-client version to \< 0.55.0: temp fix for #385 + +## v1.3.0 (2018-09-11) + +**New Features** + +- Adds support for short lived tokens and automatic token refresh #347 (Thanks \@roach!) + +**Other** + +- Update RTM rate limiting comment and error message #308 (Thanks \@benoitlavigne!) +- Use logging instead of traceback #309 (Thanks \@harlowja!) +- Remove Python 3.3 from test environments #346 (Thanks \@roach!) +- Enforced linting when using VSCode. #347 (Thanks \@roach!) + +## v1.2.1 (2018-03-26) + +- Added rate limit handling for rtm connections (thanks \@jayalane!) + +## v1.2.0 (2018-03-20) + +- You can now tell the RTM client to automatically reconnect by passing `auto_reconnect=True` + +## v1.1.3 (2018-03-01) + +- Fixed another API param encoding bug. It encodes things properly now. + +## v1.1.2 (2018-01-31) + +- Fixed an encoding issue which was encoding some Web API params incorrectly + +## v1.1.1 (2018-01-30) + +- Adds HTTP response headers to `api_call` responses to expose things like rate limit info +- Moves `token` into auth header rather than request params + +## v1.1.0 (2017-11-21) + +- Adds new SlackClientError and ResponseParseError types to describe errors - thanks \@aoberoi! +- Fix Build Error (#245) - thanks \@stasfilin! +- Include email as user property (#173) - thanks \@acaire! +- Add http reply into slack login and slack connection error (#216) - thanks \@harlowja! +- Removed unused exception class (#233) +- Fix rtm_send_message bug (#225) - thanks \@kt5356! +- Allow use of custom parameters on rtm_connect() (#210) - thanks \@kamushadenes! +- Fix link to rtm.connect docs (#223) - \@sampart! + +## v1.0.9 (2017-08-31) + +- Fixed rtm_send_message ID bug introduced in 1.0.8 + +## v1.0.8 (2017-08-31) + +- Added rtm.connect support + +## v1.0.7 (2017-08-02) + +- Fixes an issue where connecting over RTM to large teams may result in "Websocket URL expired" errors +- A bunch of packaging improvements + +## v1.0.6 (2017-06-12) + +- Added proxy support (thanks \@timfeirg!) +- Tidied up docs (thanks \@schlueter!) +- Added tox settings for Python 3 testing (thanks \@cclauss!) + +## v1.0.5 (2017-01-23) + +- Allow RTM Channel.send_message to reply to a thread +- Index users by ID instead of Name (non-breaking change) +- Added timeout to api calls +- Fixed a typo about token access in auth.rst, thanks \@kelvintaywl! +- Added Message Threads to the docs + +## v1.0.4 (2016-12-15) + +- Fixed the ability to search for a user by ID + +## v1.0.3 (2016-12-13) + +- Fixed an issue causing RTM connections to fail for large teams + +## v1.0.2 (2016-09-22) + +- Removed unused ping counter +- Fixed contributor guidelines links +- Updated documentation +- Fix bug preventing API calls requiring a file ID +- Removes files from api_calls before JSON encoding, so the request is properly formatted + +## v1.0.1 (2016-03-25) + +- Fix for \_\_eq\_\_ comparison in channels using \'#\' in channel name +- Added copyright info to the LICENSE file + +## v1.0.0 (2016-02-28) + +- The `api_call` function now returns a decoded JSON object, rather than a JSON encoded string +- Some `api_call` calls now call actions on the parent server object: + - `dm.open` + - `mpdm.open`, `groups.create`, `groups.createChild` + - `channels.create`, `channels.join` + +## v0.18.0 (2016-02-21) + +- Moves to use semver for versioning +- Adds support for private groups and MPDMs +- Switches to use requests instead of urllib +- Gets Travis CI integration working +- Fixes some formatting issues so the code will work for python 2.6 +- Cleans up some unused imports, some PEP-8 fixes and a couple bad default args fixes + +## v0.17.0 (2016-02-15) + +- Fixes the server so that it doesn\'t add duplicate users or channels to its internal lists, https://github.com/slackapi/python-slackclient/commit/0cb4bcd6e887b428e27e8059b6278b86ee661aaa +- README updates: + - Updates the URLs pointing to Slack docs for configuring authentication, https://github.com/slackapi/python-slackclient/commit/7d01515cebc80918a29100b0e4793790eb83e7b9 + - s/channnels/channels, https://github.com/slackapi/python-slackclient/commit/d45285d2f1025899dcd65e259624ee73771f94bb + - Adds users to the local cache when they join the team, https://github.com/slackapi/python-slackclient/commit/f7bb8889580cc34471ba1ddc05afc34d1a5efa23 + - Fixes urllib py 2/3 compatibility, https://github.com/slackapi/python-slackclient/commit/1046cc2375a85a22e94573e2aad954ba7287c886 diff --git a/docs/english/legacy/conversations.md b/docs/english/legacy/conversations.md new file mode 100644 index 000000000..e617273ae --- /dev/null +++ b/docs/english/legacy/conversations.md @@ -0,0 +1,123 @@ +# Conversations API + +:::danger[The [`slackclient`](https://pypi.org/project/slackclient/) PyPI project is in maintenance mode and the [slack-sdk](https://pypi.org/project/slack-sdk/) project is its successor.] + +The v3 SDK provides additional features such as Socket Mode, OAuth flow, SCIM API, Audit Logs API, better async support, retry handlers, and more. + +::: + +The Slack Conversations API provides your app with a unified interface to work with all the channel-like things encountered in Slack: public channels, private channels, direct messages, group direct messages, and shared channels. + +Refer to [using the Conversations API](/apis/web-api/using-the-conversations-api) for more information. + +## Direct messages {#direct-messages} + +The `conversations.open` API method opens either a 1:1 direct message with a single user or a multi-person direct message, depending on the number of users supplied to the `users` parameter. (For public or private channels, use the `conversations.create` API method.) + +Provide a `users` parameter as an array with 1-8 user IDs to open or resume a conversation. Providing only 1 ID will create a direct message. providing more IDs will create a new multi-party direct message or will resume an existing conversation. + +Subsequent calls with the same set of users will return the already existing conversation. + +``` python +import os +from slack import WebClient + +client = WebClient(token=os.environ["SLACK_API_TOKEN"]) +response = client.conversations_open(users=["W123456789", "U987654321"]) +``` + +See the [`conversations.open`](/reference/methods/conversations.open) API method for additional details. + +## Creating channels {#creating-channels} + +Creates a new channel, either public or private. The `name` parameter is required and may contain numbers, letters, hyphens, or underscores, and must contain fewer than 80 characters. To make the channel private, set the optional `is_private` parameter to `True`. + +``` python +import os +from slack import WebClient +from time import time + +client = WebClient(token=os.environ["SLACK_API_TOKEN"]) +channel_name = f"my-private-channel-{round(time())}" +response = client.conversations_create( + name=channel_name, + is_private=True +) +channel_id = response["channel"]["id"] +response = client.conversations_archive(channel=channel_id) +``` + +See the [`conversations.create`](/reference/methods/conversations.create) API method for additional details. + +## Getting conversation information {#more-information} + +To retrieve a set of metadata about a channel (public, private, DM, or multi-party DM), use the `conversations.info` API method. The `channel` parameter is required and must be a valid channel ID. The optional `include_locale` boolean parameter will return locale data, which may be useful if you wish to return localized responses. The `include_num_members` boolean parameter will return the number of people in a channel. + +``` python +import os +from slack import WebClient + +client = WebClient(token=os.environ["SLACK_API_TOKEN"]) +response = client.conversations_info( + channel="C031415926", + include_num_members=1 +) +``` + +See the [`conversations.info`](/reference/methods/conversations.info) API method for more details. + +## Listing conversations {#listing-conversations} + +To get a list of all the conversations in a workspace, use the `conversations.list` API method. By default, only public conversations are returned. Use the `types` parameter specify which types of conversations you're interested in. Note that `types` is a string of comma-separated values. + +``` python +import os +from slack import WebClient + +client = WebClient(token=os.environ["SLACK_API_TOKEN"]) +response = client.conversations_list() +conversations = response["channels"] +``` + +Use the `types` parameter to request additional channels, including `public_channel`, `private_channel`, `mpdm`, and `dm`. This parameter is a string of comma-separated values. + +``` python +import os +from slack import WebClient + +client = WebClient(token=os.environ["SLACK_API_TOKEN"]) +response = client.conversations_list( + types="public_channel, private_channel" +) +``` + +See the [`conversations.list`](/reference/methods/conversations.list) API method for more details. + +## Getting members of a conversation {#get-members} + +To get a list of members for a conversation, use the `conversations.members` API method with the required `channel` parameter. + +``` python +import os +from slack import WebClient + +client = WebClient(token=os.environ["SLACK_API_TOKEN"]) +response = client.conversations_members(channel="C16180339") +user_ids = response["members"] +``` + +See the [`conversations.members`](/reference/methods/conversations.members) API method for more details. + +## Leaving a conversation {#leave-conversations} + +To leave a conversation, use the `conversations.leave` API method with the required `channel` parameter containing the ID of the channel to leave. + +``` python +import os +from slack import WebClient + +client = WebClient(token=os.environ["SLACK_API_TOKEN"]) +response = client.conversations_leave(channel="C27182818") +``` + +See the [`conversations.leave`](/reference/methods/conversations.leave) API method for more details. diff --git a/docs/english/legacy/faq.md b/docs/english/legacy/faq.md new file mode 100644 index 000000000..bc2d70fa0 --- /dev/null +++ b/docs/english/legacy/faq.md @@ -0,0 +1,70 @@ +# FAQs + +:::danger[The [`slackclient`](https://pypi.org/project/slackclient/) PyPI project is in maintenance mode and the [slack-sdk](https://pypi.org/project/slack-sdk/) project is its successor.] + +The v3 SDK provides additional features such as Socket Mode, OAuth flow, SCIM API, Audit Logs API, better async support, retry handlers, and more. + +::: + +## Why can't I install `slackclient`? + +We recommend using [virtualenv (venv)](https://docs.python.org/3/tutorial/venv.html) to set up your +Python runtime as follows: + +``` bash +# Create a dedicated virtual env for running your Python scripts +python -m venv env + +# Run env\Scripts\activate on Windows OS +source env/bin/activate + +# Install slackclient PyPI package +pip install "slackclient>=2.0" + +# Set your token as an env variable (`set` command for Windows OS) +export SLACK_API_TOKEN=xoxb-*** +``` + +Then, verify the following code works on the Python REPL (you can start it using just `python`): + +``` python +import os +import logging +from slack import WebClient +logging.basicConfig(level=logging.DEBUG) +client = WebClient(token=os.environ["SLACK_API_TOKEN"]) +res = client.api_test() +``` + +If you encounter an error saying +`AttributeError: module 'slack' has no attribute 'WebClient'`, run +`pip list`. If you find both `slackclient` and `slack` in the output, +try removing `slack` by `pip uninstall slack` and reinstalling +`slackclient`. + +## Should I go with `run_async`? + +For most cases, we recommend going with `run_async=False` mode. So, the default is `False`. + +If your application turns `run_async` on, the app should follow efficient ways to use [asyncio](https://docs.python.org/3/library/asyncio.html)'s non-blocking event loops and [aiohttp](https://docs.aiohttp.org/en/stable/). Also, consider using async frameworks and their appropriate runtime. Running event loops along with Flask or similar may not be a good fit. + +If you have to simultaneously run `WebClient` with `run_async=True` outside an event loop for some reason, sharing a single `WebClient` instance doesn't work for you. Create an instance every time you run the code. The `run_async=False` mode doesn't have such issues. + +## What if I found a bug? + +That's great! Thank you. Let us know by [creating an issue](https://github.com/slackapi/python-slack-sdk/issues/new/choose), or if you're feeling particularly ambitious, why not submit a pull request with a bug fix? Check out our contributor's guide [here](https://github.com/SlackAPI/python-slack-sdk/blob/main/.github/contributing.md). + +## What if I have a feature suggestion? + +There's always something more that could be added! Let us know by [creating an issue](https://github.com/slackapi/python-slack-sdk/issues/new/choose) to start a discussion around the proposed feature. If you're feeling particularly ambitious, why not write the feature yourself, and submit a pull request? We love feedback and we also love help from our amazing community of developers! + +## How do I contribute? + +What an excellent question. First of all, please have a look at our +contributor's guide [here](https://github.com/SlackAPI/python-slack-sdk/blob/main/.github/contributing.md). + +All done? Great! While we're super excited to incorporate your new feature, there are a couple of things we want to make sure you've given thought to: +* Please include unit tests for your new code. But don't just aim to increase the test coverage, rather, we expect you to have written thoughtful tests that ensure your new feature will continue to work as expected, and to help future contributors to ensure they don't break it! +* Please document your new feature. Think about concrete use cases for your feature, and add a section to the appropriate document, including a complete sample program that demonstrates your feature. + +Including these two items with your pull request will totally make our day - and, more importantly, your future users' days! diff --git a/docs/english/legacy/index.md b/docs/english/legacy/index.md new file mode 100644 index 000000000..e2ca20613 --- /dev/null +++ b/docs/english/legacy/index.md @@ -0,0 +1,57 @@ +# Overview + +:::danger[The [`slackclient`](https://pypi.org/project/slackclient/) PyPI project is in maintenance mode and the [slack-sdk](https://pypi.org/project/slack-sdk/) project is its successor.] + +The v3 SDK provides additional features such as Socket Mode, OAuth flow, SCIM API, Audit Logs API, better async support, retry handlers, and more. + +::: + +Refer to the [migration guide](/tools/python-slack-sdk/v3-migration) to learn how to smoothly migrate your existing code. + +Slack APIs allow anyone to build full featured integrations that extend and expand the capabilities of your Slack workspace. These APIs allow you to build applications that interact with Slack just like the people on your team. They can post messages, respond to events that happen, and build complex UIs for getting work done. + +To make it easier for Python programmers to build Slack applications, we've provided this open source SDK that will help you get started building Python apps as quickly as possible. The current version is built for Python 3.7 and higher. + +## Slack platform basics {#platform-basics} + +If you're new to the Slack platform, we have a general purpose [quickstart guide](/quickstart) that isn't specific to any language or framework. Its a great place to learn all about the concepts that go into building a great Slack app. + +Before you get started building on the Slack platform, you need to set up [your app's configuration](https://api.slack.com/apps/new). This is where you define things like your apps permissions and the endpoints that Slack should use for interacting with the backend you'll build using Python. + +The app configuration page is also where you'll acquire the OAuth token you'll use to call Slack API methods. Treat this token with care, just like you would a password, because it has access to your workspace and can potentially read and write data to and from it. + +## Installation {#installation} + +We recommend using [PyPI](https://pypi.python.org/pypi) to install as follows: + +``` bash +pip install slackclient +``` + +Of course, you can always pull the source code directly into your project like this: + +``` bash +git clone https://github.com/slackapi/python-slackclient.git +``` + +And then, save a few lines of code as `./test.py` like so: + +``` python +# test.py +import sys +# Load the local source directly +sys.path.insert(1, "./python-slackclient") +# Enable debug logging +import logging +logging.basicConfig(level=logging.DEBUG) +# Verify it works +from slack import WebClient +client = WebClient() +api_response = client.api_test() +``` + +Run the code as follows: + +``` bash +python test.py +``` diff --git a/docs/english/legacy/real_time_messaging.md b/docs/english/legacy/real_time_messaging.md new file mode 100644 index 000000000..3b16e3ecd --- /dev/null +++ b/docs/english/legacy/real_time_messaging.md @@ -0,0 +1,98 @@ +# Real Time Messaging (RTM) + +:::danger[The [`slackclient`](https://pypi.org/project/slackclient/) PyPI project is in maintenance mode and the [slack-sdk](https://pypi.org/project/slack-sdk/) project is its successor.] + +The v3 SDK provides additional features such as Socket Mode, OAuth flow, SCIM API, Audit Logs API, better async support, retry handlers, and more. + +::: + +The [Legacy Real Time Messaging (RTM) API](/legacy/legacy-rtm-api) is a WebSocket-based API that allows you to receive events from Slack in real time and to send messages as users. + +If you prefer events to be pushed to your app, we recommend using the HTTP-based [Events API](/apis/events-api) instead. The Events API contains some events that aren't supported in the Legacy RTM API (such as the [app_home_opened event](/reference/events/app_home_opened)), and it supports most of the event types in the Legacy RTM API. If you'd like to use the Events API, you can use the [Python Slack Events Adaptor](https://github.com/slackapi/python-slack-events-api). + +The RTMClient allows apps to communicate with the Legacy RTM API. + +The event-driven architecture of this client allows you to simply link callbacks to their corresponding events. When an event occurs, this client executes your callback while passing along any information it receives. We also give you the ability to call our web client from inside your callbacks. + +In our example below, we watch for a [message event](/reference/events/message) that contains \"Hello\" and if it's received, we call the `say_hello()` function. We then issue a call to the web client to post back to the channel saying \"Hi\" to the user. + +## Configuring the RTM API {#configuration} + +Events using the Legacy RTM API **must** use a Slack app with a plain `bot` scope. + +If you already have a Slack app with a plain `bot` scope, you can use those credentials. If you don't and need to use the Legacy RTM API, you can create a Slack app [here](https://api.slack.com/apps?new_classic_app=1). Even if the Slack app configuration pages encourage you to upgrade to a newer permission model, don't upgrade it and continue using the \"classic\" bot permission. + +## Connecting to the RTM API {#connecting} + +``` python +import os +from slack import RTMClient + +@RTMClient.run_on(event="message") +def say_hello(**payload): + data = payload['data'] + web_client = payload['web_client'] + + if 'Hello' in data['text']: + channel_id = data['channel'] + thread_ts = data['ts'] + user = data['user'] # This is not username but user ID (the format is either U*** or W***) + + web_client.chat_postMessage( + channel=channel_id, + text=f"Hi <@{user}>!", + thread_ts=thread_ts + ) + +slack_token = os.environ["SLACK_API_TOKEN"] +rtm_client = RTMClient(token=slack_token) +rtm_client.start() +``` + +## The `rtm.start` vs. `rtm.connect` API methods {#rtm-methods} + +By default, the RTM client uses the [`rtm.connect`](/reference/methods/rtm.connect) API method to establish a WebSocket connection with Slack. The response contains basic information about the team and WebSocket URL. + +If you'd rather use the [`rtm.start`](/reference/methods/rtm.start) API method to establish the connection, which provides more information about the conversations and users on the team, you can set the `connect_method` option to `rtm.start` when instantiating the RTM Client. Note that on larger teams, use of `rtm.start` can be slow and unreliable. + +``` python +import os +from slack import RTMClient + +@RTMClient.run_on(event="message") +def say_hello(**payload): + data = payload['data'] + web_client = payload['web_client'] + if 'text' in data and 'Hello' in data['text']: + channel_id = data['channel'] + thread_ts = data['ts'] + user = data['user'] # This is not username but user ID (the format is either U*** or W***) + + web_client.chat_postMessage( + channel=channel_id, + text=f"Hi <@{user}>!", + thread_ts=thread_ts + ) + +slack_token = os.environ["SLACK_API_TOKEN"] +rtm_client = RTMClient( + token=slack_token, + connect_method='rtm.start' +) +rtm_client.start() +``` + +See the [`rtm.connect`](/reference/methods/rtm.connect) and [`rtm.start`](/reference/methods/rtm.start) API methods for more details. + +## RTM events {#rtm-events} + +``` javascript +{ + 'type': 'message', + 'ts': '1358878749.000002', + 'user': 'U023BECGF', + 'text': 'Hello' +} +``` + +Refer to the [Legacy RTM API](/legacy/legacy-rtm-api) page for more information. diff --git a/docs/english/oauth.md b/docs/english/oauth.md new file mode 100644 index 000000000..61b9af98f --- /dev/null +++ b/docs/english/oauth.md @@ -0,0 +1,251 @@ +# OAuth modules + +This page explains how to handle the Slack OAuth flow. If you're looking for a much easier way to do this, check out [Bolt for Python](https://github.com/slackapi/bolt-python), a full-stack Slack app framework. With Bolt, you won't need to implement most of the following code on your own. + +Refer to the [Python document for this module](https://docs.slack.dev/tools/python-slack-sdk/reference) for more details. + +## App installation flow {#app-installation} + +OAuth allows a user in any Slack workspace to install your app. At the end of the OAuth flow, your app gains an access token. Refer to the [installing with OAuth](/authentication/installing-with-oauth) guide for details. + +The Python Slack SDK provides the necessary modules for building the OAuth flow. + +### Starting an OAuth flow {#oauth-flow} + +The first step of the OAuth flow is to redirect a Slack user to [authorize](https://slack.com/oauth/v2/authorize) with a valid `state` parameter. To implement this process, you can use the following modules. + +Module | What its for | Default Implementation +----------------------|-----------------------------------------|------------------------- +`InstallationStore` | Persist installation data and lookup it by IDs. | `FileInstallationStore` +`OAuthStateStore` | Issue and consume `state` parameter value on the server-side. | `FileOAuthStateStore` +`AuthorizeUrlGenerator` | Build https://slack.com/oauth/v2/authorize with sufficient query parameters | (same) + +The code snippet below demonstrates how to build it using [Flask](https://flask.palletsprojects.com/). + +``` python +import os +import html +from slack_sdk.oauth import AuthorizeUrlGenerator +from slack_sdk.oauth.installation_store import FileInstallationStore, Installation +from slack_sdk.oauth.state_store import FileOAuthStateStore + +# Issue and consume state parameter value on the server-side. +state_store = FileOAuthStateStore(expiration_seconds=300, base_dir="./data") +# Persist installation data and lookup it by IDs. +installation_store = FileInstallationStore(base_dir="./data") + +# Build https://slack.com/oauth/v2/authorize with sufficient query parameters +authorize_url_generator = AuthorizeUrlGenerator( + client_id=os.environ["SLACK_CLIENT_ID"], + scopes=["app_mentions:read", "chat:write"], + user_scopes=["search:read"], +) + +from flask import Flask, request, make_response +app = Flask(__name__) + +@app.route("/slack/install", methods=["GET"]) +def oauth_start(): + # Generate a random value and store it on the server-side + state = state_store.issue() + # https://slack.com/oauth/v2/authorize?state=(generated value)&client_id={client_id}&scope=app_mentions:read,chat:write&user_scope=search:read + url = authorize_url_generator.generate(state) + return f'' \ + f'' +``` + +When accessing `https://(your domain)/slack/install`, you will see an \"Add to Slack\" button on the page. You can start the app's installation flow by clicking the button. + +### Handling a callback request from Slack {#handling-callback-requests} + +If all is well, a user goes through the Slack app installation UI and accepts all the scopes your app requests. After that happens, Slack redirects the user back to your specified Redirect URL. + +The redirection gives you a `code` parameter. You can exchange the value for an access token by calling the [oauth.v2.access](/reference/methods/oauth.v2.access) API method. + +``` python +from slack_sdk.web import WebClient +client_secret = os.environ["SLACK_CLIENT_SECRET"] + +# Redirect URL +@app.route("/slack/oauth/callback", methods=["GET"]) +def oauth_callback(): + # Retrieve the auth code and state from the request params + if "code" in request.args: + # Verify the state parameter + if state_store.consume(request.args["state"]): + client = WebClient() # no prepared token needed for this + # Complete the installation by calling oauth.v2.access API method + oauth_response = client.oauth_v2_access( + client_id=client_id, + client_secret=client_secret, + redirect_uri=redirect_uri, + code=request.args["code"] + ) + installed_enterprise = oauth_response.get("enterprise") or {} + is_enterprise_install = oauth_response.get("is_enterprise_install") + installed_team = oauth_response.get("team") or {} + installer = oauth_response.get("authed_user") or {} + incoming_webhook = oauth_response.get("incoming_webhook") or {} + bot_token = oauth_response.get("access_token") + # NOTE: oauth.v2.access doesn't include bot_id in response + bot_id = None + enterprise_url = None + if bot_token is not None: + auth_test = client.auth_test(token=bot_token) + bot_id = auth_test["bot_id"] + if is_enterprise_install is True: + enterprise_url = auth_test.get("url") + + installation = Installation( + app_id=oauth_response.get("app_id"), + enterprise_id=installed_enterprise.get("id"), + enterprise_name=installed_enterprise.get("name"), + enterprise_url=enterprise_url, + team_id=installed_team.get("id"), + team_name=installed_team.get("name"), + bot_token=bot_token, + bot_id=bot_id, + bot_user_id=oauth_response.get("bot_user_id"), + bot_scopes=oauth_response.get("scope"), # comma-separated string + user_id=installer.get("id"), + user_token=installer.get("access_token"), + user_scopes=installer.get("scope"), # comma-separated string + incoming_webhook_url=incoming_webhook.get("url"), + incoming_webhook_channel=incoming_webhook.get("channel"), + incoming_webhook_channel_id=incoming_webhook.get("channel_id"), + incoming_webhook_configuration_url=incoming_webhook.get("configuration_url"), + is_enterprise_install=is_enterprise_install, + token_type=oauth_response.get("token_type"), + ) + + # Store the installation + installation_store.save(installation) + + return "Thanks for installing this app!" + else: + return make_response(f"Try the installation again (the state value is already expired)", 400) + + error = request.args["error"] if "error" in request.args else "" + return make_response(f"Something is wrong with the installation (error: {html.escape(error)})", 400) +``` + +## Token lookup {#token-lookup} + +Now that your Flask app can choose the right access token for incoming event requests, let's add the Slack event handler endpoint. You can use the same `InstallationStore` in the Slack event handler. + +``` python +import json +from slack_sdk.errors import SlackApiError + +from slack_sdk.signature import SignatureVerifier +signing_secret = os.environ["SLACK_SIGNING_SECRET"] +signature_verifier = SignatureVerifier(signing_secret=signing_secret) + +@app.route("/slack/events", methods=["POST"]) +def slack_app(): + # Verify incoming requests from Slack + # https://docs.slack.dev/authentication/verifying-requests-from-slack + if not signature_verifier.is_valid( + body=request.get_data(), + timestamp=request.headers.get("X-Slack-Request-Timestamp"), + signature=request.headers.get("X-Slack-Signature")): + return make_response("invalid request", 403) + + # Handle a slash command invocation + if "command" in request.form \ + and request.form["command"] == "/open-modal": + try: + # in the case where this app gets a request from an Enterprise Grid workspace + enterprise_id = request.form.get("enterprise_id") + # The workspace's ID + team_id = request.form["team_id"] + # Lookup the stored bot token for this workspace + bot = installation_store.find_bot( + enterprise_id=enterprise_id, + team_id=team_id, + ) + bot_token = bot.bot_token if bot else None + if not bot_token: + # The app may be uninstalled or be used in a shared channel + return make_response("Please install this app first!", 200) + + # Open a modal using the valid bot token + client = WebClient(token=bot_token) + trigger_id = request.form["trigger_id"] + response = client.views_open( + trigger_id=trigger_id, + view={ + "type": "modal", + "callback_id": "modal-id", + "title": { + "type": "plain_text", + "text": "Awesome Modal" + }, + "submit": { + "type": "plain_text", + "text": "Submit" + }, + "blocks": [ + { + "type": "input", + "block_id": "b-id", + "label": { + "type": "plain_text", + "text": "Input label", + }, + "element": { + "action_id": "a-id", + "type": "plain_text_input", + } + } + ] + } + ) + return make_response("", 200) + except SlackApiError as e: + code = e.response["error"] + return make_response(f"Failed to open a modal due to {code}", 200) + + elif "payload" in request.form: + # Data submission from the modal + payload = json.loads(request.form["payload"]) + if payload["type"] == "view_submission" \ + and payload["view"]["callback_id"] == "modal-id": + submitted_data = payload["view"]["state"]["values"] + print(submitted_data) # {'b-id': {'a-id': {'type': 'plain_text_input', 'value': 'your input'}}} + # You can use WebClient with a valid token here too + return make_response("", 200) + + # Indicate unsupported request patterns + return make_response("", 404) +``` + +## Sign in with Slack {#siws} + +[Sign in with Slack](/authentication/sign-in-with-slack) helps users log into your service using their Slack profile. The platform feature was upgraded to be compatible with the standard [OpenID Connect](https://openid.net/connect/) specification. With slack-sdk v3.9+, implementing the OAuth flow is much easier. + +When you create a new Slack app, set the following user scopes: + +``` yaml +oauth_config: + redirect_urls: + - https://{your-domain}/slack/oauth_redirect + scopes: + user: + - openid # required + - email # optional + - profile # optional +``` + +Check [the Flask app example](https://github.com/slackapi/python-slack-sdk/blob/main/integration_tests/samples/openid_connect/flask_example.py) to learn how to implement an app that handles the OpenID Connect flow with your end-users as follows: + +**Build the OpenID Connect authorize URL** + +- `slack_sdk.oauth.OpenIDConnectAuthorizeUrlGenerator` helps you do this. +- `slack_sdk.oauth.OAuthStateStore` is still available for generating the `state` parameter value. It's available for `nonce` management, too. + +**openid.connect.\* API calls** + +- `WebClient` can perform `openid.connect.token` API calls with given `code` parameter. + +If you want to know the way with asyncio, check [the Sanic app example](https://github.com/slackapi/python-slack-sdk/blob/main/integration_tests/samples/openid_connect/sanic_example.py) in the same directory. diff --git a/docs/english/rtm.md b/docs/english/rtm.md new file mode 100644 index 000000000..d609aab04 --- /dev/null +++ b/docs/english/rtm.md @@ -0,0 +1,91 @@ +# RTM API client + +The [Legacy Real Time Messaging (RTM) API](/legacy/legacy-rtm-api) is a WebSocket-based API that allows you to receive events from Slack in real time and to send messages as users. + +If you prefer events to be pushed to your app, we recommend using the HTTP-based [Events API](/apis/events-api) instead. The Events API contains some events that aren't supported in the Legacy RTM API (such as the [app_home_opened event](/reference/events/app_home_opened)), and it supports most of the event types in the Legacy RTM API. If you'd like to use the Events API, you can use the [Python Slack Events Adaptor](https://github.com/slackapi/python-slack-events-api). + +The RTMClient allows apps to communicate with the Legacy RTM API. + +The event-driven architecture of this client allows you to simply link callbacks to their corresponding events. When an event occurs, this client executes your callback while passing along any information it receives. We also give you the ability to call our web client from inside your callbacks. + +In our example below, we watch for a [message event](/reference/events/message) that contains \"Hello\" and if it's received, we call the `say_hello()` function. We then issue a call to the web client to post back to the channel saying \"Hi\" to the user. + +## Configuring the RTM API {#configuration} + +Events using the Legacy RTM API **must** use a Slack app with a plain `bot` scope. + +If you already have a Slack app with a plain `bot` scope, you can use those credentials. If you don't and need to use the Legacy RTM API, you can create a Slack app [here](https://api.slack.com/apps?new_classic_app=1). Even if the Slack app configuration pages encourage you to upgrade to a newer permission model, don't upgrade it and continue using the \"classic\" bot permission. + +## Connecting to the RTM API {#connecting} + +Note that the import here is not `from slack_sdk.rtm import RTMClient` but `from slack_sdk.rtm_v2 import RTMClient` (`_v2` is added in the latter one). + +``` python +import os +from slack_sdk.rtm_v2 import RTMClient + +rtm = RTMClient(token=os.environ["SLACK_BOT_TOKEN"]) + +@rtm.on("message") +def handle(client: RTMClient, event: dict): + if 'Hello' in event['text']: + channel_id = event['channel'] + thread_ts = event['ts'] + user = event['user'] # This is not username but user ID (the format is either U*** or W***) + + client.web_client.chat_postMessage( + channel=channel_id, + text=f"Hi <@{user}>!", + thread_ts=thread_ts + ) + +rtm.start() +``` + +## Connecting to the RTM API (v1 client) {#connecting-v1} + +Below is a code snippet that uses the legacy version of `RTMClient`. For new app development, we **do not recommend** using it as it contains issues that have been resolved in v2. Please refer to the [list of these issues](https://github.com/slackapi/python-slack-sdk/issues?q=is%3Aissue+is%3Aclosed+milestone%3A3.3.0+label%3Artm-client) for more details. + +``` python +import os +from slack_sdk.rtm import RTMClient + +@RTMClient.run_on(event="message") +def say_hello(**payload): + data = payload['data'] + web_client = payload['web_client'] + + if 'Hello' in data['text']: + channel_id = data['channel'] + thread_ts = data['ts'] + user = data['user'] # This is not username but user ID (the format is either U*** or W***) + + web_client.chat_postMessage( + channel=channel_id, + text=f"Hi <@{user}>!", + thread_ts=thread_ts + ) + +slack_token = os.environ["SLACK_BOT_TOKEN"] +rtm_client = RTMClient(token=slack_token) +rtm_client.start() +``` + +## The `rtm.start` vs. `rtm.connect` API methods (v1 client) {#rtm-methods} + +By default, the RTM client uses the [`rtm.connect`](/reference/methods/rtm.connect) API method to establish a WebSocket connection with Slack. The response contains basic information about the team and WebSocket URL. + +See the [`rtm.connect`](/reference/methods/rtm.connect) and [`rtm.start`](/reference/methods/rtm.start) API methods for more details. Note that `slack.rtm_v2.RTMClient` does not support `rtm.start`. + +## RTM events {#rtm-events} + +``` javascript +{ + 'type': 'message', + 'ts': '1358878749.000002', + 'user': 'U023BECGF', + 'text': 'Hello' +} +``` + +Refer to the [Legacy RTM events](/legacy/legacy-rtm-api#events) section for a complete list of events. diff --git a/docs/english/scim.md b/docs/english/scim.md new file mode 100644 index 000000000..78f696056 --- /dev/null +++ b/docs/english/scim.md @@ -0,0 +1,147 @@ +# SCIM API client + +[SCIM](http://www.simplecloud.info/) is supported by a myriad of services. The SCIM API is a set of APIs for provisioning and managing user accounts and groups. SCIM is used by Single Sign-On (SSO) services and identity providers to manage people across a variety of tools, including Slack. + +Refer to [using the Slack SCIM API](/admins/scim-api) for more details. + +View the [Python document for this module](https://docs.slack.dev/tools/python-slack-sdk/reference). + +## SCIMClient {#scimclient} + +An OAuth token with [the admin scope](/reference/scopes/admin) is required to access the SCIM API. To fetch provisioned user data, you can use the `search_users` method in the client. + +``` python +import os +from slack_sdk.scim import SCIMClient + +client = SCIMClient(token=os.environ["SLACK_ORG_ADMIN_USER_TOKEN"]) + +response = client.search_users( + start_index=1, + count=100, + filter="""filter=userName Eq "Carly"""", +) +response.users # List[User] +``` + +Check out [the class source code](https://github.com/slackapi/python-slack-sdk/blob/main/slack_sdk/scim/v1/user.py) to learn more about the structure of the `user` in `response.users`. + +Similarly, the `search_groups` method is available and the shape of the `Group` object can be [found here](https://github.com/slackapi/python-slack-sdk/blob/main/slack_sdk/scim/v1/group.py). + +``` python +response = client.search_groups( + start_index=1, + count=10, +) +response.groups # List[Group] +``` + +For creating, updating, and deleting users or groups: + +``` python +from slack_sdk.scim.v1.user import User, UserName, UserEmail + +# POST /Users +# Creates a user. Must include the user_name argument and at least one email address. +# You may provide an email address as the user_name value, +# but it will be automatically converted to a Slack-appropriate username. +user = User( + user_name="cal", + name=UserName(given_name="C", family_name="Henderson"), + emails=[UserEmail(value="your-unique-name@example.com")], +) +creation_result = client.create_user(user) + +# PATCH /Users/{user_id} +# Updates an existing user resource, overwriting values for specified attributes. +patch_result = client.patch_user( + id=creation_result.user.id, + partial_user=User(user_name="chenderson"), +) + +# PUT /Users/{user_id} +# Updates an existing user resource, overwriting all values for a user +# even if an attribute is empty or not provided. +user_to_update = patch_result.user +user_to_update.name = UserName(given_name="Cal", family_name="Henderson") +update_result = client.update_user(user=user_to_update) + +# DELETE /Users/{user_id} +# Sets a Slack user to deactivated. The value of the {id} +# should be the user's corresponding Slack ID, beginning with either U or W. +delete_result = client.delete_user(user_to_update.id) +``` + +## AsyncSCIMClient {#asyncscimclient} + +If you are keen to use asyncio for SCIM API calls, we offer `AsyncSCIMClient`. This client relies on the aiohttp library. + +``` python +import asyncio +import os +from slack_sdk.scim.async_client import AsyncSCIMClient + +client = AsyncSCIMClient(token=os.environ["SLACK_ORG_ADMIN_USER_TOKEN"]) + +async def main(): + response = await client.search_groups(start_index=1, count=2) + print(response.groups) + +asyncio.run(main()) +``` + +------------------------------------------------------------------------ + +## RetryHandler {#retryhandler} + +With the default settings, only `ConnectionErrorRetryHandler` with its default configuration (=only one retry in the manner of [exponential backoff and jitter](https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/)) is enabled. The retry handler retries if an API client encounters a connectivity-related failure (e.g., connection reset by peer). + +To use other retry handlers, you can pass a list of `RetryHandler` to the client constructor. For instance, you can add the built-in `RateLimitErrorRetryHandler` this way: + +``` python +import os +from slack_sdk.scim import SCIMClient +client = SCIMClient(token=os.environ["SLACK_ORG_ADMIN_USER_TOKEN"]) + +# This handler does retries when HTTP status 429 is returned +from slack_sdk.http_retry.builtin_handlers import RateLimitErrorRetryHandler +rate_limit_handler = RateLimitErrorRetryHandler(max_retry_count=1) + +# Enable rate limited error retries as well +client.retry_handlers.append(rate_limit_handler) +``` + +You can also create one on your own by defining a new class that inherits `slack_sdk.http_retry RetryHandler` (`AsyncRetryHandler` for asyncio apps) and implements required methods (internals of `can_retry` / `prepare_for_next_retry`). Check out the source code for the ones that are built in to learn how to properly implement them. + +``` python +import socket +from typing import Optional +from slack_sdk.http_retry import (RetryHandler, RetryState, HttpRequest, HttpResponse) +from slack_sdk.http_retry.builtin_interval_calculators import BackoffRetryIntervalCalculator +from slack_sdk.http_retry.jitter import RandomJitter + +class MyRetryHandler(RetryHandler): + def _can_retry( + self, + *, + state: RetryState, + request: HttpRequest, + response: Optional[HttpResponse] = None, + error: Optional[Exception] = None + ) -> bool: + # [Errno 104] Connection reset by peer + return error is not None and isinstance(error, socket.error) and error.errno == 104 + +client = SCIMClient( + token=os.environ["SLACK_ORG_ADMIN_USER_TOKEN"], + retry_handlers=[MyRetryHandler( + max_retry_count=1, + interval_calculator=BackoffRetryIntervalCalculator( + backoff_factor=0.5, + jitter=RandomJitter(), + ), + )], +) +``` + +For asyncio apps, `Async` prefixed corresponding modules are available. All the methods in those methods are async/await compatible. Check [the source code](https://github.com/slackapi/python-slack-sdk/blob/main/slack_sdk/http_retry/async_handler.py) for more details. diff --git a/docs/english/socket-mode.md b/docs/english/socket-mode.md new file mode 100644 index 000000000..a1bf9984e --- /dev/null +++ b/docs/english/socket-mode.md @@ -0,0 +1,222 @@ +# Socket Mode client + +Socket Mode is a method of connecting your app to the Slack APIs using WebSockets instead of HTTP. You can use `slack_sdk.socket_mode.SocketModeClient` for managing [Socket Mode](/apis/events-api/using-socket-mode) connections and performing interactions with Slack. + +## Using Socket Mode {#socket-mode} + +Let's start with enabling Socket Mode. Visit [app page](http://api.slack.com/apps), choose the app you're working on, and go to **Settings** on the left pane. There are a few things to do on this page. + +- Go to **Settings** \> **Basic Information**, then add a new **App-Level Token** with the `connections:write` scope. +- Go to **Settings** \> **Socket Mode**, then toggle on **Enable Socket Mode**. +- Go to **Features** \> **App Home**, look under **Show Tabs** \> **Messages Tab**, then toggle on **Allow users to send Slash commands and messages from the messages tab**. +- Go to **Features** \> **Event Subscriptions**, then toggle on **Enable Events**. +- On the same page, expand **Subscribe to bot events**, click **Add Bot User Event**, and select **message.im**. This will allow the bot to get events for messages that are sent in 1:1 direct messages with itself. +- Go to **Features** \> **Interactivity and Shortcuts**, look under *Shortcuts*\*, click **Create a New Shortcut**, then create a new Global shortcut with the following details: + > **Name**: Hello + + > **Short Description**: Receive a Greeting + + > **Callback ID**: hello-shortcut + +- Go to **Features** \> **OAuth & Permissions** under **Scopes** \> **Bot Token Scopes**, click **Add an OAuth Scope**, and select **reactions:write**. This will allow the bot to add emoji reactions (Reacjis) to messages. +- Go to **Features** \> **Oauth & Permissions** under **OAuth Tokens for Your Workspace** and click **Install to Workspace**. + +You will be using the app-level token that starts with `xapp-`. Note that the token here is not one that starts with either `xoxb-` or `xoxp-`. + +``` python +import os +from slack_sdk.web import WebClient +from slack_sdk.socket_mode import SocketModeClient + +# Initialize SocketModeClient with an app-level token + WebClient +client = SocketModeClient( + # This app-level token will be used only for establishing a connection + app_token=os.environ.get("SLACK_APP_TOKEN"), # xapp-A111-222-xyz + # You will be using this WebClient for performing Web API calls in listeners + web_client=WebClient(token=os.environ.get("SLACK_BOT_TOKEN")) # xoxb-111-222-xyz +) + +from slack_sdk.socket_mode.response import SocketModeResponse +from slack_sdk.socket_mode.request import SocketModeRequest + +def process(client: SocketModeClient, req: SocketModeRequest): + if req.type == "events_api": + # Acknowledge the request anyway + response = SocketModeResponse(envelope_id=req.envelope_id) + client.send_socket_mode_response(response) + + # Add a reaction to the message if it's a new message + if req.payload["event"]["type"] == "message" \ + and req.payload["event"].get("subtype") is None: + client.web_client.reactions_add( + name="eyes", + channel=req.payload["event"]["channel"], + timestamp=req.payload["event"]["ts"], + ) + if req.type == "interactive" \ + and req.payload.get("type") == "shortcut": + if req.payload["callback_id"] == "hello-shortcut": + # Acknowledge the request + response = SocketModeResponse(envelope_id=req.envelope_id) + client.send_socket_mode_response(response) + # Open a welcome modal + client.web_client.views_open( + trigger_id=req.payload["trigger_id"], + view={ + "type": "modal", + "callback_id": "hello-modal", + "title": { + "type": "plain_text", + "text": "Greetings!" + }, + "submit": { + "type": "plain_text", + "text": "Good Bye" + }, + "blocks": [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "Hello!" + } + } + ] + } + ) + + if req.type == "interactive" \ + and req.payload.get("type") == "view_submission": + if req.payload["view"]["callback_id"] == "hello-modal": + # Acknowledge the request and close the modal + response = SocketModeResponse(envelope_id=req.envelope_id) + client.send_socket_mode_response(response) + +# Add a new listener to receive messages from Slack +# You can add more listeners like this +client.socket_mode_request_listeners.append(process) +# Establish a WebSocket connection to the Socket Mode servers +client.connect() +# Just not to stop this process +from threading import Event +Event().wait() +``` + +--- + +## Supported libraries {#supported-libraries} + +This SDK offers its own WebSocket client covering only required features for Socket Mode. In addition, `SocketModeClient` is implemented with a few 3rd party open source libraries. If you prefer any of the following, you can use it over the built-in one. + +|PyPI Project | SocketModeClient +|--------------|------------------ +| [`slack_sdk`](https://pypi.org/project/slack-sdk/) | [`slack_sdk.socket_mode.SocketModeClient`](https://github.com/slackapi/python-slack-sdk/blob/main/slack_sdk/socket_mode/builtin) +| [`websocket_client`](https://pypi.org/project/websocket_client/) | [`slack_sdk.socket_mode.websocket_client.SocketModeClient`](https://github.com/slackapi/python-slack-sdk/blob/main/slack_sdk/socket_mode/websocket_client) +| [`aiohttp`](https://pypi.org/project/aiohttp/) (asyncio-based) | [`slack_sdk.socket_mode.aiohttp.SocketModeClient`](https://github.com/slackapi/python-slack-sdk/blob/main/slack_sdk/socket_mode/aiohttp) +| [`websockets`](https://pypi.org/project/websockets/) (asyncio-based) | [`slack_sdk.socket_mode.websockets.SocketModeClient`](https://github.com/slackapi/python-slack-sdk/blob/main/slack_sdk/socket_mode/websockets) + +To use the [`websocket_client`](https://pypi.org/project/websocket_client/) based-one, add the[`websocket_client`](https://pypi.org/project/websocket_client/) dependency and change the import as below. + +``` python +# Note that the pockage is different +from slack_sdk.socket_mode.websocket_client import SocketModeClient + +client = SocketModeClient( + app_token=os.environ.get("SLACK_APP_TOKEN"), # xapp-A111-222-xyz + web_client=WebClient(token=os.environ.get("SLACK_BOT_TOKEN")) # xoxb-111-222-xyz +) +``` + +You can pass a few additional arguments that are specific to the library. Apart from that, all the functionalities work in the same way as the built-in version. + +--- + +## Asyncio-based libraries {#asyncio-libraries} + +To use the asyncio-based ones such as aiohttp, your app needs to be compatible with asyncio's async/await programming model. The `SocketModeClient` only works with `AsyncWebClient` and async listeners. + +``` python +import asyncio +import os +from slack_sdk.web.async_client import AsyncWebClient +from slack_sdk.socket_mode.aiohttp import SocketModeClient + +# Use async method +async def main(): + from slack_sdk.socket_mode.response import SocketModeResponse + from slack_sdk.socket_mode.request import SocketModeRequest + + # Initialize SocketModeClient with an app-level token + AsyncWebClient + client = SocketModeClient( + # This app-level token will be used only for establishing a connection + app_token=os.environ.get("SLACK_APP_TOKEN"), # xapp-A111-222-xyz + # You will be using this AsyncWebClient for performing Web API calls in listeners + web_client=AsyncWebClient(token=os.environ.get("SLACK_BOT_TOKEN")) # xoxb-111-222-xyz + ) + + # Use async method + async def process(client: SocketModeClient, req: SocketModeRequest): + if req.type == "events_api": + # Acknowledge the request anyway + response = SocketModeResponse(envelope_id=req.envelope_id) + # Don't forget having await for method calls + await client.send_socket_mode_response(response) + + # Add a reaction to the message if it's a new message + if req.payload["event"]["type"] == "message" \ + and req.payload["event"].get("subtype") is None: + await client.web_client.reactions_add( + name="eyes", + channel=req.payload["event"]["channel"], + timestamp=req.payload["event"]["ts"], + ) + if req.type == "interactive" \ + and req.payload.get("type") == "shortcut": + if req.payload["callback_id"] == "hello-shortcut": + # Acknowledge the request + response = SocketModeResponse(envelope_id=req.envelope_id) + await client.send_socket_mode_response(response) + # Open a welcome modal + await client.web_client.views_open( + trigger_id=req.payload["trigger_id"], + view={ + "type": "modal", + "callback_id": "hello-modal", + "title": { + "type": "plain_text", + "text": "Greetings!" + }, + "submit": { + "type": "plain_text", + "text": "Good Bye" + }, + "blocks": [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "Hello!" + } + } + ] + } + ) + + if req.type == "interactive" \ + and req.payload.get("type") == "view_submission": + if req.payload["view"]["callback_id"] == "hello-modal": + # Acknowledge the request and close the modal + response = SocketModeResponse(envelope_id=req.envelope_id) + await client.send_socket_mode_response(response) + + # Add a new listener to receive messages from Slack + # You can add more listeners like this + client.socket_mode_request_listeners.append(process) + # Establish a WebSocket connection to the Socket Mode servers + await client.connect() + # Just not to stop this process + await asyncio.sleep(float("inf")) + +# You can go with other way to run it. This is just for easiness to try it out. +asyncio.run(main()) +``` diff --git a/docs/english/tutorial/understanding-oauth-approve.png b/docs/english/tutorial/understanding-oauth-approve.png new file mode 100644 index 000000000..8f0263e03 Binary files /dev/null and b/docs/english/tutorial/understanding-oauth-approve.png differ diff --git a/docs/english/tutorial/understanding-oauth-flow.png b/docs/english/tutorial/understanding-oauth-flow.png new file mode 100644 index 000000000..15ef9de6d Binary files /dev/null and b/docs/english/tutorial/understanding-oauth-flow.png differ diff --git a/docs/english/tutorial/understanding-oauth-scopes.md b/docs/english/tutorial/understanding-oauth-scopes.md new file mode 100644 index 000000000..fc1c96f9f --- /dev/null +++ b/docs/english/tutorial/understanding-oauth-scopes.md @@ -0,0 +1,163 @@ +# Understanding OAuth scopes for bots + +In this tutorial, we'll: + +* explore Slack app permissions and distribution using OAuth, and along the way, learn how to identify which scopes apps need and how to use OAuth to request them. +* build an app that sends a direct message to users joining a specific channel. Once installed in a workspace, it will create a new channel named **#the-welcome-channel** if it doesn’t already exist. The channel will be used to thank users for joining the channel. We'll also share code snippets from the app, but the full source code is available on [GitHub](https://github.com/stevengill/slack-python-oauth-example). The code and implementation of OAuth is general enough that you should be able to follow along, even if Python isn't your preferred language. + +## Prerequisites {#prerequisites} + +Before we get started, ensure you have a development workspace with permissions to install apps. If you don’t have one set up, go ahead and [create one](https://slack.com/create). You also need to [create a new app](https://api.slack.com/apps/new) if you haven’t already. + +Let’s get started! + +## Determining scopes {#determine-scopes} + +Scopes are used to grant your app permission to perform functions in Slack, such as calling Web API methods and receiving Events API events. As a user goes through your app's installation flow, they'll need to permit access to the scopes your app is requesting. + +To determine which scopes we need, we should take a closer look at what our app does. Instead of scouring the entire list of scopes that might make sense, we can look at what events or methods we need for the app, and build out our scope list as we go. + +1. After installation, our app checks to see if a channel exists (private or public since we can’t create a new channel with the same name). A quick search through the list of methods leads us to the `conversations.list` API method, which we can use to get the names of public & private channels. It also shows us what scopes are needed to use this method. In our case, we need `channels:read` and `groups:read`. (We don’t need `im:read` or `mpim:read`, as we aren’t concerned about the names of direct messages.) + + ``` + import os + from slack_sdk import WebClient + + # verifies if "the-welcome-channel" already exists + def channel_exists(): + token = os.environ["SLACK_BOT_TOKEN"] + client = WebClient(token=token) + + # grab a list of all the channels in a workspace + clist = client.conversations_list() + exists = False + for k in clist["channels"]: + # look for the channel in the list of existing channels + if k['name'] == 'the-welcome-channel': + exists = True + break + if exists == False: + # create the channel since it doesn't exist + create_channel() + ``` + +2. If the channel doesn’t already exist, we need to create it. Looking through the list of API methods leads us to the `conversations.create` API method, which needs the scope `channels:manage`. + + ``` + # creates a channel named "the-welcome-channel" + def create_channel(): + token = os.environ["SLACK_BOT_TOKEN"] + client = WebClient(token=token) + resp = client.conversations_create(name="the-welcome-channel") + ``` + +3. When a user joins our newly created channel, our app sends them a direct message. To see when a user joins our channel, we need to listen for an event. Looking at our list of events, we see that `member_joined_channel` is the event that we need (_Note: events need to be added to your app’s configuration_). The scopes required for this event are `channels:read` and `groups:read` (same ones from step one). Now to send a direct message, we need to use the `chat.postMessage` API method, which requires the `chat:write` scope. + + ``` + # Create an event listener for "member_joined_channel" events + # Sends a DM to the user who joined the channel + @slack_events_adapter.on("member_joined_channel") + def member_joined_channel(event_data): + user = event_data['event']['user'] + token = os.environ["SLACK_BOT_TOKEN"] + client = WebClient(token=token) + msg = 'Welcome! Thanks for joining the-welcome-channel' + client.chat_postMessage(channel=user, text=msg) + ``` + +Our final list of scopes required are: +* `channels:read` +* `groups:read` +* `channels:manage` +* `chat:write` + +## Setting up OAuth and requesting scopes {#setup} + +If you want users to be able to install your app on additional workspaces or from the [Slack Marketplace](/slack-marketplace/slack-marketplace-review-guide), you'll need to implement an OAuth flow. + +We'll be following the general flow of OAuth with Slack, which is covered in the [installing with OAuth](/authentication/installing-with-oauth) guide and nicely illustrated in the image below: + +![OAuth flow](understanding-oauth-flow.png) + +1. **Requesting Scopes** + + This first step is sometimes also referred to as "redirect to Slack" or "Add to Slack button". In this step, we redirect to Slack and pass along our list of required scopes, client ID, and state as query parameters in the URL. You can get the client ID from the **Basic Information** section of your app. State is an optional value, but is recommended to prevent [CSRF attacks](https://en.wikipedia.org/wiki/Cross-site_request_forgery). + + ``` + https://slack.com/oauth/v2/authorize?scope=channels:read,groups:read,channels:manage,chat:write&client_id=YOUR_CLIENT_ID&state=STATE_STRING + ``` + + _It is also possible to pass in a `redirect\_uri` into your URL. A `redirect\_uri` is used for Slack to know where to send the request after the user has granted permission to your app. In our example, instead of passing one in the URL, we request that you add a Redirect URL in your app’s configuration on [api.slack.com/apps](https://api.slack.com/apps) under the **OAuth and Permissions** section._ + + Next, we'll create a route in our app that contains an **Add to Slack** button using that URL above. + + ``` + # Grab client ID from your environment variables + client_id = os.environ["SLACK_CLIENT_ID"] + # Generate random string to use as state to prevent CSRF attacks + from uuid import uuid4 + state = str(uuid4()) + + # Route to kick off Oauth flow + @app.route("/begin_auth", methods=["GET"]) + def pre_install(): + return f'' + ``` + + Now when a user navigates to the route, they should see the **Add to Slack** button. + + Clicking the button will trigger the next step. + +2. **Waiting for user approval** + + The user will see the app installation UI (shown below) and will have the option to accept the permissions and allow the app to install to the workspace: + + ![Approve installation](understanding-oauth-approve.png) + +3. **Exchanging a temporary authorization code for an access token** + + After the user approves the app, Slack will redirect the user to your specified Redirect URL. As we mentioned earlier, we did not include a `redirect_uri` in our **Add to Slack** button, so our app will use our Redirect URL specified on the app’s **OAuth and Permissions** page. + + Our Redirect URL function will have to parse the HTTP request for the `code` and `state` query parameters. We need to check that the `state` parameter was created by our app. If it is, we can now exchange the `code` for an access token. To do this, we need to call the `oauth.v2.access` API method with the `code`, `client_id`, and `client_secret`. This method will return the access token, which we can now save (preferably in a persistent database) and use for any of the Slack API method calls we make. (_Note: use this access token for all of the Slack API method calls we covered in the scopes section above_) + + ``` + # Grab client Secret from your environment variables + client_secret = os.environ["SLACK_CLIENT_SECRET"] + + # Route for Oauth flow to redirect to after user accepts scopes + @app.route("/finish_auth", methods=["GET", "POST"]) + def post_install(): + # Retrieve the auth code and state from the request params + auth_code = request.args['code'] + received_state = request.args['state'] + + # This request doesn't need a token so an empty string will suffice + client = WebClient(token="") + + # verify state received in params matches state we originally sent in auth request + if received_state == state: + # Request the auth tokens from Slack + response = client.oauth_v2_access( + client_id=client_id, + client_secret=client_secret, + code=auth_code + ) + else: + return "Invalid State" + + # Save the bot token to an environmental variable or to your data store + os.environ["SLACK_BOT_TOKEN"] = response['access_token'] + + # See if "the-welcome-channel" exists. Create it if it doesn't. + channel_exists() + + # Don't forget to let the user know that auth has succeeded! + return "Auth complete!" + ``` + +## Next steps {#next} + +At this point, you should feel more comfortable learning what scopes your app needs and using OAuth to request those scopes. A few resources you can check out next include: + +* [Slack-Python-OAuth-Example](https://github.com/stevengill/slack-python-oauth-example): we used code snippets from this app in this tutorial. The README contains more detailed information about running the app locally using ngrok, setting up a Redirect URL for OAuth, and setting up a request URL for events. +* Learn more about [installing with OAuth](/authentication/installing-with-oauth). diff --git a/docs/english/tutorial/upload-files-allow.png b/docs/english/tutorial/upload-files-allow.png new file mode 100644 index 000000000..bc7e533ad Binary files /dev/null and b/docs/english/tutorial/upload-files-allow.png differ diff --git a/docs/english/tutorial/upload-files-bot-token.png b/docs/english/tutorial/upload-files-bot-token.png new file mode 100644 index 000000000..5c29732c8 Binary files /dev/null and b/docs/english/tutorial/upload-files-bot-token.png differ diff --git a/docs/english/tutorial/upload-files-delete.png b/docs/english/tutorial/upload-files-delete.png new file mode 100644 index 000000000..cf86026f8 Binary files /dev/null and b/docs/english/tutorial/upload-files-delete.png differ diff --git a/docs/english/tutorial/upload-files-first-upload.png b/docs/english/tutorial/upload-files-first-upload.png new file mode 100644 index 000000000..a323b7a31 Binary files /dev/null and b/docs/english/tutorial/upload-files-first-upload.png differ diff --git a/docs/english/tutorial/upload-files-install.png b/docs/english/tutorial/upload-files-install.png new file mode 100644 index 000000000..85de64ff6 Binary files /dev/null and b/docs/english/tutorial/upload-files-install.png differ diff --git a/docs/english/tutorial/upload-files-invite-bot.gif b/docs/english/tutorial/upload-files-invite-bot.gif new file mode 100644 index 000000000..c34c9165c Binary files /dev/null and b/docs/english/tutorial/upload-files-invite-bot.gif differ diff --git a/docs/english/tutorial/upload-files-local-file.png b/docs/english/tutorial/upload-files-local-file.png new file mode 100644 index 000000000..3eec0f0c9 Binary files /dev/null and b/docs/english/tutorial/upload-files-local-file.png differ diff --git a/docs/english/tutorial/upload-files-with-channel.png b/docs/english/tutorial/upload-files-with-channel.png new file mode 100644 index 000000000..dbb9b3a00 Binary files /dev/null and b/docs/english/tutorial/upload-files-with-channel.png differ diff --git a/docs/english/tutorial/uploading-files.md b/docs/english/tutorial/uploading-files.md new file mode 100644 index 000000000..a9a624758 --- /dev/null +++ b/docs/english/tutorial/uploading-files.md @@ -0,0 +1,263 @@ +# Uploading files with Python + +This tutorial details how to use the [`slack-sdk` package for Python](https://pypi.org/project/slack-sdk/) to upload files to a channel in Slack with some code samples. In addition to looking at how to upload files, we'll also cover listing and deleting files via the Web API using the Python SDK. + +## Creating an app {#create-app} + +First, create a [Slack app](https://api.slack.com/apps/new). + +## Configuring your app's settings with an app manifest {#configuration} + +Creating your app using this method will include all the required settings for this tutorial, and you won't be bogged down with too many details - all you'll need to do is decide where this app will live. If you're curious about the inner workings of how this button works, refer to [App Manifests](/app-manifests) for more information. + +```yaml +_metadata: + major_version: 1 + minor_version: 1 +display_information: + name: File Writer App +features: + bot_user: + display_name: File Writer Bot +oauth_config: + scopes: + bot: + # Used to send messages to a channel + - chat:write + # This scope is not required if your app will just upload files. We've included it in order to use the `files.info` `files.list` methods. + - files:read + # Used to upload files to Slack + - files:write +``` + +## Installing your app in a workspace {#installing} + +Once you've created your app, you'll see an **Install to Workspace** button. Click it to install your app in your workspace. + +![Install to workspace](upload-files-install.png) + +Next, click **Allow** to authorize the app in your workspace. + +![Authorize app](upload-files-allow.png) + +Navigate to the **Install App** section under **Settings**. Here, you'll find your `Bot User OAuth Token`. + +![Get token](upload-files-bot-token.png) + +Set this token value as an environment variable called `SLACK_BOT_TOKEN` by using the following command: + +```bash +export SLACK_BOT_TOKEN= +``` + +With this, all your Slack app configuration is done. Let's start coding. + +## Using Python to upload a file {#upload-file-with-python} + +### Creating a new project {#create-new-project} + +First, ensure you're using Python version 3.7 or above. While the current standard is for the `python3` and `pip3` commands to use Python 3.7 or above, it's best to ensure your runtime is always using the latest version of Python. [pyenv](https://github.com/pyenv/pyenv) is a handy tool that can do this for you. + +We'll create a brand new virtual environment and install the required library dependencies using the following commands. + +```bash +echo 'slack-sdk>=3.12,<4' > requirements.txt +python3 -m venv .venv +source .venv/bin/activate +pip install -U pip +pip install -r requirements.txt +``` + +### Uploading a file {#upload-file} + +While it's possible to enter the following into the Python shell, we've gathered some code samples and wrote it in script form. + +For each of the code samples, make sure to add in the following to the top of your Python file if you're going to run it as a script - the examples won't run without it. + +```python +import logging, os + +# Sets the debug level. +# If you're using this in production, you can change this back to INFO and add extra log entries as needed. +logging.basicConfig(level=logging.DEBUG) + +# Initialize the Web API client. +# This expects that you've already set your SLACK_BOT_TOKEN as an environment variable. +# Try to resist the urge to put your token directly in your code; it is best practice not to. +from slack_sdk import WebClient +client = WebClient(os.environ["SLACK_BOT_TOKEN"]) +``` + +Let's make sure our token is correctly configured. + +```python +# Tests to see if the token is valid +auth_test = client.auth_test() +bot_user_id = auth_test["user_id"] +print(f"App's bot user: {bot_user_id}") +``` + +Once you run this code, you'll see something similar to the following within the logs: + +```bash +>>> auth_test = client.auth_test() +DEBUG:slack_sdk.web.base_client:Sending a request - url: https://www.slack.com/api/auth.test, query_params: {}, body_params: {}, files: {}, json_body: None, headers: {} +DEBUG:slack_sdk.web.base_client:Received the following response - status: 200, headers: {}, body: {"ok":true,"url":"https:\/\/example.slack.com\/","team":"Acme Corp","user":"file_writer_bot","team_id":"T1234567890","user_id":"U02PY3HA48G","bot_id":"B02P8CPE143","is_enterprise_install":false} + +>>> bot_user_id = auth_test["user_id"] +>>> print(f"App's bot user: {bot_user_id}") +App's bot user: U02PY3HA48G +``` + +Notice that your bot user's `user_id` can be found within these logs. Any files uploaded using a bot token will be associated with the bot user. + +At this point, while no files have been uploaded yet, you can call the `files.list` API method to confirm this fact. We'll do this again after we've uploaded some files to see what has changed. + +```python +>>> files = client.files_list(user=bot_user_id) +DEBUG:slack_sdk.web.base_client:Sending a request - url: https://www.slack.com/api/files.list, query_params: {}, body_params: {'user': 'U02PY3HA48G'}, files: {}, json_body: None, headers: {} +DEBUG:slack_sdk.web.base_client:Received the following response - status: 200, headers: {}, body: {"ok":true,"files":[],"paging":{"count":100,"total":0,"page":1,"pages":0}} +``` + +Let's try uploading a file using text supplied to the `content` parameter. This will upload a text file with the specified `content`. + +```python +new_file = client.files_upload_v2( + title="My Test Text File", + filename="test.txt", + content="Hi there! This is a text file!", +) +``` + +Doing this within the Python shell will display the following: + +```python +>>> new_file = client.files_upload_v2( +... title="My Test Text File", +... filename="test.txt", +... content="Hi there! This is a text file!", +... ) +DEBUG:slack_sdk.web.base_client:Sending a request - url: https://www.slack.com/api/files.getUploadURLExternal, query_params: {}, body_params: {'filename': 'test.txt', 'length': 30}, files: {}, json_body: None, headers: {'Content-Type': 'application/x-www-form-urlencoded', 'Authorization': '(redacted)', 'User-Agent': 'Python/3.11.6 slackclient/3.27.1 Darwin/23.3.0'} +DEBUG:slack_sdk.web.base_client:Received the following response - status: 200, headers: {'date': 'Fri, 08 Mar 2024 08:27:40 GMT', 'server': 'Apache', 'vary': 'Accept-Encoding', 'x-slack-req-id': '9rj4io2i10iawjdasdfas', 'x-content-type-options': 'nosniff', 'x-xss-protection': '0', 'pragma': 'no-cache', 'cache-control': 'private, no-cache, no-store, must-revalidate', 'expires': 'Sat, 26 Jul 1997 05:00:00 GMT', 'content-type': 'application/json; charset=utf-8', 'x-accepted-oauth-scopes': 'files:write', 'x-oauth-scopes': 'chat:write,files:read,files:write', 'access-control-expose-headers': 'x-slack-req-id, retry-after', 'access-control-allow-headers': 'slack-route, x-slack-version-ts, x-b3-traceid, x-b3-spanid, x-b3-parentspanid, x-b3-sampled, x-b3-flags', 'strict-transport-security': 'max-age=31536000; includeSubDomains; preload', 'referrer-policy': 'no-referrer', 'x-slack-unique-id': 'ZerL-asd3k201a0sdfa', 'x-slack-backend': 'r', 'access-control-allow-origin': '*', 'content-length': '257', 'via': '1.1 slack-prod.tinyspeck.com, envoy-www-iad-upnvxyvr, envoy-edge-nrt-ixozsome', 'x-envoy-attempt-count': '1', 'x-envoy-upstream-service-time': '195', 'x-backend': 'main_normal main_canary_with_overflow main_control_with_overflow', 'x-server': 'slack-www-hhvm-main-iad-bgpy', 'x-slack-shared-secret-outcome': 'no-match', 'x-edge-backend': 'envoy-www', 'x-slack-edge-shared-secret-outcome': 'no-match', 'connection': 'close'}, body: {"ok":true,"upload_url":"https:\/\/files.slack.com\/upload\/v1\/CwABAASWWgoAAZOR9CgFYdQZCgACF7q8rQ4fIhASAAAVDFERDNKSDNLCwACAAAAC1UwNk5GNjdGNUxNCwADAAAAC0YwNk40VDdGWk5LAAoABAAAAAAAAAAeAAsAAgAAABRmH2dkKc07lhAASAWWpZAA","file_id":"F2910294A"} +DEBUG:slack_sdk.web.base_client:('Received the following response - ', "status: 200, headers: {'Content-Type': 'text/plain; charset=utf-8', 'Content-Length': '7', 'Connection': 'close', 'x-backend': 'miata-prod-nrt-v2-5976497578-js557', 'date': 'Fri, 08 Mar 2024 08:27:40 GMT', 'x-envoy-upstream-service-time': '401', 'x-edge-backend': 'miata', 'x-slack-edge-shared-secret-outcome': 'shared-secret', 'server': 'envoy', 'via': 'envoy-edge-nwt-aaoskwwo, 1.1 f752a4d41a2512ine9asfa.cloudfront.net (CloudFront)', 'X-Cache': 'Miss from cloudfront', 'X-Amz-Cf-Pop': 'NRT51-C4', 'X-Amz-Cf-Id': 'jxcP2ao0fs4KXanisi9aiswiaKBiJQ==', 'Cross-Origin-Resource-Policy': 'cross-origin'}, body: OK - 30") +DEBUG:slack_sdk.web.base_client:Sending a request - url: https://www.slack.com/api/files.completeUploadExternal, query_params: {}, body_params: {'files': '[{"id": "F2910294A", "title": "My Test Text File"}]'}, files: {}, json_body: None, headers: {'Content-Type': 'application/x-www-form-urlencoded', 'Authorization': '(redacted)', 'User-Agent': 'Python/3.11.6 slackclient/3.27.1 Darwin/23.3.0'} +DEBUG:slack_sdk.web.base_client:Received the following response - status: 200, headers: {'date': 'Fri, 08 Mar 2024 08:27:41 GMT', ... body: {"ok":true,"files":[{"id":"F2910294A","created":1709886459,"timestamp":1709886459,"name":"test.txt","title":"My Test Text File","mimetype":"text\/plain","filetype":"text","pretty_type":"Plain Text","user":"U10AWOAW","user_team":"T12391022","editable":true,"size":30,"mode":"snippet","is_external":false,"external_type":"","is_public":false,"public_url_shared":false,"display_as_bot":false,"username":"","url_private":"https:\/\/files.slack.com\/files-pri\/T12391022-F2910294A\/test.txt","url_private_download":"https:\/\/files.slack.com\/files-pri\/T12391022-F2910294A\/download\/test.txt","permalink":"https:\/\/platform-ce.slack.com\/files\/U10AWOAW\/F2910294A\/test.txt","permalink_public":"https:\/\/slack-files.com\/T12391022-F2910294A-100e14d15f","edit_link":"https:\/\/platform-ce.slack.com\/files\/U10AWOAW\/F2910294A\/test.txt\/edit","preview":"Hi there! This is a text file!","preview_highlight":"
\n
\n
Hi there! This is a text file!<\/pre><\/div>\n<\/div>\n<\/div>\n","lines":1,"lines_more":0,"preview_is_truncated":false,"comments_count":0,"is_starred":false,"shares":{},"channels":[],"groups":[],"ims":[],"has_more_shares":false,"has_rich_preview":false,"file_access":"visible"}]}
+```
+
+We can confirm that a file has been uploaded using the `files.list` API method mentioned earlier. Wait a moment before calling this method, as there may be a bit of a lag before files are reflected within the results.
+
+```python
+>>> files = client.files_list(user=bot_user_id)
+DEBUG:slack_sdk.web.base_client:Sending a request - url: https://www.slack.com/api/files.list, query_params: {}, body_params: {'user': 'U02PY3HA48G'}, files: {}, json_body: None, headers: {}
+DEBUG:slack_sdk.web.base_client:Received the following response - status: 200, headers: {}, body: {"ok":true,"files":[{"id":"F02P5J88137","created":1638414790,"timestamp":1638414790,"name":"test.txt","title":"My Test Text File","mimetype":"text\/plain","filetype":"text","pretty_type":"Plain Text","user":"U02PY3HA48G","editable":true,"size":30,"mode":"snippet","is_external":false,"external_type":"","is_public":false,"public_url_shared":false,"display_as_bot":false,"username":"","url_private":"https:\/\/files.slack.com\/files-pri\/T03E94MJU-F02P5J88137\/test.txt","url_private_download":"https:\/\/files.slack.com\/files-pri\/T03E94MJU-F02P5J88137\/download\/test.txt","permalink":"https:\/\/seratch.slack.com\/files\/U02PY3HA48G\/F02P5J88137\/test.txt","permalink_public":"https:\/\/slack-files.com\/T03E94MJU-F02P5J88137-e3fda671e9","edit_link":"https:\/\/seratch.slack.com\/files\/U02PY3HA48G\/F02P5J88137\/test.txt\/edit","preview":"Hi there! This is a text file!","preview_highlight":"
\n
\n
Hi there! This is a text file!<\/pre><\/div>\n<\/div>\n<\/div>\n","lines":1,"lines_more":0,"preview_is_truncated":false,"channels":[],"groups":[],"ims":[],"comments_count":0}],"paging":{"count":100,"total":1,"page":1,"pages":1}}
+```
+
+Back in Slack, you'll notice that nothing has happened. How curious...
+
+### Sharing a file within a channel {#sharing}
+
+At this point, we have indeed uploaded a file to Slack, but only the bot user is able to view it.
+
+Let's share this file with other users within our workspace. To do so, we'll use the `chat.postMessage` API method to post a message.
+
+In this example, we've used the channel name `C123456789`, but you'll need to find the ID of the channel that you want to share your file to. When you're in your channel of choice, you can find the channel ID by clicking on the channel name at the top and then scrolling to the bottom of the screen that shows up.
+
+Just like in the image below, mention the File Writer Bot and invite it to the `#random` channel.
+
+![Invite to channel](upload-files-invite-bot.gif)
+
+Next, use the following code to retrieve the file's permalink and post it within a channel.
+
+```python
+file_url = new_file.get("file").get("permalink")
+new_message = client.chat_postMessage(
+    channel="C123456789",
+    text=f"Here is the file: {file_url}",
+)
+```
+
+By doing this, you'll be able to see the file within Slack.
+
+![Upload file](upload-files-first-upload.png)
+
+### Specifying a channel when uploading a file {#specifying-channel}
+
+While this exercise was very informative, having to do these two steps every time we upload a file is a bit cumbersome. Instead, we can specify the `channel` parameter to the function. This is the more common way of uploading a file from an app.
+
+```python
+upload_and_then_share_file = client.files_upload_v2(
+    channel="C123456789",
+    title="Test text data",
+    filename="test.txt",
+    content="Hi there! This is a text file!",
+    initial_comment="Here is the file:",
+)
+```
+
+By running the above code, you'll share the same file without having to send a message with the file URL.
+
+![Share file with message](upload-files-with-channel.png)
+
+### Uploading local files {#upload-local-files}
+
+If you have an image file lying around, feel free to use that but for simplicity's sake. We'll continue using a text file here. You can create a local file by using the following command:
+
+```bash
+echo 'Hi there! This is a text file!' > test.txt
+```
+
+Next, within the same directory, execute the following code. We'll specify the file path as the `file` parameter.
+
+```python
+upload_text_file = client.files_upload_v2(
+    channel="C123456789",
+    title="Test text data",
+    file="./test.txt",
+    initial_comment="Here is the file:",
+)
+```
+
+Again, we'll see that the file has been uploaded to Slack and shared within the `#random` channel.
+
+![File uploaded](upload-files-local-file.png)
+
+## Deleting a file {#deleting}
+
+Next, we'll cover how to delete a file.
+
+We've just uploaded 3 different files above (even though they may look the same). We can confirm that again using the `files.list` method.
+
+```python
+file_ids = []
+# The Python SDK will automatically paginate for you within a for-loop.
+for page in client.files_list(user=bot_user_id):
+    for file in page.get("files", []):
+        file_ids.append(file["id"])
+
+print(file_ids)
+```
+
+```python
+>>> file_ids
+['F02P5J88137', 'F02P8H5BN9G', 'F02P5K7T8SZ']
+```
+
+Let's remove these files from our Slack workspace.
+
+```python
+for page in client.files_list(user=bot_user_id):
+    for file in page.get("files", []):
+        client.files_delete(file=file["id"])
+```
+
+Once we run this, the `files` array should be empty. The count for files found within the `paging` object may take a moment to reflect the actual number of files. You'll also notice within Slack that there are several `This file was deleted.` messages being shown.
+
+![Delete a file](upload-files-delete.png)
+
+## Next steps {#next}
+
+This tutorial summarized how to use the Slack API to upload files and share them within a channel, using the Python SDK. The same principles apply to other languages as well, so if Python isn't your fancy, feel free to try out our other SDKs:
+
+* [Java Slack SDK](/tools/java-slack-sdk/)
+* [Node Slack SDK](/tools/node-slack-sdk/)
+* [Deno Slack SDK](/tools/deno-slack-sdk/)
diff --git a/docs/english/v3-migration.md b/docs/english/v3-migration.md
new file mode 100644
index 000000000..d599ac28a
--- /dev/null
+++ b/docs/english/v3-migration.md
@@ -0,0 +1,224 @@
+---
+sidebar_label: Migrating from slackclient
+---
+
+# Migrating from v2.x to v3.x {#migrating}
+
+You may still view the legacy `slackclient` v2 [documentation](/tools/python-slack-sdk/legacy/). However, the **slackclient** project is in maintenance mode and this **slack_sdk** project is the successor.
+
+## From `slackclient` 2.x {#fromv2}
+
+There are a few changes introduced in v3.0:
+
+-   The PyPI project has been renamed from `slackclient` to `slack_sdk`.
+-   Importing `slack_sdk.*` is recommended. You can still use `slack.*` with deprecation warnings.
+-   `slack_sdk` has no required dependencies. This means `aiohttp` is no longer automatically resolved.
+-   `WebClient` no longer has `run_async` and `aiohttp` specific options. If you still need the option or other `aiohttp` specific options, use `LegacyWebClient` (`slackclient` v2 compatible) or `AsyncWebClient`.
+
+We're sorry for the inconvenience.
+
+------------------------------------------------------------------------
+
+**Change:** The PyPI project has been renamed from `slackclient` to `slack_sdk`.
+
+**Action**: Remove `slackclient`, add `slack_sdk` in `requirements.txt`.
+
+Since v3, the PyPI project name is [slack_sdk](https://pypi.org/project/slack_sdk/) (technically `slack-sdk` also works).
+
+The biggest reason for the renaming is the feature coverage in v3 and newer. The SDK v3 provides not only API clients, but also other modules. As the first step, it starts supporting OAuth flow out-of-the-box. The secondary reason is to make the names more consistent. The renaming addresses the long-lived confusion between the PyPI project and package names.
+
+------------------------------------------------------------------------
+
+**Change:** Importing `slack_sdk.*` is recommended. You can still use `slack.*` with deprecation warnings.
+
+**Action**: Replace `from slack import`, `import slack`, etc. in your source code.
+
+Most imports can be simply replaced by `find your_app -name '*.py' | xargs sed -i '' 's/from slack /from slack_sdk /g'` or similar. If you use `slack.web.classes.*`, the conversion is not so simple that we recommend manually replacing imports for those.
+
+That said, all existing code can be migrated to v3 without any code changes. If you don't have time for it, you can use the `slack` package with deprecation warnings saying `UserWarning: slack package is deprecated. Please use slack_sdk.web/webhook/rtm package instead. For more info, go to https://tools slack.dev/python-slack-sdk/v3-migration/` for a while.
+
+------------------------------------------------------------------------
+
+**Change:** `slack_sdk` has no required dependencies. This means `aiohttp` is no longer automatically resolved.
+
+**Action**: Add `aiohttp` to `requirements.txt` if you use any of `AsyncWebClient`, `AsyncWebhookClient`, and `LegacyWebClient`.
+
+If you use some modules that require `aiohttp`, your `requirements.txt` needs to explicitly list `aiohttp`. The `slack_sdk` dependency doesn't resolve it for you, unlike `slackclient` v2.
+
+------------------------------------------------------------------------
+
+**Change:** `WebClient` no longer has `run_async` and `aiohttp` specific options.
+
+**Action:** If you still need the option or other `aiohttp` specific options, use `LegacyWebClient` (`slackclient` v2 compatible) or `AsyncWebClient`.
+
+The new `slack_sdk.web.WebClient` doesn't rely on `aiohttp` internally at all. The class provides only the synchronous way to call Web APIs. If you need a v2 compatible one, you can use `LegacyWebClient`. Apart from the name, there is no breaking change in the class.
+
+If you're using `run_async=True` option, we highly recommend switching to `AsyncWebClient`. `AsyncWebClient` is a straight-forward async HTTP client. You can expect the class properly works in the nature of `async/await` provided by the standard `asyncio` library.
+
+---
+
+## Migration from v1.x to v2.x {#fromv1}
+
+If you're migrating from v1.x of `slackclient` to v2.x, here's what you need to change to ensure your app continues working after updating.
+
+:::info[We have completely rewritten this library and you should only upgrade once you have fully tested it in your development environment.] 
+
+If you don't wish to upgrade yet, be sure to pin your module for the Python `slackclient` to `1.3.1`.
+
+:::
+
+### Minimum Python versions {#minimum-versions}
+
+`slackclient` v2.x requires Python 3.7 (or higher).
+
+Client v1 support:
+- Python 2: Python 2.7 was supported in the 1.x version of the client up until Dec 31st, 2019.
+- We’ll continue to add support for any new Slack features that are released as they become available on the platform. Support for token rotation is an example of a Slack feature.
+- We will no longer be adding any new client-specific functionality to v1. Support for “asynchronous programming” is an example of a client feature. Another example is storing additional data on the client.
+- We are no longer addressing bug or security fixes.
+- Github branching: The `master` branch is used for v2 code. The `v1` branch is used for v1 code.
+
+### Import changes {#import-changes}
+
+ The goal of this project is to provide a set of tools that ease the creation of Python Slack apps. To better align with this goal, we’re renaming the main module to `slack`. From `slack`, developers can import various tools. 
+
+```Python
+# Before:
+# import slackclient
+
+# After:
+from slack import WebClient
+```
+
+### RTM API changes {#RTM-changes}
+
+An RTMClient allows apps to communicate with the Slack platform's Legacy RTM API. This client allows you to link callbacks to their corresponding events. When an event occurs, this client executes your callback while passing along any information it receives.
+
+Example app in v1:
+
+Here's a simple example app that replies "Hi \<@userid\>!" in a thread if you send it a message containing "Hello".
+
+```Python
+from slackclient import SlackClient
+
+slack_token = os.environ["SLACK_API_TOKEN"]
+client = SlackClient(slack_token)
+
+def say_hello(data):
+    if 'Hello' in data['text']:
+        channel_id = data['channel']
+        thread_ts = data['ts']
+        user = data['user']
+
+        client.api_call('chat.postMessage',
+            channel=channel_id,
+            text="Hi <@{}>!".format(user),
+            thread_ts=thread_ts
+        )
+
+if client.rtm_connect():
+    while client.server.connected is True:
+        for data in client.rtm_read():
+            if "type" in data and data["type"] == "message":
+                say_hello(data)
+else:
+    print "Connection Failed"
+```
+
+Example App in v2:
+
+Here's that same example app that replies "Hi \<\@userid\>!" in a thread if you send it a message containing "Hello".
+
+```Python
+import slack
+
+slack_token = os.environ["SLACK_API_TOKEN"]
+rtmclient = slack.RTMClient(token=slack_token)
+
+@slack.RTMClient.run_on(event='message')
+def say_hello(**payload):
+    data = payload['data']
+    if 'Hello' in data['text']:
+        channel_id = data['channel']
+        thread_ts = data['ts']
+        user = data['user']
+
+        webclient = payload['web_client']
+        webclient.chat_postMessage(
+            channel=channel_id,
+            text="Hi <@{}>!".format(user),
+            thread_ts=thread_ts
+        )
+
+rtmclient.start()
+```
+
+**We no longer store any team data.** In the current 1.x version of the client, we store some channel and user information internally on [`Server.py`](https://github.com/slackapi/python-slackclient/blob/master/slackclient/server.py) in `client`. This data will now be available in the open event for consumption. Developers are then free to store any information they choose. Here's an example:
+
+```Python
+# Retrieving the team domain.
+# Before:
+# team_domain = client.server.login_data["team"]["domain"]
+
+# After:
+@slack.RTMClient.run_on(event='open')
+def get_team_data(**payload):
+    team_domain = payload['data']['team']['domain']
+```
+
+RTM usage has been completely redesigned.
+
+For new projects, we recommend using [Events API](/apis/events-api). This package `slackclient` v2 doesn't have any supports for Events API but you can try https://github.com/slackapi/python-slack-events-api that works as an enhancement of Flask web framework.
+
+In the near future, we'll be providing better supports for Events API in the official SDK.
+
+### Web Client API changes {#web-client-changes}
+
+**Token refresh removed**: 
+
+This feature originally shipped as a part of workspace tokens. Since we've [gone in a new direction](https://medium.com/slack-developer-blog/the-latest-with-app-tokens-fe878d44130c) it's safe to remove this along with any related attributes stored on the client.
+
+- ~refresh_token~
+- ~token_update_callback~
+- ~client_id~
+- ~client_secret~
+
+**`#api_call()`**:
+
+- `timeout` param has been removed. Timeout is passed at the client level now.
+- `kwargs` param has been removed. You must specify where the data you pass belongs in the request. e.g. 'data' vs 'params' vs 'files'...etc
+```Python
+# Before:
+# from slackclient import SlackClient
+#
+# client = SlackClient(os.environ["SLACK_API_TOKEN"])
+# client.api_call('chat.postMessage',
+#     timeout=30,
+#     channel='C0123456',
+#     text="Hi!")
+
+# After:
+
+import slack
+
+client = slack.WebClient(os.environ["SLACK_API_TOKEN"], timeout=30)
+client.api_call('chat.postMessage', json={
+    'channel': 'C0123456',
+    'text': 'Hi!'})
+
+# Note: That while the above is allowed, the more efficient way to call that API is like this:
+client.chat_postMessage(
+    channel='C0123456',
+    text='Hi!')
+```
+
+The WebClient provides built-in methods for the Slack Web API. These methods act as helpers, enabling you to focus less on how the request is constructed. Here are a few things this provides:
+
+- Basic information about each method through the docstring.
+- Easy file uploads: You can pass in the location of a file and the library will handle opening and retrieving the file object to be transmitted.
+- Token type validation: This gives you better error messaging when you're attempting to consume an API method your token doesn't have access to.
+- Constructs requests using Slack preferred HTTP methods and content-types.
+
+### Error handling changes {#error-handling-changes}
+
+In version 1.x, a failed API call would return the error payload to you and expect you to handle the error. In version 2.x, a failed API call will throw an exception. To handle this in your code, you will have to wrap API calls with a `try except` block.
diff --git a/docs/english/web.md b/docs/english/web.md
new file mode 100644
index 000000000..b776ce5fd
--- /dev/null
+++ b/docs/english/web.md
@@ -0,0 +1,793 @@
+# Web client
+
+The Slack Web API allows you to build applications that interact with Slack in more complex ways than the integrations we provide out of the box.
+
+Accessing Slack API methods requires an OAuth token — read more about [installing with OAuth](/authentication/installing-with-oauth).
+
+Each of these [API methods](/reference/methods) is fully documented on our developer site at [docs.slack.dev](/).
+
+## Sending a message {#sending-messages}
+
+One of the primary uses of Slack is posting messages to a channel using the channel ID, or as a DM to another person using their user ID. This method will handle either a channel ID or a user ID passed to the `channel` parameter.
+
+Your app's bot user needs to be in the channel (otherwise, you will get either `not_in_channel` or `channel_not_found` error code). If your app has the [chat:write.public](/reference/scopes/chat.write.public) scope, your app can post messages without joining a channel as long as the channel is public. See the [chat.postMessage](/reference/methods/chat.postMessage) API method for more info.
+
+``` python
+import logging
+logging.basicConfig(level=logging.DEBUG)
+
+import os
+from slack_sdk import WebClient
+from slack_sdk.errors import SlackApiError
+
+slack_token = os.environ["SLACK_BOT_TOKEN"]
+client = WebClient(token=slack_token)
+
+try:
+    response = client.chat_postMessage(
+        channel="C0XXXXXX",
+        text="Hello from your app! :tada:"
+    )
+except SlackApiError as e:
+    # You will get a SlackApiError if "ok" is False
+    assert e.response["error"]    # str like 'invalid_auth', 'channel_not_found'
+```
+
+### Sending ephemeral messages
+
+Sending an ephemeral message, which is only visible to an assigned user in a specified channel, is nearly the same as sending a regular message but with an additional `user` parameter.
+
+``` python
+import os
+from slack_sdk import WebClient
+
+slack_token = os.environ["SLACK_BOT_TOKEN"]
+client = WebClient(token=slack_token)
+
+response = client.chat_postEphemeral(
+    channel="C0XXXXXX",
+    text="Hello silently from your app! :tada:",
+    user="U0XXXXXXX"
+)
+```
+
+See the [`chat.postEphemeral`](/reference/methods/chat.postEphemeral) API method for more details.
+
+### Sending streaming messages {#sending-streaming-messages}
+
+You can have your app's messages stream in to replicate conventional AI chatbot behavior. This is done through three Web API methods:
+
+* [`chat_startStream`](/reference/methods/chat.startStream)
+* [`chat_appendStream`](/reference/methods/chat.appendStream)
+* [`chat_stopStream`](/reference/methods/chat.stopStream)
+
+:::tip[The Python Slack SDK provides a [`chat_stream()`](https://docs.slack.dev/tools/python-slack-sdk/reference/web/client.html#slack_sdk.web.client.WebClient.chat_stream) helper utility to streamline calling these methods.]
+
+See the [_Streaming messages_](/tools/bolt-python/concepts/message-sending#streaming-messages) section of the Bolt for Python docs for implementation instructions. 
+
+:::
+
+#### Starting the message stream {#starting-stream}
+
+First you need to begin the message stream:
+
+```python
+# Example: Stream a response to any message
+@app.message()
+def handle_message(message, client):
+    channel_id = event.get("channel")
+    team_id = event.get("team")
+    thread_ts = event.get("thread_ts") or event.get("ts")
+    user_id = event.get("user")
+    
+    # Start a new message stream
+    stream_response = client.chat_startStream(
+        channel=channel_id,
+        recipient_team_id=team_id,
+        recipient_user_id=user_id,
+        thread_ts=thread_ts,
+    )
+    stream_ts = stream_response["ts"]
+```
+
+#### Appending content to the message stream {#appending-stream}
+
+With the stream started, you can then append text to it in chunks to convey a streaming effect.
+
+The structure of the text coming in will depend on your source. The following code snippet uses OpenAI's response structure as an example:
+
+```python
+# continued from above
+    for event in returned_message:
+        if event.type == "response.output_text.delta":
+            client.chat_appendStream(
+                channel=channel_id, 
+                ts=stream_ts, 
+                markdown_text=f"{event.delta}"
+            )
+        else:
+            continue
+```
+
+#### Stopping the message stream {#stopping-stream}
+
+Your app can then end the stream with the `chat_stopStream` method:
+
+```python
+# continued from above
+    client.chat_stopStream(
+        channel=channel_id, 
+        ts=stream_ts
+    )
+```
+
+The method also provides you an opportunity to request user feedback on your app's responses using the [feedback buttons](/reference/block-kit/block-elements/feedback-buttons-element) block element within the [context actions](/reference/block-kit/blocks/context-actions-block) block. The user will be presented with thumbs up and thumbs down buttons which send an action to your app when pressed.
+
+```python
+def create_feedback_block() -> List[Block]:
+    blocks: List[Block] = [
+        ContextActionsBlock(
+            elements=[
+                FeedbackButtonsElement(
+                    action_id="feedback",
+                    positive_button=FeedbackButtonObject(
+                        text="Good Response",
+                        accessibility_label="Submit positive feedback on this response",
+                        value="good-feedback",
+                    ),
+                    negative_button=FeedbackButtonObject(
+                        text="Bad Response",
+                        accessibility_label="Submit negative feedback on this response",
+                        value="bad-feedback",
+                    ),
+                )
+            ]
+        )
+    ]
+    return blocks
+
+@app.message()
+def handle_message(message, client):
+    # ... previous streaming code ...
+    
+    # Stop the stream and add interactive elements
+    feedback_block = create_feedback_block()
+    client.chat_stopStream(
+        channel=channel_id, 
+        ts=stream_ts, 
+        blocks=feedback_block
+    )
+```
+
+See [Formatting messages with Block Kit](#block-kit) below for more details on using Block Kit with messages.
+
+## Formatting messages with Block Kit {#block-kit}
+
+Messages posted from apps can contain more than just text; they can also include full user interfaces composed of blocks using [Block Kit](/block-kit).
+
+The [`chat.postMessage method`](/reference/methods/chat.postMessage) takes an optional blocks argument that allows you to customize the layout of a message. Blocks can be specified
+in a single array of either dict values or [slack_sdk.models.blocks.Block](https://docs.slack.dev/tools/python-slack-sdk/reference/models/blocks/index.html) objects.
+
+To send a message to a channel, use the channel's ID. For DMs, use the user's ID.
+
+``` python
+client.chat_postMessage(
+    channel="C0XXXXXX",
+    blocks=[
+        {
+            "type": "section",
+            "text": {
+                "type": "mrkdwn",
+                "text": "Danny Torrence left the following review for your property:"
+            }
+        },
+        {
+            "type": "section",
+            "text": {
+                "type": "mrkdwn",
+                "text": " \n :star: \n Doors had too many axe holes, guest in room " +
+                    "237 was far too rowdy, whole place felt stuck in the 1920s."
+            },
+            "accessory": {
+                "type": "image",
+                "image_url": "https://images.pexels.com/photos/750319/pexels-photo-750319.jpeg",
+                "alt_text": "Haunted hotel image"
+            }
+        },
+        {
+            "type": "section",
+            "fields": [
+                {
+                    "type": "mrkdwn",
+                    "text": "*Average Rating*\n1.0"
+                }
+            ]
+        }
+    ]
+)
+```
+
+:::tip[You can use [Block Kit Builder](https://app.slack.com/block-kit-builder/) to prototype your message's look and feel.]
+
+:::
+
+## Threading messages {#threading-messages}
+
+Threaded messages are a way of grouping messages together to provide greater context. You can reply to a thread or start a new threaded conversation by simply passing the original message's `ts` ID in the `thread_ts` attribute when posting a message. If you're replying to a threaded message, you'll pass the `thread_ts` ID of the message you're replying to.
+
+A channel or DM conversation is a nearly linear timeline of messages exchanged between people, bots, and apps. When one of these messages is replied to, it becomes the parent of a thread. By default, threaded replies do not appear directly in the channel, but are instead relegated to a kind of forked timeline descending from the parent message.
+
+``` python
+response = client.chat_postMessage(
+    channel="C0XXXXXX",
+    thread_ts="1476746830.000003",
+    text="Hello from your app! :tada:"
+)
+```
+
+By default, the `reply_broadcast` parameter is set to `False`. To indicate your reply is germane to all members of a channel and therefore a notification of the reply should be posted in-channel, set the `reply_broadcast` parameter to `True`.
+
+``` python
+response = client.chat_postMessage(
+    channel="C0XXXXXX",
+    thread_ts="1476746830.000003",
+    text="Hello from your app! :tada:",
+    reply_broadcast=True
+)
+```
+:::info[While threaded messages may contain attachments and message buttons, when your reply is broadcast to the channel, it'll actually be a reference to your reply and not the reply itself.] 
+
+When appearing in the channel, it won't contain any attachments or message buttons. Updates and deletion of threaded replies works the same as regular messages.
+
+:::
+
+Refer to the [threading messages](/messaging#threading) page for more information.
+
+## Updating a message {#updating-messages}
+
+Let's say you have a bot that posts the status of a request. When that request changes, you'll want to update the message to reflect it's state.
+
+``` python
+response = client.chat_update(
+    channel="C0XXXXXX",
+    ts="1476746830.000003",
+    text="updates from your app! :tada:"
+)
+```
+
+See the [`chat.update`](/reference/methods/chat.update) API method for formatting options and some special considerations when calling this with a bot user.
+
+## Deleting a message {#deleting-messages}
+
+Sometimes you need to delete things.
+
+``` python
+response = client.chat_delete(
+    channel="C0XXXXXX",
+    ts="1476745373.000002"
+)
+```
+
+See the [`chat.delete`](/reference/methods/chat.delete) API method for more
+details.
+
+## Conversations {#conversations}
+
+The Slack Conversations API provides your app with a unified interface to work with all the channel-like things encountered in Slack: public channels, private channels, direct messages, group direct messages, and shared channels.
+
+Refer to [using the Conversations API](/apis/web-api/using-the-conversations-api) for more information.
+
+### Direct messages {#direct-messages}
+
+The `conversations.open` API method opens either a 1:1 direct message with a single user or a multi-person direct message, depending on the number of users supplied to the `users` parameter. (For public or private channels, use the `conversations.create` API method.)
+
+Provide a `users` parameter as an array with 1-8 user IDs to open or resume a conversation. Providing only 1 ID will create a direct message. providing more IDs will create a new multi-party direct message or will resume an existing conversation.
+
+Subsequent calls with the same set of users will return the already existing conversation.
+
+``` python
+import os
+from slack_sdk import WebClient
+
+client = WebClient(token=os.environ["SLACK_BOT_TOKEN"])
+response = client.conversations_open(users=["W123456789", "U987654321"])
+```
+
+See the [`conversations.open`](/reference/methods/conversations.open) API method for additional details.
+
+### Creating channels {#creating-channels}
+
+Creates a new channel, either public or private. The `name` parameter is required and may contain numbers, letters, hyphens, or underscores, and must contain fewer than 80 characters. To make the channel private, set the optional `is_private` parameter to `True`.
+
+``` python
+import os
+from slack_sdk import WebClient
+from time import time
+
+client = WebClient(token=os.environ["SLACK_BOT_TOKEN"])
+channel_name = f"my-private-channel-{round(time())}"
+response = client.conversations_create(
+    name=channel_name,
+    is_private=True
+)
+channel_id = response["channel"]["id"]
+response = client.conversations_archive(channel=channel_id)
+```
+
+See the [`conversations.create`](/reference/methods/conversations.create) API method for additional details.
+
+### Getting conversation information {#getting-conversation-info}
+
+To retrieve a set of metadata about a channel (public, private, DM, or multi-party DM), use the `conversations.info` API method. The `channel` parameter is required and must be a valid channel ID. The optional `include_locale` boolean parameter will return locale data, which may be useful if you wish to return localized responses. The `include_num_members` boolean parameter will return the number of people in a channel.
+
+``` python
+import os
+from slack_sdk import WebClient
+
+client = WebClient(token=os.environ["SLACK_BOT_TOKEN"])
+response = client.conversations_info(
+    channel="C031415926",
+    include_num_members=1
+)
+```
+
+See the [`conversations.info`](/reference/methods/conversations.info) API method for more details.
+
+### Listing conversations {#listing-conversations}
+
+To get a list of all the conversations in a workspace, use the `conversations.list` API method. By default, only public conversations are returned. Use the `types` parameter specify which types of conversations you're interested in. Note that `types` is a string of comma-separated values.
+
+``` python
+import os
+from slack_sdk import WebClient
+
+client = WebClient(token=os.environ["SLACK_BOT_TOKEN"])
+response = client.conversations_list()
+conversations = response["channels"]
+```
+
+Use the `types` parameter to request additional channels, including `public_channel`, `private_channel`, `mpdm`, and `dm`. This parameter is a string of comma-separated values.
+
+``` python
+import os
+from slack_sdk import WebClient
+
+client = WebClient(token=os.environ["SLACK_BOT_TOKEN"])
+response = client.conversations_list(
+    types="public_channel, private_channel"
+)
+```
+
+Archived channels are included by default. You can exclude them by passing `exclude_archived=True` to your request.
+
+``` python
+response = client.conversations_list(exclude_archived=True)
+```
+
+See the [`conversations.list`](/reference/methods/conversations.list) API method for more details.
+
+### Getting members of a conversation {#getting-conversation-members}
+
+To get a list of members for a conversation, use the `conversations.members` API method with the required `channel` parameter.
+
+``` python
+import os
+from slack_sdk import WebClient
+
+client = WebClient(token=os.environ["SLACK_BOT_TOKEN"])
+response = client.conversations_members(channel="C16180339")
+user_ids = response["members"]
+```
+
+See the [`conversations.members`](/reference/methods/conversations.members) API method for more details.
+
+### Joining a conversation {#joining-conversations}
+
+Channels are the social hub of most Slack teams. Here's how you hop into one:
+
+``` python
+response = client.conversations_join(channel="C0XXXXXXY")
+```
+
+If you are already in the channel, the response is slightly different. The `already_in_channel` attribute will be true, and a limited `channel` object will be returned. Bot users cannot join a channel on their own, they need to be invited by another user.
+
+See the [`conversations.join`](/reference/methods/conversations.join) API method for more details.
+
+### Leaving a conversation {#leaving-conversations}
+
+To leave a conversation, use the `conversations.leave` API method with the required `channel` parameter containing the ID of the channel to leave.
+
+``` python
+import os
+from slack_sdk import WebClient
+
+client = WebClient(token=os.environ["SLACK_BOT_TOKEN"])
+response = client.conversations_leave(channel="C27182818")
+```
+
+See the [`conversations.leave`](/reference/methods/conversations.leave) API method for more details.
+
+## Opening a modal {#opening-modals}
+
+Modals allow you to collect data from users and display dynamic information in a focused surface. Modals use the same blocks that compose messages, with the addition of an `input` block.
+
+``` python
+from slack_sdk.signature import SignatureVerifier
+signature_verifier = SignatureVerifier(os.environ["SLACK_SIGNING_SECRET"])
+
+from flask import Flask, request, make_response, jsonify
+app = Flask(__name__)
+
+@app.route("/slack/events", methods=["POST"])
+def slack_app():
+    if not signature_verifier.is_valid_request(request.get_data(), request.headers):
+        return make_response("invalid request", 403)
+
+    if "payload" in request.form:
+        payload = json.loads(request.form["payload"])
+        if payload["type"] == "shortcut" and payload["callback_id"] == "test-shortcut":
+            # Open a new modal by a global shortcut
+            try:
+                api_response = client.views_open(
+                    trigger_id=payload["trigger_id"],
+                    view={
+                        "type": "modal",
+                        "callback_id": "modal-id",
+                        "title": {
+                            "type": "plain_text",
+                            "text": "Awesome Modal"
+                        },
+                        "submit": {
+                            "type": "plain_text",
+                            "text": "Submit"
+                        },
+                        "blocks": [
+                            {
+                                "type": "input",
+                                "block_id": "b-id",
+                                "label": {
+                                    "type": "plain_text",
+                                    "text": "Input label",
+                                },
+                                "element": {
+                                    "action_id": "a-id",
+                                    "type": "plain_text_input",
+                                }
+                            }
+                        ]
+                    }
+                )
+                return make_response("", 200)
+            except SlackApiError as e:
+                code = e.response["error"]
+                return make_response(f"Failed to open a modal due to {code}", 200)
+
+        if (
+            payload["type"] == "view_submission"
+            and payload["view"]["callback_id"] == "modal-id"
+        ):
+            # Handle a data submission request from the modal
+            submitted_data = payload["view"]["state"]["values"]
+            print(submitted_data)    # {'b-id': {'a-id': {'type': 'plain_text_input', 'value': 'your input'}}}
+
+            # Close this modal with an empty response body
+            return make_response("", 200)
+
+    return make_response("", 404)
+
+if __name__ == "__main__":
+    # export SLACK_SIGNING_SECRET=***
+    # export SLACK_BOT_TOKEN=xoxb-***
+    # export FLASK_ENV=development
+    # python3 app.py
+    app.run("localhost", 3000)
+```
+
+See the [`views.open`](/reference/methods/views.open) API method more details and additional parameters.
+
+Also, to run the above example, the following [Slack app
+configurations](https://api.slack.com/apps) are required.
+
+To run the above example, the following [app configurations](https://api.slack.com/apps) are required:
+
+* Enable **Interactivity** with a valid Request URL: `https://{your-public-domain}/slack/events`
+* Add a global shortcut with the callback ID: `open-modal-shortcut`
+
+## Updating and pushing modals {#updating-pushing-modals}
+
+In response to `view_submission` requests, you can tell Slack to update the current modal view by having `"response_action": update` and an updated view. There are also other `response_action` types, such as `errors` and `push`. Refer to the [modals](/surfaces/modals) page for more details.
+
+``` python
+if (
+    payload["type"] == "view_submission"
+    and payload["view"]["callback_id"] == "modal-id"
+):
+    # Handle a data submission request from the modal
+    submitted_data = payload["view"]["state"]["values"]
+    print(submitted_data)    # {'b-id': {'a-id': {'type': 'plain_text_input', 'value': 'your input'}}}
+
+    # Update the modal with a new view
+    return make_response(
+        jsonify(
+            {
+                "response_action": "update",
+                "view": {
+                    "type": "modal",
+                    "title": {"type": "plain_text", "text": "Accepted"},
+                    "close": {"type": "plain_text", "text": "Close"},
+                    "blocks": [
+                        {
+                            "type": "section",
+                            "text": {
+                                "type": "plain_text",
+                                "text": "Thanks for submitting the data!",
+                            },
+                        }
+                    ],
+                },
+            }
+        ),
+        200,
+    )
+```
+
+If your app modifies the current modal view when receiving `block_actions` requests from Slack, you can call the `views.update` API method with the given view ID.
+
+``` python
+private_metadata = "any str data you want to store"
+response = client.views_update(
+    view_id=payload["view"]["id"],
+    hash=payload["view"]["hash"],
+    view={
+        "type": "modal",
+        "callback_id": "modal-id",
+        "private_metadata": private_metadata,
+        "title": {
+            "type": "plain_text",
+            "text": "Awesome Modal"
+        },
+        "submit": {
+            "type": "plain_text",
+            "text": "Submit"
+        },
+        "close": {
+            "type": "plain_text",
+            "text": "Cancel"
+        },
+        "blocks": [
+            {
+                "type": "input",
+                "block_id": "b-id",
+                "label": {
+                    "type": "plain_text",
+                    "text": "Input label",
+                },
+                "element": {
+                    "action_id": "a-id",
+                    "type": "plain_text_input",
+                }
+            }
+        ]
+    }
+)
+```
+
+See the [`views.update`](/reference/methods/views.update) API method for more details.
+
+If you want to push a new view onto the modal instead of updating an existing view, see the [`views.push`](/reference/methods/views.push) API method.
+
+## Emoji reactions {#emoji}
+
+You can quickly respond to any message on Slack with an emoji reaction. Reactions can be used for any purpose: voting, checking off to-do items, showing excitement, or just for fun.
+
+This method adds a reaction (emoji) to an item (`file`, `file comment`, `channel message`, `group message`, or `direct message`). One of `file`, `file_comment`, or the combination of `channel` and `timestamp` must be specified. Note that your app's bot user needs to be in the channel (otherwise, you will get either a `not_in_channel` or `channel_not_found` error code).
+
+``` python
+response = client.reactions_add(
+    channel="C0XXXXXXX",
+    name="thumbsup",
+    timestamp="1234567890.123456"
+)
+```
+
+Removing an emoji reaction is basically the same format, but you'll use the `reactions.remove` API method instead of the `reactions.add` API method.
+
+``` python
+response = client.reactions_remove(
+    channel="C0XXXXXXX",
+    name="thumbsup",
+    timestamp="1234567890.123456"
+)
+```
+
+See the [`reactions.add`](/reference/methods/reactions.add) and [`reactions.remove`](/reference/methods/reactions.remove) API methods for more details.
+
+## Uploading files {#upload-files}
+
+You can upload files to Slack and share them with people in channels. Note that your app's bot user needs to be in the channel (otherwise, you will get either `not_in_channel` or `channel_not_found` error code).
+
+``` python
+response = client.files_upload_v2(
+    file="test.pdf",
+    title="Test upload",
+    channel="C3UKJTQAC",
+    initial_comment="Here is the latest version of the file!",
+)
+```
+
+If you want to share files within a thread, you can pass `thread_ts` in addition to `channel_id` as shown below:
+
+``` python
+response = client.files_upload_v2(
+    file="test.pdf",
+    title="Test upload",
+    channel="C3UKJTQAC",
+    thread_ts="1731398999.934122",
+    initial_comment="Here is the latest version of the file!",
+)
+```
+
+See the [`files.upload`](/reference/methods/files.upload) API method for more details.
+
+## Adding a remote file {#adding-remote-files}
+
+You can add a file information that is stored in an external storage rather than in Slack.
+
+``` python
+response = client.files_remote_add(
+    external_id="the-all-hands-deck-12345",
+    external_url="https://{your domain}/files/the-all-hands-deck-12345",
+    title="The All-hands Deck",
+    preview_image="./preview.png" # will be displayed in channels
+)
+```
+
+See the [files.remote.add](/reference/methods/files.remote.add) API method for more details.
+
+## Calling API methods {#calling-API-methods}
+
+This library covers all the public endpoints as the methods in `WebClient`. That said, you may see a bit of a delay with the library release. When you're in a hurry, you can directly use the `api_call` method as below.
+
+``` python
+import os
+from slack_sdk import WebClient
+
+client = WebClient(token=os.environ['SLACK_BOT_TOKEN'])
+response = client.api_call(
+    api_method='chat.postMessage',
+    params={'channel': '#random','text': "Hello world!"}
+)
+assert response["message"]["text"] == "Hello world!"
+```
+
+## AsyncWebClient {#asyncwebclient}
+
+The webhook client is available in asynchronous programming using the standard [asyncio](https://docs.python.org/3/library/asyncio.html) library. You use `AsyncWebhookClient` instead. `AsyncWebhookClient` internally relies on the [AIOHTTP](https://docs.aiohttp.org/en/stable/) library, but it is an optional dependency. To use this class, run `pip install aiohttp` beforehand.
+
+``` python
+import asyncio
+import os
+# requires: pip install aiohttp
+from slack_sdk.web.async_client import AsyncWebClient
+from slack_sdk.errors import SlackApiError
+
+client = AsyncWebClient(token=os.environ['SLACK_API_TOKEN'])
+
+# This must be an async method
+async def post_message():
+    try:
+        # Don't forget `await` keyword here
+        response = await client.chat_postMessage(
+            channel='#random',
+            text="Hello world!"
+        )
+        assert response["message"]["text"] == "Hello world!"
+    except SlackApiError as e:
+        assert e.response["ok"] is False
+        assert e.response["error"]  # str like 'invalid_auth', 'channel_not_found'
+        print(f"Got an error: {e.response['error']}")
+
+# This is the simplest way to run the async method
+# but you can go with any ways to run it
+asyncio.run(post_message())
+```
+
+## RetryHandler {#retryhandler}
+
+With the default settings, only `ConnectionErrorRetryHandler` with its default configuration (=only one retry in the manner of [exponential backoff and jitter](https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/)) is enabled. The retry handler retries if an API client encounters a connectivity-related failure (e.g., connection reset by peer).
+
+To use other retry handlers, you can pass a list of `RetryHandler` to the client constructor. For instance, you can add the built-in `RateLimitErrorRetryHandler` this way:
+
+``` python
+import os
+from slack_sdk.web import WebClient
+client = WebClient(token=os.environ["SLACK_BOT_TOKEN"])
+
+# This handler does retries when HTTP status 429 is returned
+from slack_sdk.http_retry.builtin_handlers import RateLimitErrorRetryHandler
+rate_limit_handler = RateLimitErrorRetryHandler(max_retry_count=1)
+
+# Enable rate limited error retries as well
+client.retry_handlers.append(rate_limit_handler)
+```
+
+You can also create one on your own by defining a new class that inherits `slack_sdk.http_retry RetryHandler` (`AsyncRetryHandler` for asyncio apps) and implements required methods (internals of `can_retry` / `prepare_for_next_retry`). Check out the source code for the ones that are built in to learn how to properly implement them.
+
+``` python
+import socket
+from typing import Optional
+from slack_sdk.http_retry import (RetryHandler, RetryState, HttpRequest, HttpResponse)
+from slack_sdk.http_retry.builtin_interval_calculators import BackoffRetryIntervalCalculator
+from slack_sdk.http_retry.jitter import RandomJitter
+
+class MyRetryHandler(RetryHandler):
+    def _can_retry(
+        self,
+        *,
+        state: RetryState,
+        request: HttpRequest,
+        response: Optional[HttpResponse] = None,
+        error: Optional[Exception] = None
+    ) -> bool:
+        # [Errno 104] Connection reset by peer
+        return error is not None and isinstance(error, socket.error) and error.errno == 104
+
+client = WebClient(
+    token=os.environ["SLACK_BOT_TOKEN"],
+    retry_handlers=[MyRetryHandler(
+        max_retry_count=1,
+        interval_calculator=BackoffRetryIntervalCalculator(
+            backoff_factor=0.5,
+            jitter=RandomJitter(),
+        ),
+    )],
+)
+```
+
+For asyncio apps, `Async` prefixed corresponding modules are available. All the methods in those methods are async/await compatible. Check [the source code](https://github.com/slackapi/python-slack-sdk/blob/main/slack_sdk/http_retry/async_handler.py) for more details.
+
+## Rate limits {#rate-limits}
+
+When posting messages to a channel, Slack allows apps to send no more than one message per channel per second. We allow bursts over that limit for short periods; however, if your app continues to exceed the limit over a longer period of time, it will be rate limited. Different API methods have other limits — be sure to check the [rate limits](/apis/web-api/rate-limits) and test that your app has a graceful fallback if it should hit those limits.
+
+If you go over these limits, Slack will begin returning *HTTP 429 Too Many Requests* errors, a JSON object containing the number of calls you have been making, and a *Retry-After* header containing the number of seconds until you can retry.
+
+Here's an example of how you might handle rate limited requests:
+
+``` python
+import os
+import time
+from slack_sdk import WebClient
+from slack_sdk.errors import SlackApiError
+
+client = WebClient(token=os.environ["SLACK_BOT_TOKEN"])
+
+# Simple wrapper for sending a Slack message
+def send_slack_message(channel, message):
+    return client.chat_postMessage(
+        channel=channel,
+        text=message
+    )
+
+# Make the API call and save results to `response`
+channel = "#random"
+message = "Hello, from Python!"
+# Do until being rate limited
+while True:
+    try:
+        response = send_slack_message(channel, message)
+    except SlackApiError as e:
+        if e.response.status_code == 429:
+            # The `Retry-After` header will tell you how long to wait before retrying
+            delay = int(e.response.headers['Retry-After'])
+            print(f"Rate limited. Retrying in {delay} seconds")
+            time.sleep(delay)
+            response = send_slack_message(channel, message)
+        else:
+            # other errors
+            raise e
+```
+
+Since v3.9.0, the built-in `RateLimitErrorRetryHandler` is available as an easier way to do retries for rate limited errors. Refer to the [RetryHandler](#retryhandler) section for more details.
+
+Refer to the [rate limits](/apis/web-api/rate-limits) page for more information.
diff --git a/docs/english/webhook.md b/docs/english/webhook.md
new file mode 100644
index 000000000..feaa4c7a9
--- /dev/null
+++ b/docs/english/webhook.md
@@ -0,0 +1,152 @@
+# Webhook client
+
+## Incoming webhooks {#incoming-webhooks}
+
+You can use `slack_sdk.webhook.WebhookClient` for [incoming webhooks](/messaging/sending-messages-using-incoming-webhooks) and message responses using [`response_url`](/interactivity/handling-user-interaction#message_responses) in payloads.
+
+To use [incoming webhooks](/messaging/sending-messages-using-incoming-webhooks), calling the `WebhookClient(url)#send(payload)` method works for you. The call posts a message in a channel associated with the webhook URL.
+
+``` python
+from slack_sdk.webhook import WebhookClient
+url = "https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX"
+webhook = WebhookClient(url)
+
+response = webhook.send(text="Hello!")
+assert response.status_code == 200
+assert response.body == "ok"
+```
+
+It's also possible to use `blocks` using [Block Kit](/block-kit).
+
+``` python
+from slack_sdk.webhook import WebhookClient
+url = "https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX"
+webhook = WebhookClient(url)
+response = webhook.send(
+    text="fallback",
+    blocks=[
+        {
+            "type": "section",
+            "text": {
+                "type": "mrkdwn",
+                "text": "You have a new request:\n**"
+            }
+        }
+    ]
+)
+```
+
+## The `response_url`
+
+User actions in channels generates a [`response_url`](/interactivity/handling-user-interaction#message_responses) and includes the URL in its payload. You can use `WebhookClient` to send a message via the `response_url`.
+
+``` python
+import os
+from slack_sdk.signature import SignatureVerifier
+signature_verifier = SignatureVerifier(
+    signing_secret=os.environ["SLACK_SIGNING_SECRET"]
+)
+
+from slack_sdk.webhook import WebhookClient
+
+from flask import Flask, request, make_response
+app = Flask(__name__)
+
+@app.route("/slack/events", methods=["POST"])
+def slack_app():
+    # Verify incoming requests from Slack
+    # https://docs.slack.dev/authentication/verifying-requests-from-slack
+    if not signature_verifier.is_valid(
+        body=request.get_data(),
+        timestamp=request.headers.get("X-Slack-Request-Timestamp"),
+        signature=request.headers.get("X-Slack-Signature")):
+        return make_response("invalid request", 403)
+
+    # Handle a slash command invocation
+    if "command" in request.form \
+        and request.form["command"] == "/reply-this":
+        response_url = request.form["response_url"]
+        text = request.form["text"]
+        webhook = WebhookClient(response_url)
+        # Send a reply in the channel
+        response = webhook.send(text=f"You said '{text}'")
+        # Acknowledge this request
+        return make_response("", 200)
+
+    return make_response("", 404)
+```
+
+## AsyncWebhookClient {#asyncwebhookclient}
+
+The webhook client is available in asynchronous programming using the standard [asyncio](https://docs.python.org/3/library/asyncio.html) library. You use `AsyncWebhookClient` instead. `AsyncWebhookClient` internally relies on the [AIOHTTP](https://docs.aiohttp.org/en/stable/) library, but it is an optional dependency. To use this class, run `pip install aiohttp` beforehand.
+
+``` python
+import asyncio
+# requires: pip install aiohttp
+from slack_sdk.webhook.async_client import AsyncWebhookClient
+url = "https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX"
+
+async def send_message_via_webhook(url: str):
+    webhook = AsyncWebhookClient(url)
+    response = await webhook.send(text="Hello!")
+    assert response.status_code == 200
+    assert response.body == "ok"
+
+# This is the simplest way to run the async method
+# but you can go with any ways to run it
+asyncio.run(send_message_via_webhook(url))
+```
+
+## RetryHandler {#retryhandler}
+
+With the default settings, only `ConnectionErrorRetryHandler` with its default configuration (=only one retry in the manner of [exponential backoff and jitter](https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/)) is enabled. The retry handler retries if an API client encounters a connectivity-related failure (e.g., connection reset by peer).
+
+To use other retry handlers, you can pass a list of `RetryHandler` to the client constructor. For instance, you can add the built-in `RateLimitErrorRetryHandler` this way:
+
+``` python
+from slack_sdk.webhook import WebhookClient
+url = "https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX"
+webhook = WebhookClient(url=url)
+
+# This handler does retries when HTTP status 429 is returned
+from slack_sdk.http_retry.builtin_handlers import RateLimitErrorRetryHandler
+rate_limit_handler = RateLimitErrorRetryHandler(max_retry_count=1)
+
+# Enable rate limited error retries as well
+client.retry_handlers.append(rate_limit_handler)
+```
+
+You can also create one on your own by defining a new class that inherits `slack_sdk.http_retry RetryHandler` (`AsyncRetryHandler` for asyncio apps) and implements required methods (internals of `can_retry` / `prepare_for_next_retry`). Check out the source code for the ones that are built in to learn how to properly implement them.
+
+``` python
+import socket
+from typing import Optional
+from slack_sdk.http_retry import (RetryHandler, RetryState, HttpRequest, HttpResponse)
+from slack_sdk.http_retry.builtin_interval_calculators import BackoffRetryIntervalCalculator
+from slack_sdk.http_retry.jitter import RandomJitter
+
+class MyRetryHandler(RetryHandler):
+    def _can_retry(
+        self,
+        *,
+        state: RetryState,
+        request: HttpRequest,
+        response: Optional[HttpResponse] = None,
+        error: Optional[Exception] = None
+    ) -> bool:
+        # [Errno 104] Connection reset by peer
+        return error is not None and isinstance(error, socket.error) and error.errno == 104
+
+webhook = WebhookClient(
+    url=url,
+    retry_handlers=[MyRetryHandler(
+        max_retry_count=1,
+        interval_calculator=BackoffRetryIntervalCalculator(
+            backoff_factor=0.5,
+            jitter=RandomJitter(),
+        ),
+    )],
+)
+```
+
+For asyncio apps, `Async` prefixed corresponding modules are available. All the methods in those methods are async/await compatible. Check [the source code](https://github.com/slackapi/python-slack-sdk/blob/main/slack_sdk/http_retry/async_handler.py) for more details.
diff --git a/docs/reference/aiohttp_version_checker.html b/docs/reference/aiohttp_version_checker.html
new file mode 100644
index 000000000..9430e24fa
--- /dev/null
+++ b/docs/reference/aiohttp_version_checker.html
@@ -0,0 +1,100 @@
+
+
+
+
+
+
+slack_sdk.aiohttp_version_checker API documentation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

Module slack_sdk.aiohttp_version_checker

+
+
+

Internal module for checking aiohttp compatibility of async modules

+
+
+
+
+
+
+

Functions

+
+
+def validate_aiohttp_version(aiohttp_version: str,
print_warning: Callable[[str], None] = <function _print_warning_log>)
+
+
+
+ +Expand source code + +
def validate_aiohttp_version(
+    aiohttp_version: str,
+    print_warning: Callable[[str], None] = _print_warning_log,
+):
+    if aiohttp_version is not None:
+        elements = aiohttp_version.split(".")
+        if len(elements) >= 3:
+            # patch version can be a non-numeric value
+            major, minor, patch = int(elements[0]), int(elements[1]), elements[2]
+            if major <= 2 or (major == 3 and (minor == 6 or (minor == 7 and patch == "0"))):
+                print_warning(
+                    "We highly recommend upgrading aiohttp to 3.7.3 or higher versions."
+                    "An older version of the library may not work with the Slack server-side in the future."
+                )
+
+
+
+
+
+
+
+
+ +
+ + + diff --git a/docs/reference/audit_logs/async_client.html b/docs/reference/audit_logs/async_client.html new file mode 100644 index 000000000..600bb9c35 --- /dev/null +++ b/docs/reference/audit_logs/async_client.html @@ -0,0 +1,736 @@ + + + + + + +slack_sdk.audit_logs.async_client API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.audit_logs.async_client

+
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class AsyncAuditLogsClient +(token: str,
timeout: int = 30,
ssl: ssl.SSLContext | None = None,
proxy: str | None = None,
base_url: str = 'https://api.slack.com/audit/v1/',
session: aiohttp.client.ClientSession | None = None,
trust_env_in_session: bool = False,
auth: aiohttp.helpers.BasicAuth | None = None,
default_headers: Dict[str, str] | None = None,
user_agent_prefix: str | None = None,
user_agent_suffix: str | None = None,
logger: logging.Logger | None = None,
retry_handlers: List[slack_sdk.http_retry.async_handler.AsyncRetryHandler] | None = None)
+
+
+
+ +Expand source code + +
class AsyncAuditLogsClient:
+    BASE_URL = "https://api.slack.com/audit/v1/"
+
+    token: str
+    timeout: int
+    ssl: Optional[SSLContext]
+    proxy: Optional[str]
+    base_url: str
+    session: Optional[ClientSession]
+    trust_env_in_session: bool
+    auth: Optional[BasicAuth]
+    default_headers: Dict[str, str]
+    logger: logging.Logger
+    retry_handlers: List[AsyncRetryHandler]
+
+    def __init__(
+        self,
+        token: str,
+        timeout: int = 30,
+        ssl: Optional[SSLContext] = None,
+        proxy: Optional[str] = None,
+        base_url: str = BASE_URL,
+        session: Optional[ClientSession] = None,
+        trust_env_in_session: bool = False,
+        auth: Optional[BasicAuth] = None,
+        default_headers: Optional[Dict[str, str]] = None,
+        user_agent_prefix: Optional[str] = None,
+        user_agent_suffix: Optional[str] = None,
+        logger: Optional[logging.Logger] = None,
+        retry_handlers: Optional[List[AsyncRetryHandler]] = None,
+    ):
+        """API client for Audit Logs API
+        See https://docs.slack.dev/admins/audit-logs-api/ for more details
+
+        Args:
+            token: An admin user's token, which starts with `xoxp-`
+            timeout: Request timeout (in seconds)
+            ssl: `ssl.SSLContext` to use for requests
+            proxy: Proxy URL (e.g., `localhost:9000`, `http://localhost:9000`)
+            base_url: The base URL for API calls
+            session: `aiohttp.ClientSession` instance
+            trust_env_in_session: True/False for `aiohttp.ClientSession`
+            auth: Basic auth info for `aiohttp.ClientSession`
+            default_headers: Request headers to add to all requests
+            user_agent_prefix: Prefix for User-Agent header value
+            user_agent_suffix: Suffix for User-Agent header value
+            logger: Custom logger
+            retry_handlers: Retry handlers
+        """
+        self.token = token
+        self.timeout = timeout
+        self.ssl = ssl
+        self.proxy = proxy
+        self.base_url = base_url
+        self.session = session
+        self.trust_env_in_session = trust_env_in_session
+        self.auth = auth
+        self.default_headers = default_headers if default_headers else {}
+        self.default_headers["User-Agent"] = get_user_agent(user_agent_prefix, user_agent_suffix)
+        self.logger = logger if logger is not None else logging.getLogger(__name__)
+        self.retry_handlers = retry_handlers if retry_handlers is not None else async_default_handlers()
+
+        if self.proxy is None or len(self.proxy.strip()) == 0:
+            env_variable = load_http_proxy_from_env(self.logger)
+            if env_variable is not None:
+                self.proxy = env_variable
+
+    async def schemas(
+        self,
+        *,
+        query_params: Optional[Dict[str, Any]] = None,
+        headers: Optional[Dict[str, str]] = None,
+    ) -> AuditLogsResponse:
+        """Returns information about the kind of objects which the Audit Logs API
+        returns as a list of all objects and a short description.
+        Authentication not required.
+
+        Args:
+            query_params: Set any values if you want to add query params
+            headers: Additional request headers
+        Returns:
+            API response
+        """
+        return await self.api_call(
+            path="schemas",
+            query_params=query_params,
+            headers=headers,
+        )
+
+    async def actions(
+        self,
+        *,
+        query_params: Optional[Dict[str, Any]] = None,
+        headers: Optional[Dict[str, str]] = None,
+    ) -> AuditLogsResponse:
+        """Returns information about the kind of actions that the Audit Logs API
+        returns as a list of all actions and a short description of each.
+        Authentication not required.
+
+        Args:
+            query_params: Set any values if you want to add query params
+            headers: Additional request headers
+
+        Returns:
+            API response
+        """
+        return await self.api_call(
+            path="actions",
+            query_params=query_params,
+            headers=headers,
+        )
+
+    async def logs(
+        self,
+        *,
+        latest: Optional[int] = None,
+        oldest: Optional[int] = None,
+        limit: Optional[int] = None,
+        action: Optional[str] = None,
+        actor: Optional[str] = None,
+        entity: Optional[str] = None,
+        cursor: Optional[str] = None,
+        additional_query_params: Optional[Dict[str, Any]] = None,
+        headers: Optional[Dict[str, str]] = None,
+    ) -> AuditLogsResponse:
+        """This is the primary endpoint for retrieving actual audit events from your organization.
+        It will return a list of actions that have occurred on the installed workspace or grid organization.
+        Authentication required.
+
+        The following filters can be applied in order to narrow the range of actions returned.
+        Filters are added as query string parameters and can be combined together.
+        Multiple filter parameters are additive (a boolean AND) and are separated
+        with an ampersand (&) in the query string. Filtering is entirely optional.
+
+        Args:
+            latest: Unix timestamp of the most recent audit event to include (inclusive).
+            oldest: Unix timestamp of the least recent audit event to include (inclusive).
+                Data is not available prior to March 2018.
+            limit: Number of results to optimistically return, maximum 9999.
+            action: Name of the action.
+            actor: User ID who initiated the action.
+            entity: ID of the target entity of the action (such as a channel, workspace, organization, file).
+            cursor: The next page cursor of pagination
+            additional_query_params: Add anything else if you need to use the ones this library does not support
+            headers: Additional request headers
+
+        Returns:
+            API response
+        """
+        query_params = {
+            "latest": latest,
+            "oldest": oldest,
+            "limit": limit,
+            "action": action,
+            "actor": actor,
+            "entity": entity,
+            "cursor": cursor,
+        }
+        if additional_query_params is not None:
+            query_params.update(additional_query_params)
+        query_params = {k: v for k, v in query_params.items() if v is not None}
+        return await self.api_call(
+            path="logs",
+            query_params=query_params,
+            headers=headers,
+        )
+
+    async def api_call(
+        self,
+        *,
+        http_verb: str = "GET",
+        path: str,
+        query_params: Optional[Dict[str, Any]] = None,
+        body_params: Optional[Dict[str, Any]] = None,
+        headers: Optional[Dict[str, str]] = None,
+    ) -> AuditLogsResponse:
+        url = f"{self.base_url}{path}"
+        return await self._perform_http_request(
+            http_verb=http_verb,
+            url=url,
+            query_params=query_params,
+            body_params=body_params,
+            headers=_build_request_headers(
+                token=self.token,
+                default_headers=self.default_headers,
+                additional_headers=headers,
+            ),
+        )
+
+    async def _perform_http_request(
+        self,
+        *,
+        http_verb: str,
+        url: str,
+        query_params: Optional[Dict[str, Any]],
+        body_params: Optional[Dict[str, Any]],
+        headers: Dict[str, str],
+    ) -> AuditLogsResponse:
+        if body_params is not None:
+            body_params = json.dumps(body_params)  # type: ignore[assignment]
+        headers["Content-Type"] = "application/json;charset=utf-8"
+
+        session: Optional[ClientSession] = None
+        use_running_session = self.session and not self.session.closed
+        if use_running_session:
+            session = self.session
+        else:
+            session = aiohttp.ClientSession(
+                timeout=aiohttp.ClientTimeout(total=self.timeout),
+                auth=self.auth,
+                trust_env=self.trust_env_in_session,
+            )
+
+        last_error = None
+        resp: Optional[AuditLogsResponse] = None
+        try:
+            request_kwargs = {
+                "headers": headers,
+                "params": query_params,
+                "data": body_params,
+                "ssl": self.ssl,
+                "proxy": self.proxy,
+            }
+            retry_request = RetryHttpRequest(
+                method=http_verb,
+                url=url,
+                headers=headers,  # type: ignore[arg-type]
+                body_params=body_params,
+            )
+
+            retry_state = RetryState()
+            counter_for_safety = 0
+            while counter_for_safety < 100:
+                counter_for_safety += 1
+                # If this is a retry, the next try started here. We can reset the flag.
+                retry_state.next_attempt_requested = False
+                retry_response: Optional[RetryHttpResponse] = None
+                response_body = ""
+
+                if self.logger.level <= logging.DEBUG:
+                    headers_for_logging = {
+                        k: "(redacted)" if k.lower() == "authorization" else v for k, v in headers.items()
+                    }
+                    self.logger.debug(
+                        f"Sending a request - "
+                        f"url: {url}, "
+                        f"params: {query_params}, "
+                        f"body: {body_params}, "
+                        f"headers: {headers_for_logging}"
+                    )
+
+                try:
+                    async with session.request(http_verb, url, **request_kwargs) as res:  # type: ignore[arg-type, union-attr] # noqa: E501
+                        try:
+                            response_body = await res.text()
+                            retry_response = RetryHttpResponse(
+                                status_code=res.status,
+                                headers=res.headers,  # type: ignore[arg-type]
+                                data=response_body.encode("utf-8") if response_body is not None else None,
+                            )
+                        except aiohttp.ContentTypeError:
+                            self.logger.debug(f"No response data returned from the following API call: {url}.")
+                            retry_response = RetryHttpResponse(
+                                status_code=res.status,
+                                headers=res.headers,  # type: ignore[arg-type]
+                            )
+                        except json.decoder.JSONDecodeError as e:
+                            message = f"Failed to parse the response body: {str(e)}"
+                            raise SlackApiError(message, res)
+
+                        if res.status == 429:
+                            for handler in self.retry_handlers:
+                                if await handler.can_retry_async(
+                                    state=retry_state,
+                                    request=retry_request,
+                                    response=retry_response,
+                                ):
+                                    if self.logger.level <= logging.DEBUG:
+                                        self.logger.info(
+                                            f"A retry handler found: {type(handler).__name__} "
+                                            f"for {http_verb} {url} - rate_limited"
+                                        )
+                                    await handler.prepare_for_next_attempt_async(
+                                        state=retry_state,
+                                        request=retry_request,
+                                        response=retry_response,
+                                    )
+                                    break
+
+                        if retry_state.next_attempt_requested is False:
+                            resp = AuditLogsResponse(
+                                url=url,
+                                status_code=res.status,
+                                raw_body=response_body,
+                                headers=res.headers,  # type: ignore[arg-type]
+                            )
+                            _debug_log_response(self.logger, resp)
+                            return resp
+
+                except Exception as e:
+                    last_error = e
+                    for handler in self.retry_handlers:
+                        if await handler.can_retry_async(
+                            state=retry_state,
+                            request=retry_request,
+                            response=retry_response,
+                            error=e,
+                        ):
+                            if self.logger.level <= logging.DEBUG:
+                                self.logger.info(
+                                    f"A retry handler found: {type(handler).__name__} " f"for {http_verb} {url} - {e}"
+                                )
+                            await handler.prepare_for_next_attempt_async(
+                                state=retry_state,
+                                request=retry_request,
+                                response=retry_response,
+                                error=e,
+                            )
+                            break
+
+                    if retry_state.next_attempt_requested is False:
+                        raise last_error
+
+            if resp is not None:
+                return resp
+            raise last_error  # type: ignore[misc]
+
+        finally:
+            if not use_running_session:
+                await session.close()  # type: ignore[union-attr]
+
+        return resp
+
+

API client for Audit Logs API +See https://docs.slack.dev/admins/audit-logs-api/ for more details

+

Args

+
+
token
+
An admin user's token, which starts with xoxp-
+
timeout
+
Request timeout (in seconds)
+
ssl
+
ssl.SSLContext to use for requests
+
proxy
+
Proxy URL (e.g., localhost:9000, http://localhost:9000)
+
base_url
+
The base URL for API calls
+
session
+
aiohttp.ClientSession instance
+
trust_env_in_session
+
True/False for aiohttp.ClientSession
+
auth
+
Basic auth info for aiohttp.ClientSession
+
default_headers
+
Request headers to add to all requests
+
user_agent_prefix
+
Prefix for User-Agent header value
+
user_agent_suffix
+
Suffix for User-Agent header value
+
logger
+
Custom logger
+
retry_handlers
+
Retry handlers
+
+

Class variables

+
+
var BASE_URL
+
+

The type of the None singleton.

+
+
var auth : aiohttp.helpers.BasicAuth | None
+
+

The type of the None singleton.

+
+
var base_url : str
+
+

The type of the None singleton.

+
+
var default_headers : Dict[str, str]
+
+

The type of the None singleton.

+
+
var logger : logging.Logger
+
+

The type of the None singleton.

+
+
var proxy : str | None
+
+

The type of the None singleton.

+
+
var retry_handlers : List[slack_sdk.http_retry.async_handler.AsyncRetryHandler]
+
+

The type of the None singleton.

+
+
var session : aiohttp.client.ClientSession | None
+
+

The type of the None singleton.

+
+
var ssl : ssl.SSLContext | None
+
+

The type of the None singleton.

+
+
var timeout : int
+
+

The type of the None singleton.

+
+
var token : str
+
+

The type of the None singleton.

+
+
var trust_env_in_session : bool
+
+

The type of the None singleton.

+
+
+

Methods

+
+
+async def actions(self,
*,
query_params: Dict[str, Any] | None = None,
headers: Dict[str, str] | None = None) ‑> AuditLogsResponse
+
+
+
+ +Expand source code + +
async def actions(
+    self,
+    *,
+    query_params: Optional[Dict[str, Any]] = None,
+    headers: Optional[Dict[str, str]] = None,
+) -> AuditLogsResponse:
+    """Returns information about the kind of actions that the Audit Logs API
+    returns as a list of all actions and a short description of each.
+    Authentication not required.
+
+    Args:
+        query_params: Set any values if you want to add query params
+        headers: Additional request headers
+
+    Returns:
+        API response
+    """
+    return await self.api_call(
+        path="actions",
+        query_params=query_params,
+        headers=headers,
+    )
+
+

Returns information about the kind of actions that the Audit Logs API +returns as a list of all actions and a short description of each. +Authentication not required.

+

Args

+
+
query_params
+
Set any values if you want to add query params
+
headers
+
Additional request headers
+
+

Returns

+

API response

+
+
+async def api_call(self,
*,
http_verb: str = 'GET',
path: str,
query_params: Dict[str, Any] | None = None,
body_params: Dict[str, Any] | None = None,
headers: Dict[str, str] | None = None) ‑> AuditLogsResponse
+
+
+
+ +Expand source code + +
async def api_call(
+    self,
+    *,
+    http_verb: str = "GET",
+    path: str,
+    query_params: Optional[Dict[str, Any]] = None,
+    body_params: Optional[Dict[str, Any]] = None,
+    headers: Optional[Dict[str, str]] = None,
+) -> AuditLogsResponse:
+    url = f"{self.base_url}{path}"
+    return await self._perform_http_request(
+        http_verb=http_verb,
+        url=url,
+        query_params=query_params,
+        body_params=body_params,
+        headers=_build_request_headers(
+            token=self.token,
+            default_headers=self.default_headers,
+            additional_headers=headers,
+        ),
+    )
+
+
+
+
+async def logs(self,
*,
latest: int | None = None,
oldest: int | None = None,
limit: int | None = None,
action: str | None = None,
actor: str | None = None,
entity: str | None = None,
cursor: str | None = None,
additional_query_params: Dict[str, Any] | None = None,
headers: Dict[str, str] | None = None) ‑> AuditLogsResponse
+
+
+
+ +Expand source code + +
async def logs(
+    self,
+    *,
+    latest: Optional[int] = None,
+    oldest: Optional[int] = None,
+    limit: Optional[int] = None,
+    action: Optional[str] = None,
+    actor: Optional[str] = None,
+    entity: Optional[str] = None,
+    cursor: Optional[str] = None,
+    additional_query_params: Optional[Dict[str, Any]] = None,
+    headers: Optional[Dict[str, str]] = None,
+) -> AuditLogsResponse:
+    """This is the primary endpoint for retrieving actual audit events from your organization.
+    It will return a list of actions that have occurred on the installed workspace or grid organization.
+    Authentication required.
+
+    The following filters can be applied in order to narrow the range of actions returned.
+    Filters are added as query string parameters and can be combined together.
+    Multiple filter parameters are additive (a boolean AND) and are separated
+    with an ampersand (&) in the query string. Filtering is entirely optional.
+
+    Args:
+        latest: Unix timestamp of the most recent audit event to include (inclusive).
+        oldest: Unix timestamp of the least recent audit event to include (inclusive).
+            Data is not available prior to March 2018.
+        limit: Number of results to optimistically return, maximum 9999.
+        action: Name of the action.
+        actor: User ID who initiated the action.
+        entity: ID of the target entity of the action (such as a channel, workspace, organization, file).
+        cursor: The next page cursor of pagination
+        additional_query_params: Add anything else if you need to use the ones this library does not support
+        headers: Additional request headers
+
+    Returns:
+        API response
+    """
+    query_params = {
+        "latest": latest,
+        "oldest": oldest,
+        "limit": limit,
+        "action": action,
+        "actor": actor,
+        "entity": entity,
+        "cursor": cursor,
+    }
+    if additional_query_params is not None:
+        query_params.update(additional_query_params)
+    query_params = {k: v for k, v in query_params.items() if v is not None}
+    return await self.api_call(
+        path="logs",
+        query_params=query_params,
+        headers=headers,
+    )
+
+

This is the primary endpoint for retrieving actual audit events from your organization. +It will return a list of actions that have occurred on the installed workspace or grid organization. +Authentication required.

+

The following filters can be applied in order to narrow the range of actions returned. +Filters are added as query string parameters and can be combined together. +Multiple filter parameters are additive (a boolean AND) and are separated +with an ampersand (&) in the query string. Filtering is entirely optional.

+

Args

+
+
latest
+
Unix timestamp of the most recent audit event to include (inclusive).
+
oldest
+
Unix timestamp of the least recent audit event to include (inclusive). +Data is not available prior to March 2018.
+
limit
+
Number of results to optimistically return, maximum 9999.
+
action
+
Name of the action.
+
actor
+
User ID who initiated the action.
+
entity
+
ID of the target entity of the action (such as a channel, workspace, organization, file).
+
cursor
+
The next page cursor of pagination
+
additional_query_params
+
Add anything else if you need to use the ones this library does not support
+
headers
+
Additional request headers
+
+

Returns

+

API response

+
+
+async def schemas(self,
*,
query_params: Dict[str, Any] | None = None,
headers: Dict[str, str] | None = None) ‑> AuditLogsResponse
+
+
+
+ +Expand source code + +
async def schemas(
+    self,
+    *,
+    query_params: Optional[Dict[str, Any]] = None,
+    headers: Optional[Dict[str, str]] = None,
+) -> AuditLogsResponse:
+    """Returns information about the kind of objects which the Audit Logs API
+    returns as a list of all objects and a short description.
+    Authentication not required.
+
+    Args:
+        query_params: Set any values if you want to add query params
+        headers: Additional request headers
+    Returns:
+        API response
+    """
+    return await self.api_call(
+        path="schemas",
+        query_params=query_params,
+        headers=headers,
+    )
+
+

Returns information about the kind of objects which the Audit Logs API +returns as a list of all objects and a short description. +Authentication not required.

+

Args

+
+
query_params
+
Set any values if you want to add query params
+
headers
+
Additional request headers
+
+

Returns

+

API response

+
+
+
+
+
+
+ +
+ + + diff --git a/docs/reference/audit_logs/index.html b/docs/reference/audit_logs/index.html new file mode 100644 index 000000000..940d34e1c --- /dev/null +++ b/docs/reference/audit_logs/index.html @@ -0,0 +1,828 @@ + + + + + + +slack_sdk.audit_logs API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.audit_logs

+
+
+

Audit Logs API is a set of APIs for monitoring what’s happening in your Enterprise Grid organization.

+

Refer to https://docs.slack.dev/tools/python-slack-sdk/audit-logs for details.

+
+
+

Sub-modules

+
+
slack_sdk.audit_logs.async_client
+
+
+
+
slack_sdk.audit_logs.v1
+
+

Audit Logs API is a set of APIs for monitoring what’s happening in your Enterprise Grid organization …

+
+
+
+
+
+
+
+
+

Classes

+
+
+class AuditLogsClient +(token: str,
timeout: int = 30,
ssl: ssl.SSLContext | None = None,
proxy: str | None = None,
base_url: str = 'https://api.slack.com/audit/v1/',
default_headers: Dict[str, str] | None = None,
user_agent_prefix: str | None = None,
user_agent_suffix: str | None = None,
logger: logging.Logger | None = None,
retry_handlers: List[RetryHandler] | None = None)
+
+
+
+ +Expand source code + +
class AuditLogsClient:
+    BASE_URL = "https://api.slack.com/audit/v1/"
+
+    token: str
+    timeout: int
+    ssl: Optional[SSLContext]
+    proxy: Optional[str]
+    base_url: str
+    default_headers: Dict[str, str]
+    logger: logging.Logger
+    retry_handlers: List[RetryHandler]
+
+    def __init__(
+        self,
+        token: str,
+        timeout: int = 30,
+        ssl: Optional[SSLContext] = None,
+        proxy: Optional[str] = None,
+        base_url: str = BASE_URL,
+        default_headers: Optional[Dict[str, str]] = None,
+        user_agent_prefix: Optional[str] = None,
+        user_agent_suffix: Optional[str] = None,
+        logger: Optional[logging.Logger] = None,
+        retry_handlers: Optional[List[RetryHandler]] = None,
+    ):
+        """API client for Audit Logs API
+        See https://docs.slack.dev/admins/audit-logs-api/ for more details
+
+        Args:
+            token: An admin user's token, which starts with `xoxp-`
+            timeout: Request timeout (in seconds)
+            ssl: `ssl.SSLContext` to use for requests
+            proxy: Proxy URL (e.g., `localhost:9000`, `http://localhost:9000`)
+            base_url: The base URL for API calls
+            default_headers: Request headers to add to all requests
+            user_agent_prefix: Prefix for User-Agent header value
+            user_agent_suffix: Suffix for User-Agent header value
+            logger: Custom logger
+            retry_handlers: Retry handlers
+        """
+        self.token = token
+        self.timeout = timeout
+        self.ssl = ssl
+        self.proxy = proxy
+        self.base_url = base_url
+        self.default_headers = default_headers if default_headers else {}
+        self.default_headers["User-Agent"] = get_user_agent(user_agent_prefix, user_agent_suffix)
+        self.logger = logger if logger is not None else logging.getLogger(__name__)
+        self.retry_handlers = retry_handlers if retry_handlers is not None else default_retry_handlers()
+
+        if self.proxy is None or len(self.proxy.strip()) == 0:
+            env_variable = load_http_proxy_from_env(self.logger)
+            if env_variable is not None:
+                self.proxy = env_variable
+
+    def schemas(
+        self,
+        *,
+        query_params: Optional[Dict[str, Any]] = None,
+        headers: Optional[Dict[str, str]] = None,
+    ) -> AuditLogsResponse:
+        """Returns information about the kind of objects which the Audit Logs API
+        returns as a list of all objects and a short description.
+        Authentication not required.
+
+        Args:
+            query_params: Set any values if you want to add query params
+            headers: Additional request headers
+        Returns:
+            API response
+        """
+        return self.api_call(
+            path="schemas",
+            query_params=query_params,
+            headers=headers,
+        )
+
+    def actions(
+        self,
+        *,
+        query_params: Optional[Dict[str, Any]] = None,
+        headers: Optional[Dict[str, str]] = None,
+    ) -> AuditLogsResponse:
+        """Returns information about the kind of actions that the Audit Logs API
+        returns as a list of all actions and a short description of each.
+        Authentication not required.
+
+        Args:
+            query_params: Set any values if you want to add query params
+            headers: Additional request headers
+
+        Returns:
+            API response
+        """
+        return self.api_call(
+            path="actions",
+            query_params=query_params,
+            headers=headers,
+        )
+
+    def logs(
+        self,
+        *,
+        latest: Optional[int] = None,
+        oldest: Optional[int] = None,
+        limit: Optional[int] = None,
+        action: Optional[str] = None,
+        actor: Optional[str] = None,
+        entity: Optional[str] = None,
+        cursor: Optional[str] = None,
+        additional_query_params: Optional[Dict[str, Any]] = None,
+        headers: Optional[Dict[str, str]] = None,
+    ) -> AuditLogsResponse:
+        """This is the primary endpoint for retrieving actual audit events from your organization.
+        It will return a list of actions that have occurred on the installed workspace or grid organization.
+        Authentication required.
+
+        The following filters can be applied in order to narrow the range of actions returned.
+        Filters are added as query string parameters and can be combined together.
+        Multiple filter parameters are additive (a boolean AND) and are separated
+        with an ampersand (&) in the query string. Filtering is entirely optional.
+
+        Args:
+            latest: Unix timestamp of the most recent audit event to include (inclusive).
+            oldest: Unix timestamp of the least recent audit event to include (inclusive).
+                Data is not available prior to March 2018.
+            limit: Number of results to optimistically return, maximum 9999.
+            action: Name of the action.
+            actor: User ID who initiated the action.
+            entity: ID of the target entity of the action (such as a channel, workspace, organization, file).
+            cursor: The next page cursor of pagination
+            additional_query_params: Add anything else if you need to use the ones this library does not support
+            headers: Additional request headers
+
+        Returns:
+            API response
+        """
+        query_params = {
+            "latest": latest,
+            "oldest": oldest,
+            "limit": limit,
+            "action": action,
+            "actor": actor,
+            "entity": entity,
+            "cursor": cursor,
+        }
+        if additional_query_params is not None:
+            query_params.update(additional_query_params)
+        query_params = {k: v for k, v in query_params.items() if v is not None}
+        return self.api_call(
+            path="logs",
+            query_params=query_params,
+            headers=headers,
+        )
+
+    def api_call(
+        self,
+        *,
+        http_verb: str = "GET",
+        path: str,
+        query_params: Optional[Dict[str, Any]] = None,
+        body_params: Optional[Dict[str, Any]] = None,
+        headers: Optional[Dict[str, str]] = None,
+    ) -> AuditLogsResponse:
+        """Performs a Slack API request and returns the result."""
+        url = f"{self.base_url}{path}"
+        query = _build_query(query_params)
+        if len(query) > 0:
+            url += f"?{query}"
+
+        return self._perform_http_request(
+            http_verb=http_verb,
+            url=url,
+            body=body_params,
+            headers=_build_request_headers(
+                token=self.token,
+                default_headers=self.default_headers,
+                additional_headers=headers,
+            ),
+        )
+
+    def _perform_http_request(
+        self,
+        *,
+        http_verb: str = "GET",
+        url: str,
+        body: Optional[Dict[str, Any]] = None,
+        headers: Dict[str, str],
+    ) -> AuditLogsResponse:
+        if body is not None:
+            body = json.dumps(body)  # type: ignore[assignment]
+        headers["Content-Type"] = "application/json;charset=utf-8"
+
+        if self.logger.level <= logging.DEBUG:
+            headers_for_logging = {k: "(redacted)" if k.lower() == "authorization" else v for k, v in headers.items()}
+            self.logger.debug(f"Sending a request - url: {url}, body: {body}, headers: {headers_for_logging}")
+
+        # NOTE: Intentionally ignore the `http_verb` here
+        # Slack APIs accepts any API method requests with POST methods
+        req = Request(
+            method=http_verb,
+            url=url,
+            data=body.encode("utf-8") if body is not None else None,  # type: ignore[attr-defined]
+            headers=headers,
+        )
+        resp = None
+        last_error = None
+
+        retry_state = RetryState()
+        counter_for_safety = 0
+        while counter_for_safety < 100:
+            counter_for_safety += 1
+            # If this is a retry, the next try started here. We can reset the flag.
+            retry_state.next_attempt_requested = False
+
+            try:
+                resp = self._perform_http_request_internal(url, req)
+                # The resp is a 200 OK response
+                return resp
+
+            except HTTPError as e:
+                # read the response body here
+                charset = e.headers.get_content_charset() or "utf-8"
+                response_body: str = e.read().decode(charset)
+                # As adding new values to HTTPError#headers can be ignored, building a new dict object here
+                response_headers = dict(e.headers.items())
+                resp = AuditLogsResponse(
+                    url=url,
+                    status_code=e.code,
+                    raw_body=response_body,
+                    headers=response_headers,
+                )
+                if e.code == 429:
+                    # for backward-compatibility with WebClient (v.2.5.0 or older)
+                    if "retry-after" not in resp.headers and "Retry-After" in resp.headers:
+                        resp.headers["retry-after"] = resp.headers["Retry-After"]
+                    if "Retry-After" not in resp.headers and "retry-after" in resp.headers:
+                        resp.headers["Retry-After"] = resp.headers["retry-after"]
+                _debug_log_response(self.logger, resp)
+
+                # Try to find a retry handler for this error
+                retry_request = RetryHttpRequest.from_urllib_http_request(req)
+                retry_response = RetryHttpResponse(
+                    status_code=e.code,
+                    headers={k: [v] for k, v in e.headers.items()},
+                    data=response_body.encode("utf-8") if response_body is not None else None,
+                )
+                for handler in self.retry_handlers:
+                    if handler.can_retry(
+                        state=retry_state,
+                        request=retry_request,
+                        response=retry_response,
+                        error=e,
+                    ):
+                        if self.logger.level <= logging.DEBUG:
+                            self.logger.info(
+                                f"A retry handler found: {type(handler).__name__} for {req.method} {req.full_url} - {e}"
+                            )
+                        handler.prepare_for_next_attempt(
+                            state=retry_state,
+                            request=retry_request,
+                            response=retry_response,
+                            error=e,
+                        )
+                        break
+
+                if retry_state.next_attempt_requested is False:
+                    return resp
+
+            except Exception as err:
+                last_error = err
+                self.logger.error(f"Failed to send a request to Slack API server: {err}")
+
+                # Try to find a retry handler for this error
+                retry_request = RetryHttpRequest.from_urllib_http_request(req)
+                for handler in self.retry_handlers:
+                    if handler.can_retry(
+                        state=retry_state,
+                        request=retry_request,
+                        response=None,
+                        error=err,
+                    ):
+                        if self.logger.level <= logging.DEBUG:
+                            self.logger.info(
+                                f"A retry handler found: {type(handler).__name__} for {req.method} {req.full_url} - {err}"
+                            )
+                        handler.prepare_for_next_attempt(
+                            state=retry_state,
+                            request=retry_request,
+                            response=None,
+                            error=err,
+                        )
+                        self.logger.info(f"Going to retry the same request: {req.method} {req.full_url}")
+                        break
+
+                if retry_state.next_attempt_requested is False:
+                    raise err
+
+        if resp is not None:
+            return resp
+        raise last_error  # type: ignore[misc]
+
+    def _perform_http_request_internal(self, url: str, req: Request) -> AuditLogsResponse:
+        opener: Optional[OpenerDirector] = None
+        # for security (BAN-B310)
+        if url.lower().startswith("http"):
+            if self.proxy is not None:
+                if isinstance(self.proxy, str):
+                    opener = urllib.request.build_opener(
+                        ProxyHandler({"http": self.proxy, "https": self.proxy}),
+                        HTTPSHandler(context=self.ssl),
+                    )
+                else:
+                    raise SlackRequestError(f"Invalid proxy detected: {self.proxy} must be a str value")
+        else:
+            raise SlackRequestError(f"Invalid URL detected: {url}")
+
+        http_resp: HTTPResponse
+        if opener:
+            http_resp = opener.open(req, timeout=self.timeout)
+        else:
+            http_resp = urlopen(req, context=self.ssl, timeout=self.timeout)
+        charset: str = http_resp.headers.get_content_charset() or "utf-8"
+        response_body: str = http_resp.read().decode(charset)
+        resp = AuditLogsResponse(
+            url=url,
+            status_code=http_resp.status,
+            raw_body=response_body,
+            headers=http_resp.headers,  # type: ignore[arg-type]
+        )
+        _debug_log_response(self.logger, resp)
+        return resp
+
+

API client for Audit Logs API +See https://docs.slack.dev/admins/audit-logs-api/ for more details

+

Args

+
+
token
+
An admin user's token, which starts with xoxp-
+
timeout
+
Request timeout (in seconds)
+
ssl
+
ssl.SSLContext to use for requests
+
proxy
+
Proxy URL (e.g., localhost:9000, http://localhost:9000)
+
base_url
+
The base URL for API calls
+
default_headers
+
Request headers to add to all requests
+
user_agent_prefix
+
Prefix for User-Agent header value
+
user_agent_suffix
+
Suffix for User-Agent header value
+
logger
+
Custom logger
+
retry_handlers
+
Retry handlers
+
+

Class variables

+
+
var BASE_URL
+
+

The type of the None singleton.

+
+
var base_url : str
+
+

The type of the None singleton.

+
+
var default_headers : Dict[str, str]
+
+

The type of the None singleton.

+
+
var logger : logging.Logger
+
+

The type of the None singleton.

+
+
var proxy : str | None
+
+

The type of the None singleton.

+
+
var retry_handlers : List[RetryHandler]
+
+

The type of the None singleton.

+
+
var ssl : ssl.SSLContext | None
+
+

The type of the None singleton.

+
+
var timeout : int
+
+

The type of the None singleton.

+
+
var token : str
+
+

The type of the None singleton.

+
+
+

Methods

+
+
+def actions(self,
*,
query_params: Dict[str, Any] | None = None,
headers: Dict[str, str] | None = None) ‑> AuditLogsResponse
+
+
+
+ +Expand source code + +
def actions(
+    self,
+    *,
+    query_params: Optional[Dict[str, Any]] = None,
+    headers: Optional[Dict[str, str]] = None,
+) -> AuditLogsResponse:
+    """Returns information about the kind of actions that the Audit Logs API
+    returns as a list of all actions and a short description of each.
+    Authentication not required.
+
+    Args:
+        query_params: Set any values if you want to add query params
+        headers: Additional request headers
+
+    Returns:
+        API response
+    """
+    return self.api_call(
+        path="actions",
+        query_params=query_params,
+        headers=headers,
+    )
+
+

Returns information about the kind of actions that the Audit Logs API +returns as a list of all actions and a short description of each. +Authentication not required.

+

Args

+
+
query_params
+
Set any values if you want to add query params
+
headers
+
Additional request headers
+
+

Returns

+

API response

+
+
+def api_call(self,
*,
http_verb: str = 'GET',
path: str,
query_params: Dict[str, Any] | None = None,
body_params: Dict[str, Any] | None = None,
headers: Dict[str, str] | None = None) ‑> AuditLogsResponse
+
+
+
+ +Expand source code + +
def api_call(
+    self,
+    *,
+    http_verb: str = "GET",
+    path: str,
+    query_params: Optional[Dict[str, Any]] = None,
+    body_params: Optional[Dict[str, Any]] = None,
+    headers: Optional[Dict[str, str]] = None,
+) -> AuditLogsResponse:
+    """Performs a Slack API request and returns the result."""
+    url = f"{self.base_url}{path}"
+    query = _build_query(query_params)
+    if len(query) > 0:
+        url += f"?{query}"
+
+    return self._perform_http_request(
+        http_verb=http_verb,
+        url=url,
+        body=body_params,
+        headers=_build_request_headers(
+            token=self.token,
+            default_headers=self.default_headers,
+            additional_headers=headers,
+        ),
+    )
+
+

Performs a Slack API request and returns the result.

+
+
+def logs(self,
*,
latest: int | None = None,
oldest: int | None = None,
limit: int | None = None,
action: str | None = None,
actor: str | None = None,
entity: str | None = None,
cursor: str | None = None,
additional_query_params: Dict[str, Any] | None = None,
headers: Dict[str, str] | None = None) ‑> AuditLogsResponse
+
+
+
+ +Expand source code + +
def logs(
+    self,
+    *,
+    latest: Optional[int] = None,
+    oldest: Optional[int] = None,
+    limit: Optional[int] = None,
+    action: Optional[str] = None,
+    actor: Optional[str] = None,
+    entity: Optional[str] = None,
+    cursor: Optional[str] = None,
+    additional_query_params: Optional[Dict[str, Any]] = None,
+    headers: Optional[Dict[str, str]] = None,
+) -> AuditLogsResponse:
+    """This is the primary endpoint for retrieving actual audit events from your organization.
+    It will return a list of actions that have occurred on the installed workspace or grid organization.
+    Authentication required.
+
+    The following filters can be applied in order to narrow the range of actions returned.
+    Filters are added as query string parameters and can be combined together.
+    Multiple filter parameters are additive (a boolean AND) and are separated
+    with an ampersand (&) in the query string. Filtering is entirely optional.
+
+    Args:
+        latest: Unix timestamp of the most recent audit event to include (inclusive).
+        oldest: Unix timestamp of the least recent audit event to include (inclusive).
+            Data is not available prior to March 2018.
+        limit: Number of results to optimistically return, maximum 9999.
+        action: Name of the action.
+        actor: User ID who initiated the action.
+        entity: ID of the target entity of the action (such as a channel, workspace, organization, file).
+        cursor: The next page cursor of pagination
+        additional_query_params: Add anything else if you need to use the ones this library does not support
+        headers: Additional request headers
+
+    Returns:
+        API response
+    """
+    query_params = {
+        "latest": latest,
+        "oldest": oldest,
+        "limit": limit,
+        "action": action,
+        "actor": actor,
+        "entity": entity,
+        "cursor": cursor,
+    }
+    if additional_query_params is not None:
+        query_params.update(additional_query_params)
+    query_params = {k: v for k, v in query_params.items() if v is not None}
+    return self.api_call(
+        path="logs",
+        query_params=query_params,
+        headers=headers,
+    )
+
+

This is the primary endpoint for retrieving actual audit events from your organization. +It will return a list of actions that have occurred on the installed workspace or grid organization. +Authentication required.

+

The following filters can be applied in order to narrow the range of actions returned. +Filters are added as query string parameters and can be combined together. +Multiple filter parameters are additive (a boolean AND) and are separated +with an ampersand (&) in the query string. Filtering is entirely optional.

+

Args

+
+
latest
+
Unix timestamp of the most recent audit event to include (inclusive).
+
oldest
+
Unix timestamp of the least recent audit event to include (inclusive). +Data is not available prior to March 2018.
+
limit
+
Number of results to optimistically return, maximum 9999.
+
action
+
Name of the action.
+
actor
+
User ID who initiated the action.
+
entity
+
ID of the target entity of the action (such as a channel, workspace, organization, file).
+
cursor
+
The next page cursor of pagination
+
additional_query_params
+
Add anything else if you need to use the ones this library does not support
+
headers
+
Additional request headers
+
+

Returns

+

API response

+
+
+def schemas(self,
*,
query_params: Dict[str, Any] | None = None,
headers: Dict[str, str] | None = None) ‑> AuditLogsResponse
+
+
+
+ +Expand source code + +
def schemas(
+    self,
+    *,
+    query_params: Optional[Dict[str, Any]] = None,
+    headers: Optional[Dict[str, str]] = None,
+) -> AuditLogsResponse:
+    """Returns information about the kind of objects which the Audit Logs API
+    returns as a list of all objects and a short description.
+    Authentication not required.
+
+    Args:
+        query_params: Set any values if you want to add query params
+        headers: Additional request headers
+    Returns:
+        API response
+    """
+    return self.api_call(
+        path="schemas",
+        query_params=query_params,
+        headers=headers,
+    )
+
+

Returns information about the kind of objects which the Audit Logs API +returns as a list of all objects and a short description. +Authentication not required.

+

Args

+
+
query_params
+
Set any values if you want to add query params
+
headers
+
Additional request headers
+
+

Returns

+

API response

+
+
+
+
+class AuditLogsResponse +(*, url: str, status_code: int, raw_body: str | None, headers: dict) +
+
+
+ +Expand source code + +
class AuditLogsResponse:
+    url: str
+    status_code: int
+    headers: Dict[str, Any]
+    raw_body: Optional[str]
+    body: Optional[Dict[str, Any]]
+    typed_body: Optional[LogsResponse]
+
+    @property  # type: ignore[no-redef]
+    def typed_body(self) -> Optional[LogsResponse]:
+        if self.body is None:
+            return None
+        return LogsResponse(**self.body)
+
+    def __init__(
+        self,
+        *,
+        url: str,
+        status_code: int,
+        raw_body: Optional[str],
+        headers: dict,
+    ):
+        self.url = url
+        self.status_code = status_code
+        self.headers = headers
+        self.raw_body = raw_body
+        self.body = json.loads(raw_body) if raw_body is not None and raw_body.startswith("{") else None
+
+
+

Class variables

+
+
var body : Dict[str, Any] | None
+
+

The type of the None singleton.

+
+
var headers : Dict[str, Any]
+
+

The type of the None singleton.

+
+
var raw_body : str | None
+
+

The type of the None singleton.

+
+
var status_code : int
+
+

The type of the None singleton.

+
+
var url : str
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
prop typed_bodyLogsResponse | None
+
+
+ +Expand source code + +
@property  # type: ignore[no-redef]
+def typed_body(self) -> Optional[LogsResponse]:
+    if self.body is None:
+        return None
+    return LogsResponse(**self.body)
+
+
+
+
+
+
+
+
+ +
+ + + diff --git a/docs/reference/audit_logs/v1/async_client.html b/docs/reference/audit_logs/v1/async_client.html new file mode 100644 index 000000000..e3f01fafc --- /dev/null +++ b/docs/reference/audit_logs/v1/async_client.html @@ -0,0 +1,738 @@ + + + + + + +slack_sdk.audit_logs.v1.async_client API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.audit_logs.v1.async_client

+
+
+

Audit Logs API is a set of APIs for monitoring what’s happening in your Enterprise Grid organization.

+

Refer to https://docs.slack.dev/tools/python-slack-sdk/audit-logs for details.

+
+
+
+
+
+
+
+
+

Classes

+
+
+class AsyncAuditLogsClient +(token: str,
timeout: int = 30,
ssl: ssl.SSLContext | None = None,
proxy: str | None = None,
base_url: str = 'https://api.slack.com/audit/v1/',
session: aiohttp.client.ClientSession | None = None,
trust_env_in_session: bool = False,
auth: aiohttp.helpers.BasicAuth | None = None,
default_headers: Dict[str, str] | None = None,
user_agent_prefix: str | None = None,
user_agent_suffix: str | None = None,
logger: logging.Logger | None = None,
retry_handlers: List[slack_sdk.http_retry.async_handler.AsyncRetryHandler] | None = None)
+
+
+
+ +Expand source code + +
class AsyncAuditLogsClient:
+    BASE_URL = "https://api.slack.com/audit/v1/"
+
+    token: str
+    timeout: int
+    ssl: Optional[SSLContext]
+    proxy: Optional[str]
+    base_url: str
+    session: Optional[ClientSession]
+    trust_env_in_session: bool
+    auth: Optional[BasicAuth]
+    default_headers: Dict[str, str]
+    logger: logging.Logger
+    retry_handlers: List[AsyncRetryHandler]
+
+    def __init__(
+        self,
+        token: str,
+        timeout: int = 30,
+        ssl: Optional[SSLContext] = None,
+        proxy: Optional[str] = None,
+        base_url: str = BASE_URL,
+        session: Optional[ClientSession] = None,
+        trust_env_in_session: bool = False,
+        auth: Optional[BasicAuth] = None,
+        default_headers: Optional[Dict[str, str]] = None,
+        user_agent_prefix: Optional[str] = None,
+        user_agent_suffix: Optional[str] = None,
+        logger: Optional[logging.Logger] = None,
+        retry_handlers: Optional[List[AsyncRetryHandler]] = None,
+    ):
+        """API client for Audit Logs API
+        See https://docs.slack.dev/admins/audit-logs-api/ for more details
+
+        Args:
+            token: An admin user's token, which starts with `xoxp-`
+            timeout: Request timeout (in seconds)
+            ssl: `ssl.SSLContext` to use for requests
+            proxy: Proxy URL (e.g., `localhost:9000`, `http://localhost:9000`)
+            base_url: The base URL for API calls
+            session: `aiohttp.ClientSession` instance
+            trust_env_in_session: True/False for `aiohttp.ClientSession`
+            auth: Basic auth info for `aiohttp.ClientSession`
+            default_headers: Request headers to add to all requests
+            user_agent_prefix: Prefix for User-Agent header value
+            user_agent_suffix: Suffix for User-Agent header value
+            logger: Custom logger
+            retry_handlers: Retry handlers
+        """
+        self.token = token
+        self.timeout = timeout
+        self.ssl = ssl
+        self.proxy = proxy
+        self.base_url = base_url
+        self.session = session
+        self.trust_env_in_session = trust_env_in_session
+        self.auth = auth
+        self.default_headers = default_headers if default_headers else {}
+        self.default_headers["User-Agent"] = get_user_agent(user_agent_prefix, user_agent_suffix)
+        self.logger = logger if logger is not None else logging.getLogger(__name__)
+        self.retry_handlers = retry_handlers if retry_handlers is not None else async_default_handlers()
+
+        if self.proxy is None or len(self.proxy.strip()) == 0:
+            env_variable = load_http_proxy_from_env(self.logger)
+            if env_variable is not None:
+                self.proxy = env_variable
+
+    async def schemas(
+        self,
+        *,
+        query_params: Optional[Dict[str, Any]] = None,
+        headers: Optional[Dict[str, str]] = None,
+    ) -> AuditLogsResponse:
+        """Returns information about the kind of objects which the Audit Logs API
+        returns as a list of all objects and a short description.
+        Authentication not required.
+
+        Args:
+            query_params: Set any values if you want to add query params
+            headers: Additional request headers
+        Returns:
+            API response
+        """
+        return await self.api_call(
+            path="schemas",
+            query_params=query_params,
+            headers=headers,
+        )
+
+    async def actions(
+        self,
+        *,
+        query_params: Optional[Dict[str, Any]] = None,
+        headers: Optional[Dict[str, str]] = None,
+    ) -> AuditLogsResponse:
+        """Returns information about the kind of actions that the Audit Logs API
+        returns as a list of all actions and a short description of each.
+        Authentication not required.
+
+        Args:
+            query_params: Set any values if you want to add query params
+            headers: Additional request headers
+
+        Returns:
+            API response
+        """
+        return await self.api_call(
+            path="actions",
+            query_params=query_params,
+            headers=headers,
+        )
+
+    async def logs(
+        self,
+        *,
+        latest: Optional[int] = None,
+        oldest: Optional[int] = None,
+        limit: Optional[int] = None,
+        action: Optional[str] = None,
+        actor: Optional[str] = None,
+        entity: Optional[str] = None,
+        cursor: Optional[str] = None,
+        additional_query_params: Optional[Dict[str, Any]] = None,
+        headers: Optional[Dict[str, str]] = None,
+    ) -> AuditLogsResponse:
+        """This is the primary endpoint for retrieving actual audit events from your organization.
+        It will return a list of actions that have occurred on the installed workspace or grid organization.
+        Authentication required.
+
+        The following filters can be applied in order to narrow the range of actions returned.
+        Filters are added as query string parameters and can be combined together.
+        Multiple filter parameters are additive (a boolean AND) and are separated
+        with an ampersand (&) in the query string. Filtering is entirely optional.
+
+        Args:
+            latest: Unix timestamp of the most recent audit event to include (inclusive).
+            oldest: Unix timestamp of the least recent audit event to include (inclusive).
+                Data is not available prior to March 2018.
+            limit: Number of results to optimistically return, maximum 9999.
+            action: Name of the action.
+            actor: User ID who initiated the action.
+            entity: ID of the target entity of the action (such as a channel, workspace, organization, file).
+            cursor: The next page cursor of pagination
+            additional_query_params: Add anything else if you need to use the ones this library does not support
+            headers: Additional request headers
+
+        Returns:
+            API response
+        """
+        query_params = {
+            "latest": latest,
+            "oldest": oldest,
+            "limit": limit,
+            "action": action,
+            "actor": actor,
+            "entity": entity,
+            "cursor": cursor,
+        }
+        if additional_query_params is not None:
+            query_params.update(additional_query_params)
+        query_params = {k: v for k, v in query_params.items() if v is not None}
+        return await self.api_call(
+            path="logs",
+            query_params=query_params,
+            headers=headers,
+        )
+
+    async def api_call(
+        self,
+        *,
+        http_verb: str = "GET",
+        path: str,
+        query_params: Optional[Dict[str, Any]] = None,
+        body_params: Optional[Dict[str, Any]] = None,
+        headers: Optional[Dict[str, str]] = None,
+    ) -> AuditLogsResponse:
+        url = f"{self.base_url}{path}"
+        return await self._perform_http_request(
+            http_verb=http_verb,
+            url=url,
+            query_params=query_params,
+            body_params=body_params,
+            headers=_build_request_headers(
+                token=self.token,
+                default_headers=self.default_headers,
+                additional_headers=headers,
+            ),
+        )
+
+    async def _perform_http_request(
+        self,
+        *,
+        http_verb: str,
+        url: str,
+        query_params: Optional[Dict[str, Any]],
+        body_params: Optional[Dict[str, Any]],
+        headers: Dict[str, str],
+    ) -> AuditLogsResponse:
+        if body_params is not None:
+            body_params = json.dumps(body_params)  # type: ignore[assignment]
+        headers["Content-Type"] = "application/json;charset=utf-8"
+
+        session: Optional[ClientSession] = None
+        use_running_session = self.session and not self.session.closed
+        if use_running_session:
+            session = self.session
+        else:
+            session = aiohttp.ClientSession(
+                timeout=aiohttp.ClientTimeout(total=self.timeout),
+                auth=self.auth,
+                trust_env=self.trust_env_in_session,
+            )
+
+        last_error = None
+        resp: Optional[AuditLogsResponse] = None
+        try:
+            request_kwargs = {
+                "headers": headers,
+                "params": query_params,
+                "data": body_params,
+                "ssl": self.ssl,
+                "proxy": self.proxy,
+            }
+            retry_request = RetryHttpRequest(
+                method=http_verb,
+                url=url,
+                headers=headers,  # type: ignore[arg-type]
+                body_params=body_params,
+            )
+
+            retry_state = RetryState()
+            counter_for_safety = 0
+            while counter_for_safety < 100:
+                counter_for_safety += 1
+                # If this is a retry, the next try started here. We can reset the flag.
+                retry_state.next_attempt_requested = False
+                retry_response: Optional[RetryHttpResponse] = None
+                response_body = ""
+
+                if self.logger.level <= logging.DEBUG:
+                    headers_for_logging = {
+                        k: "(redacted)" if k.lower() == "authorization" else v for k, v in headers.items()
+                    }
+                    self.logger.debug(
+                        f"Sending a request - "
+                        f"url: {url}, "
+                        f"params: {query_params}, "
+                        f"body: {body_params}, "
+                        f"headers: {headers_for_logging}"
+                    )
+
+                try:
+                    async with session.request(http_verb, url, **request_kwargs) as res:  # type: ignore[arg-type, union-attr] # noqa: E501
+                        try:
+                            response_body = await res.text()
+                            retry_response = RetryHttpResponse(
+                                status_code=res.status,
+                                headers=res.headers,  # type: ignore[arg-type]
+                                data=response_body.encode("utf-8") if response_body is not None else None,
+                            )
+                        except aiohttp.ContentTypeError:
+                            self.logger.debug(f"No response data returned from the following API call: {url}.")
+                            retry_response = RetryHttpResponse(
+                                status_code=res.status,
+                                headers=res.headers,  # type: ignore[arg-type]
+                            )
+                        except json.decoder.JSONDecodeError as e:
+                            message = f"Failed to parse the response body: {str(e)}"
+                            raise SlackApiError(message, res)
+
+                        if res.status == 429:
+                            for handler in self.retry_handlers:
+                                if await handler.can_retry_async(
+                                    state=retry_state,
+                                    request=retry_request,
+                                    response=retry_response,
+                                ):
+                                    if self.logger.level <= logging.DEBUG:
+                                        self.logger.info(
+                                            f"A retry handler found: {type(handler).__name__} "
+                                            f"for {http_verb} {url} - rate_limited"
+                                        )
+                                    await handler.prepare_for_next_attempt_async(
+                                        state=retry_state,
+                                        request=retry_request,
+                                        response=retry_response,
+                                    )
+                                    break
+
+                        if retry_state.next_attempt_requested is False:
+                            resp = AuditLogsResponse(
+                                url=url,
+                                status_code=res.status,
+                                raw_body=response_body,
+                                headers=res.headers,  # type: ignore[arg-type]
+                            )
+                            _debug_log_response(self.logger, resp)
+                            return resp
+
+                except Exception as e:
+                    last_error = e
+                    for handler in self.retry_handlers:
+                        if await handler.can_retry_async(
+                            state=retry_state,
+                            request=retry_request,
+                            response=retry_response,
+                            error=e,
+                        ):
+                            if self.logger.level <= logging.DEBUG:
+                                self.logger.info(
+                                    f"A retry handler found: {type(handler).__name__} " f"for {http_verb} {url} - {e}"
+                                )
+                            await handler.prepare_for_next_attempt_async(
+                                state=retry_state,
+                                request=retry_request,
+                                response=retry_response,
+                                error=e,
+                            )
+                            break
+
+                    if retry_state.next_attempt_requested is False:
+                        raise last_error
+
+            if resp is not None:
+                return resp
+            raise last_error  # type: ignore[misc]
+
+        finally:
+            if not use_running_session:
+                await session.close()  # type: ignore[union-attr]
+
+        return resp
+
+

API client for Audit Logs API +See https://docs.slack.dev/admins/audit-logs-api/ for more details

+

Args

+
+
token
+
An admin user's token, which starts with xoxp-
+
timeout
+
Request timeout (in seconds)
+
ssl
+
ssl.SSLContext to use for requests
+
proxy
+
Proxy URL (e.g., localhost:9000, http://localhost:9000)
+
base_url
+
The base URL for API calls
+
session
+
aiohttp.ClientSession instance
+
trust_env_in_session
+
True/False for aiohttp.ClientSession
+
auth
+
Basic auth info for aiohttp.ClientSession
+
default_headers
+
Request headers to add to all requests
+
user_agent_prefix
+
Prefix for User-Agent header value
+
user_agent_suffix
+
Suffix for User-Agent header value
+
logger
+
Custom logger
+
retry_handlers
+
Retry handlers
+
+

Class variables

+
+
var BASE_URL
+
+

The type of the None singleton.

+
+
var auth : aiohttp.helpers.BasicAuth | None
+
+

The type of the None singleton.

+
+
var base_url : str
+
+

The type of the None singleton.

+
+
var default_headers : Dict[str, str]
+
+

The type of the None singleton.

+
+
var logger : logging.Logger
+
+

The type of the None singleton.

+
+
var proxy : str | None
+
+

The type of the None singleton.

+
+
var retry_handlers : List[slack_sdk.http_retry.async_handler.AsyncRetryHandler]
+
+

The type of the None singleton.

+
+
var session : aiohttp.client.ClientSession | None
+
+

The type of the None singleton.

+
+
var ssl : ssl.SSLContext | None
+
+

The type of the None singleton.

+
+
var timeout : int
+
+

The type of the None singleton.

+
+
var token : str
+
+

The type of the None singleton.

+
+
var trust_env_in_session : bool
+
+

The type of the None singleton.

+
+
+

Methods

+
+
+async def actions(self,
*,
query_params: Dict[str, Any] | None = None,
headers: Dict[str, str] | None = None) ‑> AuditLogsResponse
+
+
+
+ +Expand source code + +
async def actions(
+    self,
+    *,
+    query_params: Optional[Dict[str, Any]] = None,
+    headers: Optional[Dict[str, str]] = None,
+) -> AuditLogsResponse:
+    """Returns information about the kind of actions that the Audit Logs API
+    returns as a list of all actions and a short description of each.
+    Authentication not required.
+
+    Args:
+        query_params: Set any values if you want to add query params
+        headers: Additional request headers
+
+    Returns:
+        API response
+    """
+    return await self.api_call(
+        path="actions",
+        query_params=query_params,
+        headers=headers,
+    )
+
+

Returns information about the kind of actions that the Audit Logs API +returns as a list of all actions and a short description of each. +Authentication not required.

+

Args

+
+
query_params
+
Set any values if you want to add query params
+
headers
+
Additional request headers
+
+

Returns

+

API response

+
+
+async def api_call(self,
*,
http_verb: str = 'GET',
path: str,
query_params: Dict[str, Any] | None = None,
body_params: Dict[str, Any] | None = None,
headers: Dict[str, str] | None = None) ‑> AuditLogsResponse
+
+
+
+ +Expand source code + +
async def api_call(
+    self,
+    *,
+    http_verb: str = "GET",
+    path: str,
+    query_params: Optional[Dict[str, Any]] = None,
+    body_params: Optional[Dict[str, Any]] = None,
+    headers: Optional[Dict[str, str]] = None,
+) -> AuditLogsResponse:
+    url = f"{self.base_url}{path}"
+    return await self._perform_http_request(
+        http_verb=http_verb,
+        url=url,
+        query_params=query_params,
+        body_params=body_params,
+        headers=_build_request_headers(
+            token=self.token,
+            default_headers=self.default_headers,
+            additional_headers=headers,
+        ),
+    )
+
+
+
+
+async def logs(self,
*,
latest: int | None = None,
oldest: int | None = None,
limit: int | None = None,
action: str | None = None,
actor: str | None = None,
entity: str | None = None,
cursor: str | None = None,
additional_query_params: Dict[str, Any] | None = None,
headers: Dict[str, str] | None = None) ‑> AuditLogsResponse
+
+
+
+ +Expand source code + +
async def logs(
+    self,
+    *,
+    latest: Optional[int] = None,
+    oldest: Optional[int] = None,
+    limit: Optional[int] = None,
+    action: Optional[str] = None,
+    actor: Optional[str] = None,
+    entity: Optional[str] = None,
+    cursor: Optional[str] = None,
+    additional_query_params: Optional[Dict[str, Any]] = None,
+    headers: Optional[Dict[str, str]] = None,
+) -> AuditLogsResponse:
+    """This is the primary endpoint for retrieving actual audit events from your organization.
+    It will return a list of actions that have occurred on the installed workspace or grid organization.
+    Authentication required.
+
+    The following filters can be applied in order to narrow the range of actions returned.
+    Filters are added as query string parameters and can be combined together.
+    Multiple filter parameters are additive (a boolean AND) and are separated
+    with an ampersand (&) in the query string. Filtering is entirely optional.
+
+    Args:
+        latest: Unix timestamp of the most recent audit event to include (inclusive).
+        oldest: Unix timestamp of the least recent audit event to include (inclusive).
+            Data is not available prior to March 2018.
+        limit: Number of results to optimistically return, maximum 9999.
+        action: Name of the action.
+        actor: User ID who initiated the action.
+        entity: ID of the target entity of the action (such as a channel, workspace, organization, file).
+        cursor: The next page cursor of pagination
+        additional_query_params: Add anything else if you need to use the ones this library does not support
+        headers: Additional request headers
+
+    Returns:
+        API response
+    """
+    query_params = {
+        "latest": latest,
+        "oldest": oldest,
+        "limit": limit,
+        "action": action,
+        "actor": actor,
+        "entity": entity,
+        "cursor": cursor,
+    }
+    if additional_query_params is not None:
+        query_params.update(additional_query_params)
+    query_params = {k: v for k, v in query_params.items() if v is not None}
+    return await self.api_call(
+        path="logs",
+        query_params=query_params,
+        headers=headers,
+    )
+
+

This is the primary endpoint for retrieving actual audit events from your organization. +It will return a list of actions that have occurred on the installed workspace or grid organization. +Authentication required.

+

The following filters can be applied in order to narrow the range of actions returned. +Filters are added as query string parameters and can be combined together. +Multiple filter parameters are additive (a boolean AND) and are separated +with an ampersand (&) in the query string. Filtering is entirely optional.

+

Args

+
+
latest
+
Unix timestamp of the most recent audit event to include (inclusive).
+
oldest
+
Unix timestamp of the least recent audit event to include (inclusive). +Data is not available prior to March 2018.
+
limit
+
Number of results to optimistically return, maximum 9999.
+
action
+
Name of the action.
+
actor
+
User ID who initiated the action.
+
entity
+
ID of the target entity of the action (such as a channel, workspace, organization, file).
+
cursor
+
The next page cursor of pagination
+
additional_query_params
+
Add anything else if you need to use the ones this library does not support
+
headers
+
Additional request headers
+
+

Returns

+

API response

+
+
+async def schemas(self,
*,
query_params: Dict[str, Any] | None = None,
headers: Dict[str, str] | None = None) ‑> AuditLogsResponse
+
+
+
+ +Expand source code + +
async def schemas(
+    self,
+    *,
+    query_params: Optional[Dict[str, Any]] = None,
+    headers: Optional[Dict[str, str]] = None,
+) -> AuditLogsResponse:
+    """Returns information about the kind of objects which the Audit Logs API
+    returns as a list of all objects and a short description.
+    Authentication not required.
+
+    Args:
+        query_params: Set any values if you want to add query params
+        headers: Additional request headers
+    Returns:
+        API response
+    """
+    return await self.api_call(
+        path="schemas",
+        query_params=query_params,
+        headers=headers,
+    )
+
+

Returns information about the kind of objects which the Audit Logs API +returns as a list of all objects and a short description. +Authentication not required.

+

Args

+
+
query_params
+
Set any values if you want to add query params
+
headers
+
Additional request headers
+
+

Returns

+

API response

+
+
+
+
+
+
+ +
+ + + diff --git a/docs/reference/audit_logs/v1/client.html b/docs/reference/audit_logs/v1/client.html new file mode 100644 index 000000000..fb3939178 --- /dev/null +++ b/docs/reference/audit_logs/v1/client.html @@ -0,0 +1,721 @@ + + + + + + +slack_sdk.audit_logs.v1.client API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.audit_logs.v1.client

+
+
+

Audit Logs API is a set of APIs for monitoring what’s happening in your Enterprise Grid organization.

+

Refer to https://docs.slack.dev/tools/python-slack-sdk/audit-logs for details.

+
+
+
+
+
+
+
+
+

Classes

+
+
+class AuditLogsClient +(token: str,
timeout: int = 30,
ssl: ssl.SSLContext | None = None,
proxy: str | None = None,
base_url: str = 'https://api.slack.com/audit/v1/',
default_headers: Dict[str, str] | None = None,
user_agent_prefix: str | None = None,
user_agent_suffix: str | None = None,
logger: logging.Logger | None = None,
retry_handlers: List[RetryHandler] | None = None)
+
+
+
+ +Expand source code + +
class AuditLogsClient:
+    BASE_URL = "https://api.slack.com/audit/v1/"
+
+    token: str
+    timeout: int
+    ssl: Optional[SSLContext]
+    proxy: Optional[str]
+    base_url: str
+    default_headers: Dict[str, str]
+    logger: logging.Logger
+    retry_handlers: List[RetryHandler]
+
+    def __init__(
+        self,
+        token: str,
+        timeout: int = 30,
+        ssl: Optional[SSLContext] = None,
+        proxy: Optional[str] = None,
+        base_url: str = BASE_URL,
+        default_headers: Optional[Dict[str, str]] = None,
+        user_agent_prefix: Optional[str] = None,
+        user_agent_suffix: Optional[str] = None,
+        logger: Optional[logging.Logger] = None,
+        retry_handlers: Optional[List[RetryHandler]] = None,
+    ):
+        """API client for Audit Logs API
+        See https://docs.slack.dev/admins/audit-logs-api/ for more details
+
+        Args:
+            token: An admin user's token, which starts with `xoxp-`
+            timeout: Request timeout (in seconds)
+            ssl: `ssl.SSLContext` to use for requests
+            proxy: Proxy URL (e.g., `localhost:9000`, `http://localhost:9000`)
+            base_url: The base URL for API calls
+            default_headers: Request headers to add to all requests
+            user_agent_prefix: Prefix for User-Agent header value
+            user_agent_suffix: Suffix for User-Agent header value
+            logger: Custom logger
+            retry_handlers: Retry handlers
+        """
+        self.token = token
+        self.timeout = timeout
+        self.ssl = ssl
+        self.proxy = proxy
+        self.base_url = base_url
+        self.default_headers = default_headers if default_headers else {}
+        self.default_headers["User-Agent"] = get_user_agent(user_agent_prefix, user_agent_suffix)
+        self.logger = logger if logger is not None else logging.getLogger(__name__)
+        self.retry_handlers = retry_handlers if retry_handlers is not None else default_retry_handlers()
+
+        if self.proxy is None or len(self.proxy.strip()) == 0:
+            env_variable = load_http_proxy_from_env(self.logger)
+            if env_variable is not None:
+                self.proxy = env_variable
+
+    def schemas(
+        self,
+        *,
+        query_params: Optional[Dict[str, Any]] = None,
+        headers: Optional[Dict[str, str]] = None,
+    ) -> AuditLogsResponse:
+        """Returns information about the kind of objects which the Audit Logs API
+        returns as a list of all objects and a short description.
+        Authentication not required.
+
+        Args:
+            query_params: Set any values if you want to add query params
+            headers: Additional request headers
+        Returns:
+            API response
+        """
+        return self.api_call(
+            path="schemas",
+            query_params=query_params,
+            headers=headers,
+        )
+
+    def actions(
+        self,
+        *,
+        query_params: Optional[Dict[str, Any]] = None,
+        headers: Optional[Dict[str, str]] = None,
+    ) -> AuditLogsResponse:
+        """Returns information about the kind of actions that the Audit Logs API
+        returns as a list of all actions and a short description of each.
+        Authentication not required.
+
+        Args:
+            query_params: Set any values if you want to add query params
+            headers: Additional request headers
+
+        Returns:
+            API response
+        """
+        return self.api_call(
+            path="actions",
+            query_params=query_params,
+            headers=headers,
+        )
+
+    def logs(
+        self,
+        *,
+        latest: Optional[int] = None,
+        oldest: Optional[int] = None,
+        limit: Optional[int] = None,
+        action: Optional[str] = None,
+        actor: Optional[str] = None,
+        entity: Optional[str] = None,
+        cursor: Optional[str] = None,
+        additional_query_params: Optional[Dict[str, Any]] = None,
+        headers: Optional[Dict[str, str]] = None,
+    ) -> AuditLogsResponse:
+        """This is the primary endpoint for retrieving actual audit events from your organization.
+        It will return a list of actions that have occurred on the installed workspace or grid organization.
+        Authentication required.
+
+        The following filters can be applied in order to narrow the range of actions returned.
+        Filters are added as query string parameters and can be combined together.
+        Multiple filter parameters are additive (a boolean AND) and are separated
+        with an ampersand (&) in the query string. Filtering is entirely optional.
+
+        Args:
+            latest: Unix timestamp of the most recent audit event to include (inclusive).
+            oldest: Unix timestamp of the least recent audit event to include (inclusive).
+                Data is not available prior to March 2018.
+            limit: Number of results to optimistically return, maximum 9999.
+            action: Name of the action.
+            actor: User ID who initiated the action.
+            entity: ID of the target entity of the action (such as a channel, workspace, organization, file).
+            cursor: The next page cursor of pagination
+            additional_query_params: Add anything else if you need to use the ones this library does not support
+            headers: Additional request headers
+
+        Returns:
+            API response
+        """
+        query_params = {
+            "latest": latest,
+            "oldest": oldest,
+            "limit": limit,
+            "action": action,
+            "actor": actor,
+            "entity": entity,
+            "cursor": cursor,
+        }
+        if additional_query_params is not None:
+            query_params.update(additional_query_params)
+        query_params = {k: v for k, v in query_params.items() if v is not None}
+        return self.api_call(
+            path="logs",
+            query_params=query_params,
+            headers=headers,
+        )
+
+    def api_call(
+        self,
+        *,
+        http_verb: str = "GET",
+        path: str,
+        query_params: Optional[Dict[str, Any]] = None,
+        body_params: Optional[Dict[str, Any]] = None,
+        headers: Optional[Dict[str, str]] = None,
+    ) -> AuditLogsResponse:
+        """Performs a Slack API request and returns the result."""
+        url = f"{self.base_url}{path}"
+        query = _build_query(query_params)
+        if len(query) > 0:
+            url += f"?{query}"
+
+        return self._perform_http_request(
+            http_verb=http_verb,
+            url=url,
+            body=body_params,
+            headers=_build_request_headers(
+                token=self.token,
+                default_headers=self.default_headers,
+                additional_headers=headers,
+            ),
+        )
+
+    def _perform_http_request(
+        self,
+        *,
+        http_verb: str = "GET",
+        url: str,
+        body: Optional[Dict[str, Any]] = None,
+        headers: Dict[str, str],
+    ) -> AuditLogsResponse:
+        if body is not None:
+            body = json.dumps(body)  # type: ignore[assignment]
+        headers["Content-Type"] = "application/json;charset=utf-8"
+
+        if self.logger.level <= logging.DEBUG:
+            headers_for_logging = {k: "(redacted)" if k.lower() == "authorization" else v for k, v in headers.items()}
+            self.logger.debug(f"Sending a request - url: {url}, body: {body}, headers: {headers_for_logging}")
+
+        # NOTE: Intentionally ignore the `http_verb` here
+        # Slack APIs accepts any API method requests with POST methods
+        req = Request(
+            method=http_verb,
+            url=url,
+            data=body.encode("utf-8") if body is not None else None,  # type: ignore[attr-defined]
+            headers=headers,
+        )
+        resp = None
+        last_error = None
+
+        retry_state = RetryState()
+        counter_for_safety = 0
+        while counter_for_safety < 100:
+            counter_for_safety += 1
+            # If this is a retry, the next try started here. We can reset the flag.
+            retry_state.next_attempt_requested = False
+
+            try:
+                resp = self._perform_http_request_internal(url, req)
+                # The resp is a 200 OK response
+                return resp
+
+            except HTTPError as e:
+                # read the response body here
+                charset = e.headers.get_content_charset() or "utf-8"
+                response_body: str = e.read().decode(charset)
+                # As adding new values to HTTPError#headers can be ignored, building a new dict object here
+                response_headers = dict(e.headers.items())
+                resp = AuditLogsResponse(
+                    url=url,
+                    status_code=e.code,
+                    raw_body=response_body,
+                    headers=response_headers,
+                )
+                if e.code == 429:
+                    # for backward-compatibility with WebClient (v.2.5.0 or older)
+                    if "retry-after" not in resp.headers and "Retry-After" in resp.headers:
+                        resp.headers["retry-after"] = resp.headers["Retry-After"]
+                    if "Retry-After" not in resp.headers and "retry-after" in resp.headers:
+                        resp.headers["Retry-After"] = resp.headers["retry-after"]
+                _debug_log_response(self.logger, resp)
+
+                # Try to find a retry handler for this error
+                retry_request = RetryHttpRequest.from_urllib_http_request(req)
+                retry_response = RetryHttpResponse(
+                    status_code=e.code,
+                    headers={k: [v] for k, v in e.headers.items()},
+                    data=response_body.encode("utf-8") if response_body is not None else None,
+                )
+                for handler in self.retry_handlers:
+                    if handler.can_retry(
+                        state=retry_state,
+                        request=retry_request,
+                        response=retry_response,
+                        error=e,
+                    ):
+                        if self.logger.level <= logging.DEBUG:
+                            self.logger.info(
+                                f"A retry handler found: {type(handler).__name__} for {req.method} {req.full_url} - {e}"
+                            )
+                        handler.prepare_for_next_attempt(
+                            state=retry_state,
+                            request=retry_request,
+                            response=retry_response,
+                            error=e,
+                        )
+                        break
+
+                if retry_state.next_attempt_requested is False:
+                    return resp
+
+            except Exception as err:
+                last_error = err
+                self.logger.error(f"Failed to send a request to Slack API server: {err}")
+
+                # Try to find a retry handler for this error
+                retry_request = RetryHttpRequest.from_urllib_http_request(req)
+                for handler in self.retry_handlers:
+                    if handler.can_retry(
+                        state=retry_state,
+                        request=retry_request,
+                        response=None,
+                        error=err,
+                    ):
+                        if self.logger.level <= logging.DEBUG:
+                            self.logger.info(
+                                f"A retry handler found: {type(handler).__name__} for {req.method} {req.full_url} - {err}"
+                            )
+                        handler.prepare_for_next_attempt(
+                            state=retry_state,
+                            request=retry_request,
+                            response=None,
+                            error=err,
+                        )
+                        self.logger.info(f"Going to retry the same request: {req.method} {req.full_url}")
+                        break
+
+                if retry_state.next_attempt_requested is False:
+                    raise err
+
+        if resp is not None:
+            return resp
+        raise last_error  # type: ignore[misc]
+
+    def _perform_http_request_internal(self, url: str, req: Request) -> AuditLogsResponse:
+        opener: Optional[OpenerDirector] = None
+        # for security (BAN-B310)
+        if url.lower().startswith("http"):
+            if self.proxy is not None:
+                if isinstance(self.proxy, str):
+                    opener = urllib.request.build_opener(
+                        ProxyHandler({"http": self.proxy, "https": self.proxy}),
+                        HTTPSHandler(context=self.ssl),
+                    )
+                else:
+                    raise SlackRequestError(f"Invalid proxy detected: {self.proxy} must be a str value")
+        else:
+            raise SlackRequestError(f"Invalid URL detected: {url}")
+
+        http_resp: HTTPResponse
+        if opener:
+            http_resp = opener.open(req, timeout=self.timeout)
+        else:
+            http_resp = urlopen(req, context=self.ssl, timeout=self.timeout)
+        charset: str = http_resp.headers.get_content_charset() or "utf-8"
+        response_body: str = http_resp.read().decode(charset)
+        resp = AuditLogsResponse(
+            url=url,
+            status_code=http_resp.status,
+            raw_body=response_body,
+            headers=http_resp.headers,  # type: ignore[arg-type]
+        )
+        _debug_log_response(self.logger, resp)
+        return resp
+
+

API client for Audit Logs API +See https://docs.slack.dev/admins/audit-logs-api/ for more details

+

Args

+
+
token
+
An admin user's token, which starts with xoxp-
+
timeout
+
Request timeout (in seconds)
+
ssl
+
ssl.SSLContext to use for requests
+
proxy
+
Proxy URL (e.g., localhost:9000, http://localhost:9000)
+
base_url
+
The base URL for API calls
+
default_headers
+
Request headers to add to all requests
+
user_agent_prefix
+
Prefix for User-Agent header value
+
user_agent_suffix
+
Suffix for User-Agent header value
+
logger
+
Custom logger
+
retry_handlers
+
Retry handlers
+
+

Class variables

+
+
var BASE_URL
+
+

The type of the None singleton.

+
+
var base_url : str
+
+

The type of the None singleton.

+
+
var default_headers : Dict[str, str]
+
+

The type of the None singleton.

+
+
var logger : logging.Logger
+
+

The type of the None singleton.

+
+
var proxy : str | None
+
+

The type of the None singleton.

+
+
var retry_handlers : List[RetryHandler]
+
+

The type of the None singleton.

+
+
var ssl : ssl.SSLContext | None
+
+

The type of the None singleton.

+
+
var timeout : int
+
+

The type of the None singleton.

+
+
var token : str
+
+

The type of the None singleton.

+
+
+

Methods

+
+
+def actions(self,
*,
query_params: Dict[str, Any] | None = None,
headers: Dict[str, str] | None = None) ‑> AuditLogsResponse
+
+
+
+ +Expand source code + +
def actions(
+    self,
+    *,
+    query_params: Optional[Dict[str, Any]] = None,
+    headers: Optional[Dict[str, str]] = None,
+) -> AuditLogsResponse:
+    """Returns information about the kind of actions that the Audit Logs API
+    returns as a list of all actions and a short description of each.
+    Authentication not required.
+
+    Args:
+        query_params: Set any values if you want to add query params
+        headers: Additional request headers
+
+    Returns:
+        API response
+    """
+    return self.api_call(
+        path="actions",
+        query_params=query_params,
+        headers=headers,
+    )
+
+

Returns information about the kind of actions that the Audit Logs API +returns as a list of all actions and a short description of each. +Authentication not required.

+

Args

+
+
query_params
+
Set any values if you want to add query params
+
headers
+
Additional request headers
+
+

Returns

+

API response

+
+
+def api_call(self,
*,
http_verb: str = 'GET',
path: str,
query_params: Dict[str, Any] | None = None,
body_params: Dict[str, Any] | None = None,
headers: Dict[str, str] | None = None) ‑> AuditLogsResponse
+
+
+
+ +Expand source code + +
def api_call(
+    self,
+    *,
+    http_verb: str = "GET",
+    path: str,
+    query_params: Optional[Dict[str, Any]] = None,
+    body_params: Optional[Dict[str, Any]] = None,
+    headers: Optional[Dict[str, str]] = None,
+) -> AuditLogsResponse:
+    """Performs a Slack API request and returns the result."""
+    url = f"{self.base_url}{path}"
+    query = _build_query(query_params)
+    if len(query) > 0:
+        url += f"?{query}"
+
+    return self._perform_http_request(
+        http_verb=http_verb,
+        url=url,
+        body=body_params,
+        headers=_build_request_headers(
+            token=self.token,
+            default_headers=self.default_headers,
+            additional_headers=headers,
+        ),
+    )
+
+

Performs a Slack API request and returns the result.

+
+
+def logs(self,
*,
latest: int | None = None,
oldest: int | None = None,
limit: int | None = None,
action: str | None = None,
actor: str | None = None,
entity: str | None = None,
cursor: str | None = None,
additional_query_params: Dict[str, Any] | None = None,
headers: Dict[str, str] | None = None) ‑> AuditLogsResponse
+
+
+
+ +Expand source code + +
def logs(
+    self,
+    *,
+    latest: Optional[int] = None,
+    oldest: Optional[int] = None,
+    limit: Optional[int] = None,
+    action: Optional[str] = None,
+    actor: Optional[str] = None,
+    entity: Optional[str] = None,
+    cursor: Optional[str] = None,
+    additional_query_params: Optional[Dict[str, Any]] = None,
+    headers: Optional[Dict[str, str]] = None,
+) -> AuditLogsResponse:
+    """This is the primary endpoint for retrieving actual audit events from your organization.
+    It will return a list of actions that have occurred on the installed workspace or grid organization.
+    Authentication required.
+
+    The following filters can be applied in order to narrow the range of actions returned.
+    Filters are added as query string parameters and can be combined together.
+    Multiple filter parameters are additive (a boolean AND) and are separated
+    with an ampersand (&) in the query string. Filtering is entirely optional.
+
+    Args:
+        latest: Unix timestamp of the most recent audit event to include (inclusive).
+        oldest: Unix timestamp of the least recent audit event to include (inclusive).
+            Data is not available prior to March 2018.
+        limit: Number of results to optimistically return, maximum 9999.
+        action: Name of the action.
+        actor: User ID who initiated the action.
+        entity: ID of the target entity of the action (such as a channel, workspace, organization, file).
+        cursor: The next page cursor of pagination
+        additional_query_params: Add anything else if you need to use the ones this library does not support
+        headers: Additional request headers
+
+    Returns:
+        API response
+    """
+    query_params = {
+        "latest": latest,
+        "oldest": oldest,
+        "limit": limit,
+        "action": action,
+        "actor": actor,
+        "entity": entity,
+        "cursor": cursor,
+    }
+    if additional_query_params is not None:
+        query_params.update(additional_query_params)
+    query_params = {k: v for k, v in query_params.items() if v is not None}
+    return self.api_call(
+        path="logs",
+        query_params=query_params,
+        headers=headers,
+    )
+
+

This is the primary endpoint for retrieving actual audit events from your organization. +It will return a list of actions that have occurred on the installed workspace or grid organization. +Authentication required.

+

The following filters can be applied in order to narrow the range of actions returned. +Filters are added as query string parameters and can be combined together. +Multiple filter parameters are additive (a boolean AND) and are separated +with an ampersand (&) in the query string. Filtering is entirely optional.

+

Args

+
+
latest
+
Unix timestamp of the most recent audit event to include (inclusive).
+
oldest
+
Unix timestamp of the least recent audit event to include (inclusive). +Data is not available prior to March 2018.
+
limit
+
Number of results to optimistically return, maximum 9999.
+
action
+
Name of the action.
+
actor
+
User ID who initiated the action.
+
entity
+
ID of the target entity of the action (such as a channel, workspace, organization, file).
+
cursor
+
The next page cursor of pagination
+
additional_query_params
+
Add anything else if you need to use the ones this library does not support
+
headers
+
Additional request headers
+
+

Returns

+

API response

+
+
+def schemas(self,
*,
query_params: Dict[str, Any] | None = None,
headers: Dict[str, str] | None = None) ‑> AuditLogsResponse
+
+
+
+ +Expand source code + +
def schemas(
+    self,
+    *,
+    query_params: Optional[Dict[str, Any]] = None,
+    headers: Optional[Dict[str, str]] = None,
+) -> AuditLogsResponse:
+    """Returns information about the kind of objects which the Audit Logs API
+    returns as a list of all objects and a short description.
+    Authentication not required.
+
+    Args:
+        query_params: Set any values if you want to add query params
+        headers: Additional request headers
+    Returns:
+        API response
+    """
+    return self.api_call(
+        path="schemas",
+        query_params=query_params,
+        headers=headers,
+    )
+
+

Returns information about the kind of objects which the Audit Logs API +returns as a list of all objects and a short description. +Authentication not required.

+

Args

+
+
query_params
+
Set any values if you want to add query params
+
headers
+
Additional request headers
+
+

Returns

+

API response

+
+
+
+
+
+
+ +
+ + + diff --git a/docs/reference/audit_logs/v1/index.html b/docs/reference/audit_logs/v1/index.html new file mode 100644 index 000000000..7c3def12a --- /dev/null +++ b/docs/reference/audit_logs/v1/index.html @@ -0,0 +1,100 @@ + + + + + + +slack_sdk.audit_logs.v1 API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.audit_logs.v1

+
+
+

Audit Logs API is a set of APIs for monitoring what’s happening in your Enterprise Grid organization.

+

Refer to https://docs.slack.dev/tools/python-slack-sdk/audit-logs for details.

+
+
+

Sub-modules

+
+
slack_sdk.audit_logs.v1.async_client
+
+

Audit Logs API is a set of APIs for monitoring what’s happening in your Enterprise Grid organization …

+
+
slack_sdk.audit_logs.v1.client
+
+

Audit Logs API is a set of APIs for monitoring what’s happening in your Enterprise Grid organization …

+
+
slack_sdk.audit_logs.v1.internal_utils
+
+
+
+
slack_sdk.audit_logs.v1.logs
+
+
+
+
slack_sdk.audit_logs.v1.response
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+ + + diff --git a/docs/reference/audit_logs/v1/internal_utils.html b/docs/reference/audit_logs/v1/internal_utils.html new file mode 100644 index 000000000..38dd60513 --- /dev/null +++ b/docs/reference/audit_logs/v1/internal_utils.html @@ -0,0 +1,66 @@ + + + + + + +slack_sdk.audit_logs.v1.internal_utils API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.audit_logs.v1.internal_utils

+
+
+
+
+
+
+
+
+
+
+
+
+ +
+ + + diff --git a/docs/reference/audit_logs/v1/logs.html b/docs/reference/audit_logs/v1/logs.html new file mode 100644 index 000000000..a36d22e5a --- /dev/null +++ b/docs/reference/audit_logs/v1/logs.html @@ -0,0 +1,3388 @@ + + + + + + +slack_sdk.audit_logs.v1.logs API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.audit_logs.v1.logs

+
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class AAARequest +(*, id: str | None = None, team_id: str | None = None, **kwargs) +
+
+
+ +Expand source code + +
class AAARequest:
+    id: Optional[str]
+    team_id: Optional[str]
+
+    def __init__(
+        self,
+        *,
+        id: Optional[str] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> None:
+        self.id = id
+        self.team_id = team_id
+
+
+

Class variables

+
+
var id : str | None
+
+

The type of the None singleton.

+
+
var team_id : str | None
+
+

The type of the None singleton.

+
+
+
+
+class AAARule +(*,
id: str | None = None,
team_id: str | None = None,
title: str | None = None,
action: Dict[str, Any] | AAARuleAction | None = None,
condition: Dict[str, Any] | AAARuleCondition | None = None,
**kwargs)
+
+
+
+ +Expand source code + +
class AAARule:
+    id: Optional[str]
+    team_id: Optional[str]
+    title: Optional[str]
+    action: Optional[AAARuleAction]
+    condition: Optional[AAARuleCondition]
+
+    def __init__(
+        self,
+        *,
+        id: Optional[str] = None,
+        team_id: Optional[str] = None,
+        title: Optional[str] = None,
+        action: Optional[Union[Dict[str, Any], AAARuleAction]] = None,
+        condition: Optional[Union[Dict[str, Any], AAARuleCondition]] = None,
+        **kwargs,
+    ) -> None:
+        self.id = id
+        self.team_id = team_id
+        self.title = title
+        self.action = action if action is None or isinstance(action, AAARuleAction) else AAARuleAction(**action)
+        self.condition = (
+            condition if condition is None or isinstance(condition, AAARuleCondition) else AAARuleCondition(**condition)
+        )
+
+
+

Class variables

+
+
var actionAAARuleAction | None
+
+

The type of the None singleton.

+
+
var conditionAAARuleCondition | None
+
+

The type of the None singleton.

+
+
var id : str | None
+
+

The type of the None singleton.

+
+
var team_id : str | None
+
+

The type of the None singleton.

+
+
var title : str | None
+
+

The type of the None singleton.

+
+
+
+
+class AAARuleAction +(*,
resolution: Dict[str, Any] | AAARuleActionResolution | None = None,
notify: List[Dict[str, Any] | AAARuleActionNotify] | None = None,
**kwargs)
+
+
+
+ +Expand source code + +
class AAARuleAction:
+    resolution: Optional[AAARuleActionResolution]
+    notify: Optional[List[AAARuleActionNotify]]
+
+    def __init__(
+        self,
+        *,
+        resolution: Optional[Union[Dict[str, Any], AAARuleActionResolution]] = None,
+        notify: Optional[List[Union[Dict[str, Any], AAARuleActionNotify]]] = None,
+        **kwargs,
+    ) -> None:
+        self.resolution = (
+            resolution
+            if resolution is None or isinstance(resolution, AAARuleActionResolution)
+            else AAARuleActionResolution(**resolution)
+        )
+        self.notify = None
+        if notify is not None:
+            self.notify = []
+            for a in notify:
+                if isinstance(a, dict):
+                    self.notify.append(AAARuleActionNotify(**a))
+                else:
+                    self.notify.append(a)
+
+
+

Class variables

+
+
var notify : List[AAARuleActionNotify] | None
+
+

The type of the None singleton.

+
+
var resolutionAAARuleActionResolution | None
+
+

The type of the None singleton.

+
+
+
+
+class AAARuleActionNotify +(*, entity_type: str | None = None, **kwargs) +
+
+
+ +Expand source code + +
class AAARuleActionNotify:
+    entity_type: Optional[str]
+
+    def __init__(
+        self,
+        *,
+        entity_type: Optional[str] = None,
+        **kwargs,
+    ) -> None:
+        self.entity_type = entity_type
+
+
+

Class variables

+
+
var entity_type : str | None
+
+

The type of the None singleton.

+
+
+
+
+class AAARuleActionResolution +(*, value: str | None = None, **kwargs) +
+
+
+ +Expand source code + +
class AAARuleActionResolution:
+    value: Optional[str]
+
+    def __init__(
+        self,
+        *,
+        value: Optional[str] = None,
+        **kwargs,
+    ) -> None:
+        self.value = value
+
+
+

Class variables

+
+
var value : str | None
+
+

The type of the None singleton.

+
+
+
+
+class AAARuleCondition +(*,
datatype: str | None = None,
operator: str | None = None,
values: List[Dict[str, Any] | AAARuleConditionValue] | None = None,
entity_type: str | None = None,
**kwargs)
+
+
+
+ +Expand source code + +
class AAARuleCondition:
+    datatype: Optional[str]
+    operator: Optional[str]
+    values: Optional[List[AAARuleConditionValue]]
+    entity_type: Optional[str]
+
+    def __init__(
+        self,
+        *,
+        datatype: Optional[str] = None,
+        operator: Optional[str] = None,
+        values: Optional[List[Union[Dict[str, Any], AAARuleConditionValue]]] = None,
+        entity_type: Optional[str] = None,
+        **kwargs,
+    ) -> None:
+        self.datatype = datatype
+        self.operator = operator
+        self.values = None
+        if values is not None:
+            self.values = []
+            for a in values:
+                if isinstance(a, dict):
+                    self.values.append(AAARuleConditionValue(**a))
+                else:
+                    self.values.append(a)
+        self.entity_type = entity_type
+
+
+

Class variables

+
+
var datatype : str | None
+
+

The type of the None singleton.

+
+
var entity_type : str | None
+
+

The type of the None singleton.

+
+
var operator : str | None
+
+

The type of the None singleton.

+
+
var values : List[AAARuleConditionValue] | None
+
+

The type of the None singleton.

+
+
+
+
+class AAARuleConditionValue +(*,
field: str | None = None,
values: List[str] | None = None,
datatype: str | None = None,
operator: str | None = None,
**kwargs)
+
+
+
+ +Expand source code + +
class AAARuleConditionValue:
+    field: Optional[str]
+    values: Optional[List[str]]
+    datatype: Optional[str]
+    operator: Optional[str]
+
+    def __init__(
+        self,
+        *,
+        field: Optional[str] = None,
+        values: Optional[List[str]] = None,
+        datatype: Optional[str] = None,
+        operator: Optional[str] = None,
+        **kwargs,
+    ) -> None:
+        self.field = field
+        self.values = values
+        self.datatype = datatype
+        self.operator = operator
+
+
+

Class variables

+
+
var datatype : str | None
+
+

The type of the None singleton.

+
+
var field : str | None
+
+

The type of the None singleton.

+
+
var operator : str | None
+
+

The type of the None singleton.

+
+
var values : List[str] | None
+
+

The type of the None singleton.

+
+
+
+
+class AccountTypeRole +(*, id: str | None = None, name: str | None = None, **kwargs) +
+
+
+ +Expand source code + +
class AccountTypeRole:
+    id: Optional[str]
+    name: Optional[str]
+    unknown_fields: Dict[str, Any]
+
+    def __init__(
+        self,
+        *,
+        id: Optional[str] = None,
+        name: Optional[str] = None,
+        **kwargs,
+    ) -> None:
+        self.id = id
+        self.name = name
+        self.unknown_fields = kwargs
+
+
+

Class variables

+
+
var id : str | None
+
+

The type of the None singleton.

+
+
var name : str | None
+
+

The type of the None singleton.

+
+
var unknown_fields : Dict[str, Any]
+
+

The type of the None singleton.

+
+
+
+
+class Actor +(type: str | None = None,
user: User | Dict[str, Any] | None = None,
**kwargs)
+
+
+
+ +Expand source code + +
class Actor:
+    type: Optional[str]
+    user: Optional[User]
+    unknown_fields: Dict[str, Any]
+
+    def __init__(
+        self,
+        type: Optional[str] = None,
+        user: Optional[Union[User, Dict[str, Any]]] = None,
+        **kwargs,
+    ) -> None:
+        self.type = type
+        self.user = User(**user) if isinstance(user, dict) else user
+        self.unknown_fields = kwargs
+
+
+

Class variables

+
+
var type : str | None
+
+

The type of the None singleton.

+
+
var unknown_fields : Dict[str, Any]
+
+

The type of the None singleton.

+
+
var userUser | None
+
+

The type of the None singleton.

+
+
+
+
+class App +(*,
id: str | None = None,
name: str | None = None,
is_distributed: bool | None = None,
is_directory_approved: bool | None = None,
is_workflow_app: bool | None = None,
scopes: List[str] | None = None,
**kwargs)
+
+
+
+ +Expand source code + +
class App:
+    id: Optional[str]
+    name: Optional[str]
+    is_distributed: Optional[bool]
+    is_directory_approved: Optional[bool]
+    is_workflow_app: Optional[bool]
+    scopes: Optional[List[str]]
+    unknown_fields: Dict[str, Any]
+
+    def __init__(
+        self,
+        *,
+        id: Optional[str] = None,
+        name: Optional[str] = None,
+        is_distributed: Optional[bool] = None,
+        is_directory_approved: Optional[bool] = None,
+        is_workflow_app: Optional[bool] = None,
+        scopes: Optional[List[str]] = None,
+        **kwargs,
+    ) -> None:
+        self.id = id
+        self.name = name
+        self.is_distributed = is_distributed
+        self.is_directory_approved = is_directory_approved
+        self.is_workflow_app = is_workflow_app
+        self.scopes = scopes
+        self.unknown_fields = kwargs
+
+
+

Class variables

+
+
var id : str | None
+
+

The type of the None singleton.

+
+
var is_directory_approved : bool | None
+
+

The type of the None singleton.

+
+
var is_distributed : bool | None
+
+

The type of the None singleton.

+
+
var is_workflow_app : bool | None
+
+

The type of the None singleton.

+
+
var name : str | None
+
+

The type of the None singleton.

+
+
var scopes : List[str] | None
+
+

The type of the None singleton.

+
+
var unknown_fields : Dict[str, Any]
+
+

The type of the None singleton.

+
+
+
+
+class Attribute +(*,
name: str | None = None,
type: str | None = None,
items: AttributeItems | None = None,
**kwargs)
+
+
+
+ +Expand source code + +
class Attribute:
+    name: Optional[str]
+    type: Optional[str]
+    items: Optional[AttributeItems]
+
+    def __init__(
+        self,
+        *,
+        name: Optional[str] = None,
+        type: Optional[str] = None,
+        items: Optional[AttributeItems] = None,
+        **kwargs,
+    ) -> None:
+        self.name = name
+        self.type = type
+        self.items = items
+
+
+

Class variables

+
+
var itemsAttributeItems | None
+
+

The type of the None singleton.

+
+
var name : str | None
+
+

The type of the None singleton.

+
+
var type : str | None
+
+

The type of the None singleton.

+
+
+
+
+class AttributeItems +(*, type: str | None = None, **kwargs) +
+
+
+ +Expand source code + +
class AttributeItems:
+    type: Optional[str]
+
+    def __init__(
+        self,
+        *,
+        type: Optional[str] = None,
+        **kwargs,
+    ) -> None:
+        self.type = type
+
+
+

Class variables

+
+
var type : str | None
+
+

The type of the None singleton.

+
+
+
+
+class Channel +(*,
id: str | None = None,
privacy: str | None = None,
name: str | None = None,
is_shared: bool | None = None,
is_org_shared: bool | None = None,
teams_shared_with: List[str] | None = None,
original_connected_channel_id: str | None = None,
is_salesforce_channel: bool | None = None,
**kwargs)
+
+
+
+ +Expand source code + +
class Channel:
+    id: Optional[str]
+    privacy: Optional[str]
+    name: Optional[str]
+    is_shared: Optional[bool]
+    is_org_shared: Optional[bool]
+    teams_shared_with: Optional[List[str]]
+    original_connected_channel_id: Optional[str]
+    is_salesforce_channel: Optional[bool]
+    unknown_fields: Dict[str, Any]
+
+    def __init__(
+        self,
+        *,
+        id: Optional[str] = None,
+        privacy: Optional[str] = None,
+        name: Optional[str] = None,
+        is_shared: Optional[bool] = None,
+        is_org_shared: Optional[bool] = None,
+        teams_shared_with: Optional[List[str]] = None,
+        original_connected_channel_id: Optional[str] = None,
+        is_salesforce_channel: Optional[bool] = None,
+        **kwargs,
+    ) -> None:
+        self.id = id
+        self.privacy = privacy
+        self.name = name
+        self.is_shared = is_shared
+        self.is_org_shared = is_org_shared
+        self.teams_shared_with = teams_shared_with
+        self.original_connected_channel_id = original_connected_channel_id
+        self.is_salesforce_channel = is_salesforce_channel
+        self.unknown_fields = kwargs
+
+
+

Class variables

+
+
var id : str | None
+
+

The type of the None singleton.

+
+
var is_org_shared : bool | None
+
+

The type of the None singleton.

+
+
var is_salesforce_channel : bool | None
+
+

The type of the None singleton.

+
+
var is_shared : bool | None
+
+

The type of the None singleton.

+
+
var name : str | None
+
+

The type of the None singleton.

+
+
var original_connected_channel_id : str | None
+
+

The type of the None singleton.

+
+
var privacy : str | None
+
+

The type of the None singleton.

+
+
var teams_shared_with : List[str] | None
+
+

The type of the None singleton.

+
+
var unknown_fields : Dict[str, Any]
+
+

The type of the None singleton.

+
+
+
+
+class Context +(*,
location: Location | Dict[str, Any] | None = None,
ua: str | None = None,
ip_address: str | None = None,
session_id: str | None = None,
app: App | Dict[str, Any] | None = None,
**kwargs)
+
+
+
+ +Expand source code + +
class Context:
+    location: Optional[Location]
+    ua: Optional[str]
+    ip_address: Optional[str]
+    session_id: Optional[str]
+    app: Optional[App]
+    unknown_fields: Dict[str, Any]
+
+    def __init__(
+        self,
+        *,
+        location: Optional[Union[Location, Dict[str, Any]]] = None,
+        ua: Optional[str] = None,
+        ip_address: Optional[str] = None,
+        session_id: Optional[str] = None,
+        app: Optional[Union[App, Dict[str, Any]]] = None,
+        **kwargs,
+    ) -> None:
+        self.location = Location(**location) if isinstance(location, dict) else location
+        self.ua = ua
+        self.ip_address = ip_address
+        self.session_id = session_id
+        self.app = App(**app) if isinstance(app, dict) else app
+        self.unknown_fields = kwargs
+
+
+

Class variables

+
+
var appApp | None
+
+

The type of the None singleton.

+
+
var ip_address : str | None
+
+

The type of the None singleton.

+
+
var locationLocation | None
+
+

The type of the None singleton.

+
+
var session_id : str | None
+
+

The type of the None singleton.

+
+
var ua : str | None
+
+

The type of the None singleton.

+
+
var unknown_fields : Dict[str, Any]
+
+

The type of the None singleton.

+
+
+
+
+class ConversationPref +(*, type: List[str] | None = None, user: List[str] | None = None, **kwargs) +
+
+
+ +Expand source code + +
class ConversationPref:
+    type: Optional[List[str]]
+    user: Optional[List[str]]
+
+    def __init__(
+        self,
+        *,
+        type: Optional[List[str]] = None,
+        user: Optional[List[str]] = None,
+        **kwargs,
+    ) -> None:
+        self.type = type
+        self.user = user
+        self.unknown_fields = kwargs
+
+
+

Class variables

+
+
var type : List[str] | None
+
+

The type of the None singleton.

+
+
var user : List[str] | None
+
+

The type of the None singleton.

+
+
+
+
+class Details +(*,
name: str | None = None,
new_value: str | List[str] | Dict[str, Any] | None = None,
previous_value: str | List[str] | Dict[str, Any] | None = None,
expires_on: int | None = None,
mobile_only: bool | None = None,
web_only: bool | None = None,
non_sso_only: bool | None = None,
type: str | None = None,
is_workflow: bool | None = None,
inviter: User | Dict[str, Any] | None = None,
kicker: User | Dict[str, Any] | None = None,
shared_to: str | None = None,
reason: str | None = None,
origin_team: str | None = None,
target_team: str | None = None,
is_internal_integration: bool | None = None,
cleared_resolution: str | None = None,
app_owner_id: str | None = None,
bot_scopes: List[str] | None = None,
new_scopes: List[str] | None = None,
previous_scopes: List[str] | None = None,
granular_bot_token: bool | None = None,
scopes: List[str] | None = None,
scopes_bot: List[str] | None = None,
resolution: str | None = None,
app_previously_resolved: bool | None = None,
admin_app_id: str | None = None,
bot_id: str | None = None,
installer_user_id: str | None = None,
approver_id: str | None = None,
approval_type: str | None = None,
app_previously_approved: bool | None = None,
old_scopes: List[str] | None = None,
channels: List[str] | None = None,
permissions: List[Dict[str, Any]] | None = None,
new_version_id: str | None = None,
trigger: str | None = None,
export_type: str | None = None,
export_start_ts: str | None = None,
export_end_ts: str | None = None,
barrier_id: str | None = None,
primary_usergroup_id: str | None = None,
barriered_from_usergroup_ids: List[str] | None = None,
restricted_subjects: List[str] | None = None,
duration: int | None = None,
desktop_app_browser_quit: bool | None = None,
invite_id: str | None = None,
external_organization_id: str | None = None,
external_organization_name: str | None = None,
external_user_id: str | None = None,
external_user_email: str | None = None,
channel_id: str | None = None,
added_team_id: str | None = None,
is_token_rotation_enabled_app: bool | None = None,
old_retention_policy: Dict[str, Any] | RetentionPolicy | None = None,
new_retention_policy: Dict[str, Any] | RetentionPolicy | None = None,
who_can_post: Dict[str, List[str]] | ConversationPref | None = None,
can_thread: Dict[str, List[str]] | ConversationPref | None = None,
is_external_limited: bool | None = None,
exporting_team_id: int | None = None,
session_search_start: int | None = None,
deprecation_search_end: int | None = None,
is_error: bool | None = None,
creator: str | None = None,
team: str | None = None,
app_id: str | None = None,
enable_at_here: Dict[str, Any] | FeatureEnablement | None = None,
enable_at_channel: Dict[str, Any] | FeatureEnablement | None = None,
can_huddle: Dict[str, Any] | FeatureEnablement | None = None,
url_private: str | None = None,
shared_with: Dict[str, Any] | SharedWith | None = None,
initiated_by: str | None = None,
source_team: str | None = None,
destination_team: str | None = None,
succeeded_users: List[str] | str | None = None,
failed_users: List[str] | str | None = None,
enterprise: str | None = None,
subteam: str | None = None,
action: str | None = None,
idp_group_member_count: int | None = None,
workspace_member_count: int | None = None,
added_user_count: int | None = None,
added_user_error_count: int | None = None,
reactivated_user_count: int | None = None,
removed_user_count: int | None = None,
removed_user_error_count: int | None = None,
total_removal_count: int | None = None,
is_flagged: str | None = None,
target_user: str | None = None,
idp_config_id: str | None = None,
config_type: str | None = None,
idp_entity_id_hash: str | None = None,
label: str | None = None,
previous_profile: Dict[str, Any] | Profile | None = None,
new_profile: Dict[str, Any] | Profile | None = None,
target_user_id: str | None = None,
space_file_id: Dict[str, Any] | SpaceFileId | None = None,
target_entity: str | None = None,
target_entity_id: str | None = None,
changed_permissions: List[str] | None = None,
datastore_name: str | None = None,
attributes: List[Dict[str, str] | Attribute] | None = None,
channel: str | None = None,
entity_type: str | None = None,
actor: str | None = None,
access_level: str | None = None,
functions: List[str] | None = None,
workflows: List[str] | None = None,
datastores: List[str] | None = None,
permissions_updated: bool | None = None,
matched_rule: Dict[str, Any] | AAARule | None = None,
request: Dict[str, Any] | AAARequest | None = None,
rules_checked: List[Dict[str, Any] | AAARule] | None = None,
disconnecting_team: str | None = None,
is_channel_canvas: bool | None = None,
linked_channel_id: str | None = None,
column_id: str | None = None,
row_id: str | None = None,
cell_date_updated: int | None = None,
view_id: str | None = None,
user: str | None = None,
**kwargs)
+
+
+
+ +Expand source code + +
class Details:
+    name: Optional[str]
+    new_value: Optional[Union[str, List[str], Dict[str, Any]]]
+    previous_value: Optional[Union[str, List[str], Dict[str, Any]]]
+    expires_on: Optional[int]
+    mobile_only: Optional[bool]
+    web_only: Optional[bool]
+    non_sso_only: Optional[bool]
+    type: Optional[str]
+    is_workflow: Optional[bool]
+    inviter: Optional[User]
+    kicker: Optional[User]
+    shared_to: Optional[str]
+    reason: Optional[str]
+    origin_team: Optional[str]
+    target_team: Optional[str]
+    is_internal_integration: Optional[bool]
+    cleared_resolution: Optional[str]
+    app_owner_id: Optional[str]
+    bot_scopes: Optional[List[str]]
+    new_scopes: Optional[List[str]]
+    previous_scopes: Optional[List[str]]
+    granular_bot_token: Optional[bool]
+    scopes: Optional[List[str]]
+    scopes_bot: Optional[List[str]]
+    resolution: Optional[str]
+    app_previously_resolved: Optional[bool]
+    admin_app_id: Optional[str]
+    bot_id: Optional[str]
+    installer_user_id: Optional[str]
+    approver_id: Optional[str]
+    approval_type: Optional[str]
+    app_previously_approved: Optional[bool]
+    old_scopes: Optional[List[str]]
+    channels: Optional[List[str]]
+    permissions: Optional[List[Dict[str, Any]]]
+    new_version_id: Optional[str]
+    trigger: Optional[str]
+    export_type: Optional[str]
+    export_start_ts: Optional[str]
+    export_end_ts: Optional[str]
+    barrier_id: Optional[str]
+    primary_usergroup_id: Optional[str]
+    barriered_from_usergroup_ids: Optional[List[str]]
+    restricted_subjects: Optional[List[str]]
+    duration: Optional[int]
+    desktop_app_browser_quit: Optional[bool]
+    invite_id: Optional[str]
+    external_organization_id: Optional[str]
+    external_organization_name: Optional[str]
+    external_user_id: Optional[str]
+    external_user_email: Optional[str]
+    channel_id: Optional[str]
+    added_team_id: Optional[str]
+    unknown_fields: Dict[str, Any]
+    is_token_rotation_enabled_app: Optional[bool]
+    old_retention_policy: Optional[RetentionPolicy]
+    new_retention_policy: Optional[RetentionPolicy]
+    who_can_post: Optional[ConversationPref]
+    can_thread: Optional[ConversationPref]
+    is_external_limited: Optional[bool]
+    exporting_team_id: Optional[int]
+    session_search_start: Optional[int]
+    deprecation_search_end: Optional[int]
+    is_error: Optional[bool]
+    creator: Optional[str]
+    team: Optional[str]
+    app_id: Optional[str]
+    enable_at_here: Optional[FeatureEnablement]
+    enable_at_channel: Optional[FeatureEnablement]
+    can_huddle: Optional[FeatureEnablement]
+    url_private: Optional[str]
+    shared_with: Optional[SharedWith]
+    initiated_by: Optional[str]
+    source_team: Optional[str]
+    destination_team: Optional[str]
+    succeeded_users: Optional[List[str]]
+    failed_users: Optional[List[str]]
+    enterprise: Optional[str]
+    subteam: Optional[str]
+    action: Optional[str]
+    idp_group_member_count: Optional[int]
+    workspace_member_count: Optional[int]
+    added_user_count: Optional[int]
+    added_user_error_count: Optional[int]
+    reactivated_user_count: Optional[int]
+    removed_user_count: Optional[int]
+    removed_user_error_count: Optional[int]
+    total_removal_count: Optional[int]
+    is_flagged: Optional[str]
+    target_user: Optional[str]
+    idp_config_id: Optional[str]
+    config_type: Optional[str]
+    idp_entity_id_hash: Optional[str]
+    label: Optional[str]
+    previous_profile: Optional[Profile]
+    new_profile: Optional[Profile]
+    target_user_id: Optional[str]
+    space_file_id: Optional[SpaceFileId]
+    target_entity: Optional[str]
+    target_entity_id: Optional[str]
+    changed_permissions: Optional[List[str]]
+    datastore_name: Optional[str]
+    attributes: Optional[List[Attribute]]
+    channel: Optional[str]
+    entity_type: Optional[str]
+    actor: Optional[str]
+    access_level: Optional[str]
+    functions: Optional[List[str]]
+    workflows: Optional[List[str]]
+    datastores: Optional[List[str]]
+    permissions_updated: Optional[bool]
+    matched_rule: Optional[AAARule]
+    request: Optional[AAARequest]
+    rules_checked: Optional[List[AAARule]]
+    disconnecting_team: Optional[str]
+    is_channel_canvas: Optional[bool]
+    linked_channel_id: Optional[str]
+    column_id: Optional[str]
+    row_id: Optional[str]
+    cell_date_updated: Optional[int]
+    view_id: Optional[str]
+    user: Optional[str]
+
+    def __init__(
+        self,
+        *,
+        name: Optional[str] = None,
+        new_value: Optional[Union[str, List[str], Dict[str, Any]]] = None,
+        previous_value: Optional[Union[str, List[str], Dict[str, Any]]] = None,
+        expires_on: Optional[int] = None,
+        mobile_only: Optional[bool] = None,
+        web_only: Optional[bool] = None,
+        non_sso_only: Optional[bool] = None,
+        type: Optional[str] = None,
+        is_workflow: Optional[bool] = None,
+        inviter: Optional[Union[Dict[str, Any], User]] = None,
+        kicker: Optional[Union[Dict[str, Any], User]] = None,
+        shared_to: Optional[str] = None,
+        reason: Optional[str] = None,
+        origin_team: Optional[str] = None,
+        target_team: Optional[str] = None,
+        is_internal_integration: Optional[bool] = None,
+        cleared_resolution: Optional[str] = None,
+        app_owner_id: Optional[str] = None,
+        bot_scopes: Optional[List[str]] = None,
+        new_scopes: Optional[List[str]] = None,
+        previous_scopes: Optional[List[str]] = None,
+        granular_bot_token: Optional[bool] = None,
+        scopes: Optional[List[str]] = None,
+        scopes_bot: Optional[List[str]] = None,
+        resolution: Optional[str] = None,
+        app_previously_resolved: Optional[bool] = None,
+        admin_app_id: Optional[str] = None,
+        bot_id: Optional[str] = None,
+        installer_user_id: Optional[str] = None,
+        approver_id: Optional[str] = None,
+        approval_type: Optional[str] = None,
+        app_previously_approved: Optional[bool] = None,
+        old_scopes: Optional[List[str]] = None,
+        channels: Optional[List[str]] = None,
+        permissions: Optional[List[Dict[str, Any]]] = None,
+        new_version_id: Optional[str] = None,
+        trigger: Optional[str] = None,
+        export_type: Optional[str] = None,
+        export_start_ts: Optional[str] = None,
+        export_end_ts: Optional[str] = None,
+        barrier_id: Optional[str] = None,
+        primary_usergroup_id: Optional[str] = None,
+        barriered_from_usergroup_ids: Optional[List[str]] = None,
+        restricted_subjects: Optional[List[str]] = None,
+        duration: Optional[int] = None,
+        desktop_app_browser_quit: Optional[bool] = None,
+        invite_id: Optional[str] = None,
+        external_organization_id: Optional[str] = None,
+        external_organization_name: Optional[str] = None,
+        external_user_id: Optional[str] = None,
+        external_user_email: Optional[str] = None,
+        channel_id: Optional[str] = None,
+        added_team_id: Optional[str] = None,
+        is_token_rotation_enabled_app: Optional[bool] = None,
+        old_retention_policy: Optional[Union[Dict[str, Any], RetentionPolicy]] = None,
+        new_retention_policy: Optional[Union[Dict[str, Any], RetentionPolicy]] = None,
+        who_can_post: Optional[Union[Dict[str, List[str]], ConversationPref]] = None,
+        can_thread: Optional[Union[Dict[str, List[str]], ConversationPref]] = None,
+        is_external_limited: Optional[bool] = None,
+        exporting_team_id: Optional[int] = None,
+        session_search_start: Optional[int] = None,
+        deprecation_search_end: Optional[int] = None,
+        is_error: Optional[bool] = None,
+        creator: Optional[str] = None,
+        team: Optional[str] = None,
+        app_id: Optional[str] = None,
+        enable_at_here: Optional[Union[Dict[str, Any], FeatureEnablement]] = None,
+        enable_at_channel: Optional[Union[Dict[str, Any], FeatureEnablement]] = None,
+        can_huddle: Optional[Union[Dict[str, Any], FeatureEnablement]] = None,
+        url_private: Optional[str] = None,
+        shared_with: Optional[Union[Dict[str, Any], SharedWith]] = None,
+        initiated_by: Optional[str] = None,
+        source_team: Optional[str] = None,
+        destination_team: Optional[str] = None,
+        succeeded_users: Optional[Union[List[str], str]] = None,
+        failed_users: Optional[Union[List[str], str]] = None,
+        enterprise: Optional[str] = None,
+        subteam: Optional[str] = None,
+        action: Optional[str] = None,
+        idp_group_member_count: Optional[int] = None,
+        workspace_member_count: Optional[int] = None,
+        added_user_count: Optional[int] = None,
+        added_user_error_count: Optional[int] = None,
+        reactivated_user_count: Optional[int] = None,
+        removed_user_count: Optional[int] = None,
+        removed_user_error_count: Optional[int] = None,
+        total_removal_count: Optional[int] = None,
+        is_flagged: Optional[str] = None,
+        target_user: Optional[str] = None,
+        idp_config_id: Optional[str] = None,
+        config_type: Optional[str] = None,
+        idp_entity_id_hash: Optional[str] = None,
+        label: Optional[str] = None,
+        previous_profile: Optional[Union[Dict[str, Any], Profile]] = None,
+        new_profile: Optional[Union[Dict[str, Any], Profile]] = None,
+        target_user_id: Optional[str] = None,
+        space_file_id: Optional[Union[Dict[str, Any], SpaceFileId]] = None,
+        target_entity: Optional[str] = None,
+        target_entity_id: Optional[str] = None,
+        changed_permissions: Optional[List[str]] = None,
+        datastore_name: Optional[str] = None,
+        attributes: Optional[List[Union[Dict[str, str], Attribute]]] = None,
+        channel: Optional[str] = None,
+        entity_type: Optional[str] = None,
+        actor: Optional[str] = None,
+        access_level: Optional[str] = None,
+        functions: Optional[List[str]] = None,
+        workflows: Optional[List[str]] = None,
+        datastores: Optional[List[str]] = None,
+        permissions_updated: Optional[bool] = None,
+        matched_rule: Optional[Union[Dict[str, Any], AAARule]] = None,
+        request: Optional[Union[Dict[str, Any], AAARequest]] = None,
+        rules_checked: Optional[List[Union[Dict[str, Any], AAARule]]] = None,
+        disconnecting_team: Optional[str] = None,
+        is_channel_canvas: Optional[bool] = None,
+        linked_channel_id: Optional[str] = None,
+        column_id: Optional[str] = None,
+        row_id: Optional[str] = None,
+        cell_date_updated: Optional[int] = None,
+        view_id: Optional[str] = None,
+        user: Optional[str] = None,
+        **kwargs,
+    ) -> None:
+        self.name = name
+        self.new_value = new_value
+        self.previous_value = previous_value
+        self.expires_on = expires_on
+        self.mobile_only = mobile_only
+        self.web_only = web_only
+        self.non_sso_only = non_sso_only
+        self.type = type
+        self.is_workflow = is_workflow
+        self.inviter = inviter if inviter is None or isinstance(inviter, User) else User(**inviter)
+        self.kicker = kicker if kicker is None or isinstance(kicker, User) else User(**kicker)
+        self.shared_to = shared_to
+        self.reason = reason
+        self.origin_team = origin_team
+        self.target_team = target_team
+        self.is_internal_integration = is_internal_integration
+        self.cleared_resolution = cleared_resolution
+        self.app_owner_id = app_owner_id
+        self.bot_scopes = bot_scopes
+        self.new_scopes = new_scopes
+        self.previous_scopes = previous_scopes
+        self.granular_bot_token = granular_bot_token
+        self.scopes = scopes
+        self.scopes_bot = scopes_bot
+        self.resolution = resolution
+        self.app_previously_resolved = app_previously_resolved
+        self.admin_app_id = admin_app_id
+        self.bot_id = bot_id
+        self.unknown_fields = kwargs
+        self.installer_user_id = installer_user_id
+        self.approver_id = approver_id
+        self.approval_type = approval_type
+        self.app_previously_approved = app_previously_approved
+        self.old_scopes = old_scopes
+        self.channels = channels
+        self.permissions = permissions
+        self.new_version_id = new_version_id
+        self.trigger = trigger
+        self.export_type = export_type
+        self.export_start_ts = export_start_ts
+        self.export_end_ts = export_end_ts
+        self.barrier_id = barrier_id
+        self.primary_usergroup_id = primary_usergroup_id
+        self.barriered_from_usergroup_ids = barriered_from_usergroup_ids
+        self.restricted_subjects = restricted_subjects
+        self.duration = duration
+        self.desktop_app_browser_quit = desktop_app_browser_quit
+        self.invite_id = invite_id
+        self.external_organization_id = external_organization_id
+        self.external_organization_name = external_organization_name
+        self.external_user_id = external_user_id
+        self.external_user_email = external_user_email
+        self.channel_id = channel_id
+        self.added_team_id = added_team_id
+        self.is_token_rotation_enabled_app = is_token_rotation_enabled_app
+        self.old_retention_policy = (
+            old_retention_policy
+            if old_retention_policy is None or isinstance(old_retention_policy, RetentionPolicy)
+            else RetentionPolicy(**old_retention_policy)
+        )
+        self.new_retention_policy = (
+            new_retention_policy
+            if new_retention_policy is None or isinstance(new_retention_policy, RetentionPolicy)
+            else RetentionPolicy(**new_retention_policy)
+        )
+        self.who_can_post = (
+            who_can_post
+            if who_can_post is None or isinstance(who_can_post, ConversationPref)
+            else ConversationPref(**who_can_post)
+        )
+        self.can_thread = (
+            can_thread if can_thread is None or isinstance(can_thread, ConversationPref) else ConversationPref(**can_thread)
+        )
+        self.is_external_limited = is_external_limited
+        self.exporting_team_id = exporting_team_id
+        self.session_search_start = session_search_start
+        self.deprecation_search_end = deprecation_search_end
+        self.is_error = is_error
+        self.creator = creator
+        self.team = team
+        self.app_id = app_id
+        self.enable_at_here = (
+            enable_at_here
+            if enable_at_here is None or isinstance(enable_at_here, FeatureEnablement)
+            else FeatureEnablement(**enable_at_here)
+        )
+        self.enable_at_channel = (
+            enable_at_channel
+            if enable_at_channel is None or isinstance(enable_at_channel, FeatureEnablement)
+            else FeatureEnablement(**enable_at_channel)
+        )
+        self.can_huddle = (
+            can_huddle
+            if can_huddle is None or isinstance(can_huddle, FeatureEnablement)
+            else FeatureEnablement(**can_huddle)
+        )
+        self.url_private = url_private
+        self.shared_with = (
+            shared_with if shared_with is None or isinstance(shared_with, SharedWith) else SharedWith(**shared_with)
+        )
+        self.initiated_by = initiated_by
+        self.source_team = source_team
+        self.destination_team = destination_team
+        self.succeeded_users = (
+            succeeded_users if succeeded_users is None or isinstance(succeeded_users, list) else json.loads(succeeded_users)
+        )
+        self.failed_users = (
+            failed_users if failed_users is None or isinstance(failed_users, list) else json.loads(failed_users)
+        )
+        self.enterprise = enterprise
+        self.subteam = subteam
+        self.action = action
+        self.idp_group_member_count = idp_group_member_count
+        self.workspace_member_count = workspace_member_count
+        self.added_user_count = added_user_count
+        self.added_user_error_count = added_user_error_count
+        self.reactivated_user_count = reactivated_user_count
+        self.removed_user_count = removed_user_count
+        self.removed_user_error_count = removed_user_error_count
+        self.total_removal_count = total_removal_count
+        self.is_flagged = is_flagged
+        self.target_user = target_user
+        self.idp_config_id = idp_config_id
+        self.config_type = config_type
+        self.idp_entity_id_hash = idp_entity_id_hash
+        self.label = label
+        self.previous_profile = (
+            previous_profile
+            if previous_profile is None or isinstance(previous_profile, Profile)
+            else Profile(**previous_profile)
+        )
+        self.new_profile = new_profile if new_profile is None or isinstance(new_profile, Profile) else Profile(**new_profile)
+        self.target_user_id = target_user_id
+        self.space_file_id = (
+            space_file_id
+            if space_file_id is None or isinstance(space_file_id, SpaceFileId)
+            else SpaceFileId(**space_file_id)
+        )
+        self.target_entity = target_entity
+        self.target_entity_id = target_entity_id
+        self.changed_permissions = changed_permissions
+        self.datastore_name = datastore_name
+        self.attributes = None
+        if attributes is not None:
+            self.attributes = []
+            for a in attributes:
+                if isinstance(a, dict):
+                    self.attributes.append(Attribute(**a))  # type: ignore[arg-type]
+                else:
+                    self.attributes.append(a)
+        self.channel = channel
+        self.entity_type = entity_type
+        self.actor = actor
+        self.access_level = access_level
+        self.functions = functions
+        self.workflows = workflows
+        self.datastores = datastores
+        self.permissions_updated = permissions_updated
+        self.matched_rule = (
+            matched_rule if matched_rule is None or isinstance(matched_rule, AAARule) else AAARule(**matched_rule)
+        )
+        self.request = request if request is None or isinstance(request, AAARequest) else AAARequest(**request)
+        self.rules_checked = None
+        if rules_checked is not None:
+            self.rules_checked = []
+            for a in rules_checked:  # type: ignore[assignment]
+                if isinstance(a, dict):
+                    self.rules_checked.append(AAARule(**a))  # type: ignore[arg-type]
+                else:
+                    self.rules_checked.append(a)  # type: ignore[arg-type]
+        self.disconnecting_team = disconnecting_team
+        self.is_channel_canvas = is_channel_canvas
+        self.linked_channel_id = linked_channel_id
+        self.column_id = column_id
+        self.row_id = row_id
+        self.cell_date_updated = cell_date_updated
+        self.view_id = view_id
+        self.user = user
+
+
+

Class variables

+
+
var access_level : str | None
+
+

The type of the None singleton.

+
+
var action : str | None
+
+

The type of the None singleton.

+
+
var actor : str | None
+
+

The type of the None singleton.

+
+
var added_team_id : str | None
+
+

The type of the None singleton.

+
+
var added_user_count : int | None
+
+

The type of the None singleton.

+
+
var added_user_error_count : int | None
+
+

The type of the None singleton.

+
+
var admin_app_id : str | None
+
+

The type of the None singleton.

+
+
var app_id : str | None
+
+

The type of the None singleton.

+
+
var app_owner_id : str | None
+
+

The type of the None singleton.

+
+
var app_previously_approved : bool | None
+
+

The type of the None singleton.

+
+
var app_previously_resolved : bool | None
+
+

The type of the None singleton.

+
+
var approval_type : str | None
+
+

The type of the None singleton.

+
+
var approver_id : str | None
+
+

The type of the None singleton.

+
+
var attributes : List[Attribute] | None
+
+

The type of the None singleton.

+
+
var barrier_id : str | None
+
+

The type of the None singleton.

+
+
var barriered_from_usergroup_ids : List[str] | None
+
+

The type of the None singleton.

+
+
var bot_id : str | None
+
+

The type of the None singleton.

+
+
var bot_scopes : List[str] | None
+
+

The type of the None singleton.

+
+
var can_huddleFeatureEnablement | None
+
+

The type of the None singleton.

+
+
var can_threadConversationPref | None
+
+

The type of the None singleton.

+
+
var cell_date_updated : int | None
+
+

The type of the None singleton.

+
+
var changed_permissions : List[str] | None
+
+

The type of the None singleton.

+
+
var channel : str | None
+
+

The type of the None singleton.

+
+
var channel_id : str | None
+
+

The type of the None singleton.

+
+
var channels : List[str] | None
+
+

The type of the None singleton.

+
+
var cleared_resolution : str | None
+
+

The type of the None singleton.

+
+
var column_id : str | None
+
+

The type of the None singleton.

+
+
var config_type : str | None
+
+

The type of the None singleton.

+
+
var creator : str | None
+
+

The type of the None singleton.

+
+
var datastore_name : str | None
+
+

The type of the None singleton.

+
+
var datastores : List[str] | None
+
+

The type of the None singleton.

+
+
var deprecation_search_end : int | None
+
+

The type of the None singleton.

+
+
var desktop_app_browser_quit : bool | None
+
+

The type of the None singleton.

+
+
var destination_team : str | None
+
+

The type of the None singleton.

+
+
var disconnecting_team : str | None
+
+

The type of the None singleton.

+
+
var duration : int | None
+
+

The type of the None singleton.

+
+
var enable_at_channelFeatureEnablement | None
+
+

The type of the None singleton.

+
+
var enable_at_hereFeatureEnablement | None
+
+

The type of the None singleton.

+
+
var enterprise : str | None
+
+

The type of the None singleton.

+
+
var entity_type : str | None
+
+

The type of the None singleton.

+
+
var expires_on : int | None
+
+

The type of the None singleton.

+
+
var export_end_ts : str | None
+
+

The type of the None singleton.

+
+
var export_start_ts : str | None
+
+

The type of the None singleton.

+
+
var export_type : str | None
+
+

The type of the None singleton.

+
+
var exporting_team_id : int | None
+
+

The type of the None singleton.

+
+
var external_organization_id : str | None
+
+

The type of the None singleton.

+
+
var external_organization_name : str | None
+
+

The type of the None singleton.

+
+
var external_user_email : str | None
+
+

The type of the None singleton.

+
+
var external_user_id : str | None
+
+

The type of the None singleton.

+
+
var failed_users : List[str] | None
+
+

The type of the None singleton.

+
+
var functions : List[str] | None
+
+

The type of the None singleton.

+
+
var granular_bot_token : bool | None
+
+

The type of the None singleton.

+
+
var idp_config_id : str | None
+
+

The type of the None singleton.

+
+
var idp_entity_id_hash : str | None
+
+

The type of the None singleton.

+
+
var idp_group_member_count : int | None
+
+

The type of the None singleton.

+
+
var initiated_by : str | None
+
+

The type of the None singleton.

+
+
var installer_user_id : str | None
+
+

The type of the None singleton.

+
+
var invite_id : str | None
+
+

The type of the None singleton.

+
+
var inviterUser | None
+
+

The type of the None singleton.

+
+
var is_channel_canvas : bool | None
+
+

The type of the None singleton.

+
+
var is_error : bool | None
+
+

The type of the None singleton.

+
+
var is_external_limited : bool | None
+
+

The type of the None singleton.

+
+
var is_flagged : str | None
+
+

The type of the None singleton.

+
+
var is_internal_integration : bool | None
+
+

The type of the None singleton.

+
+
var is_token_rotation_enabled_app : bool | None
+
+

The type of the None singleton.

+
+
var is_workflow : bool | None
+
+

The type of the None singleton.

+
+
var kickerUser | None
+
+

The type of the None singleton.

+
+
var label : str | None
+
+

The type of the None singleton.

+
+
var linked_channel_id : str | None
+
+

The type of the None singleton.

+
+
var matched_ruleAAARule | None
+
+

The type of the None singleton.

+
+
var mobile_only : bool | None
+
+

The type of the None singleton.

+
+
var name : str | None
+
+

The type of the None singleton.

+
+
var new_profileProfile | None
+
+

The type of the None singleton.

+
+
var new_retention_policyRetentionPolicy | None
+
+

The type of the None singleton.

+
+
var new_scopes : List[str] | None
+
+

The type of the None singleton.

+
+
var new_value : str | List[str] | Dict[str, Any] | None
+
+

The type of the None singleton.

+
+
var new_version_id : str | None
+
+

The type of the None singleton.

+
+
var non_sso_only : bool | None
+
+

The type of the None singleton.

+
+
var old_retention_policyRetentionPolicy | None
+
+

The type of the None singleton.

+
+
var old_scopes : List[str] | None
+
+

The type of the None singleton.

+
+
var origin_team : str | None
+
+

The type of the None singleton.

+
+
var permissions : List[Dict[str, Any]] | None
+
+

The type of the None singleton.

+
+
var permissions_updated : bool | None
+
+

The type of the None singleton.

+
+
var previous_profileProfile | None
+
+

The type of the None singleton.

+
+
var previous_scopes : List[str] | None
+
+

The type of the None singleton.

+
+
var previous_value : str | List[str] | Dict[str, Any] | None
+
+

The type of the None singleton.

+
+
var primary_usergroup_id : str | None
+
+

The type of the None singleton.

+
+
var reactivated_user_count : int | None
+
+

The type of the None singleton.

+
+
var reason : str | None
+
+

The type of the None singleton.

+
+
var removed_user_count : int | None
+
+

The type of the None singleton.

+
+
var removed_user_error_count : int | None
+
+

The type of the None singleton.

+
+
var requestAAARequest | None
+
+

The type of the None singleton.

+
+
var resolution : str | None
+
+

The type of the None singleton.

+
+
var restricted_subjects : List[str] | None
+
+

The type of the None singleton.

+
+
var row_id : str | None
+
+

The type of the None singleton.

+
+
var rules_checked : List[AAARule] | None
+
+

The type of the None singleton.

+
+
var scopes : List[str] | None
+
+

The type of the None singleton.

+
+
var scopes_bot : List[str] | None
+
+

The type of the None singleton.

+
+
var session_search_start : int | None
+
+

The type of the None singleton.

+
+
var shared_to : str | None
+
+

The type of the None singleton.

+
+
var shared_withSharedWith | None
+
+

The type of the None singleton.

+
+
var source_team : str | None
+
+

The type of the None singleton.

+
+
var space_file_idSpaceFileId | None
+
+

The type of the None singleton.

+
+
var subteam : str | None
+
+

The type of the None singleton.

+
+
var succeeded_users : List[str] | None
+
+

The type of the None singleton.

+
+
var target_entity : str | None
+
+

The type of the None singleton.

+
+
var target_entity_id : str | None
+
+

The type of the None singleton.

+
+
var target_team : str | None
+
+

The type of the None singleton.

+
+
var target_user : str | None
+
+

The type of the None singleton.

+
+
var target_user_id : str | None
+
+

The type of the None singleton.

+
+
var team : str | None
+
+

The type of the None singleton.

+
+
var total_removal_count : int | None
+
+

The type of the None singleton.

+
+
var trigger : str | None
+
+

The type of the None singleton.

+
+
var type : str | None
+
+

The type of the None singleton.

+
+
var unknown_fields : Dict[str, Any]
+
+

The type of the None singleton.

+
+
var url_private : str | None
+
+

The type of the None singleton.

+
+
var user : str | None
+
+

The type of the None singleton.

+
+
var view_id : str | None
+
+

The type of the None singleton.

+
+
var web_only : bool | None
+
+

The type of the None singleton.

+
+
var who_can_postConversationPref | None
+
+

The type of the None singleton.

+
+
var workflows : List[str] | None
+
+

The type of the None singleton.

+
+
var workspace_member_count : int | None
+
+

The type of the None singleton.

+
+
+
+
+class Entity +(*,
type: str | None = None,
user: User | Dict[str, Any] | None = None,
workspace: Location | Dict[str, Any] | None = None,
enterprise: Location | Dict[str, Any] | None = None,
channel: Channel | Dict[str, Any] | None = None,
file: File | Dict[str, Any] | None = None,
app: App | Dict[str, Any] | None = None,
message: Message | Dict[str, Any] | None = None,
huddle: Huddle | Dict[str, Any] | None = None,
role: Role | Dict[str, Any] | None = None,
usergroup: Usergroup | Dict[str, Any] | None = None,
workflow: Workflow | Dict[str, Any] | None = None,
barrier: InformationBarrier | Dict[str, Any] | None = None,
workflow_v2: WorkflowV2 | Dict[str, Any] | None = None,
account_type_role: AccountTypeRole | Dict[str, Any] | None = None,
list: SlackList | Dict[str, Any] | None = None,
**kwargs)
+
+
+
+ +Expand source code + +
class Entity:
+    type: Optional[str]
+    user: Optional[User]
+    workspace: Optional[Location]
+    enterprise: Optional[Location]
+    channel: Optional[Channel]
+    file: Optional[File]
+    app: Optional[App]
+    message: Optional[Message]
+    huddle: Optional[Huddle]
+    role: Optional[Role]
+    usergroup: Optional[Usergroup]
+    workflow: Optional[Workflow]
+    barrier: Optional[InformationBarrier]
+    workflow_v2: Optional[WorkflowV2]
+    account_type_role: Optional[AccountTypeRole]
+    list: Optional[SlackList]
+    unknown_fields: Dict[str, Any]
+
+    def __init__(
+        self,
+        *,
+        type: Optional[str] = None,
+        user: Optional[Union[User, Dict[str, Any]]] = None,
+        workspace: Optional[Union[Location, Dict[str, Any]]] = None,
+        enterprise: Optional[Union[Location, Dict[str, Any]]] = None,
+        channel: Optional[Union[Channel, Dict[str, Any]]] = None,
+        file: Optional[Union[File, Dict[str, Any]]] = None,
+        app: Optional[Union[App, Dict[str, Any]]] = None,
+        message: Optional[Union[Message, Dict[str, Any]]] = None,
+        huddle: Optional[Union[Huddle, Dict[str, Any]]] = None,
+        role: Optional[Union[Role, Dict[str, Any]]] = None,
+        usergroup: Optional[Union[Usergroup, Dict[str, Any]]] = None,
+        workflow: Optional[Union[Workflow, Dict[str, Any]]] = None,
+        barrier: Optional[Union[InformationBarrier, Dict[str, Any]]] = None,
+        workflow_v2: Optional[Union[WorkflowV2, Dict[str, Any]]] = None,
+        account_type_role: Optional[Union[AccountTypeRole, Dict[str, Any]]] = None,
+        list: Optional[Union[SlackList, Dict[str, Any]]] = None,
+        **kwargs,
+    ) -> None:
+        self.type = type
+        self.user = User(**user) if isinstance(user, dict) else user
+        self.workspace = Location(**workspace) if isinstance(workspace, dict) else workspace
+        self.enterprise = Location(**enterprise) if isinstance(enterprise, dict) else enterprise
+        self.channel = Channel(**channel) if isinstance(channel, dict) else channel
+        self.file = File(**file) if isinstance(file, dict) else file
+        self.app = App(**app) if isinstance(app, dict) else app
+        self.message = Message(**message) if isinstance(message, dict) else message
+        self.huddle = Huddle(**huddle) if isinstance(huddle, dict) else huddle
+        self.role = Role(**role) if isinstance(role, dict) else role
+        self.usergroup = Usergroup(**usergroup) if isinstance(usergroup, dict) else usergroup
+        self.workflow = Workflow(**workflow) if isinstance(workflow, dict) else workflow
+        self.barrier = InformationBarrier(**barrier) if isinstance(barrier, dict) else barrier
+        self.workflow_v2 = WorkflowV2(**workflow_v2) if isinstance(workflow_v2, dict) else workflow_v2
+        self.account_type_role = (
+            AccountTypeRole(**account_type_role) if isinstance(account_type_role, dict) else account_type_role
+        )
+        self.list = SlackList(**list) if isinstance(list, dict) else list
+        self.unknown_fields = kwargs
+
+
+

Class variables

+
+
var account_type_roleAccountTypeRole | None
+
+

The type of the None singleton.

+
+
var appApp | None
+
+

The type of the None singleton.

+
+
var barrierInformationBarrier | None
+
+

The type of the None singleton.

+
+
var channelChannel | None
+
+

The type of the None singleton.

+
+
var enterpriseLocation | None
+
+

The type of the None singleton.

+
+
var fileFile | None
+
+

The type of the None singleton.

+
+
var huddleHuddle | None
+
+

The type of the None singleton.

+
+
var listSlackList | None
+
+

The type of the None singleton.

+
+
var messageMessage | None
+
+

The type of the None singleton.

+
+
var roleRole | None
+
+

The type of the None singleton.

+
+
var type : str | None
+
+

The type of the None singleton.

+
+
var unknown_fields : Dict[str, Any]
+
+

The type of the None singleton.

+
+
var userUser | None
+
+

The type of the None singleton.

+
+
var usergroupUsergroup | None
+
+

The type of the None singleton.

+
+
var workflowWorkflow | None
+
+

The type of the None singleton.

+
+
var workflow_v2WorkflowV2 | None
+
+

The type of the None singleton.

+
+
var workspaceLocation | None
+
+

The type of the None singleton.

+
+
+
+
+class Entry +(*,
id: str | None = None,
date_create: int | None = None,
action: str | None = None,
actor: Actor | Dict[str, Any] | None = None,
entity: Entity | Dict[str, Any] | None = None,
context: Context | Dict[str, Any] | None = None,
details: Details | Dict[str, Any] | None = None,
**kwargs)
+
+
+
+ +Expand source code + +
class Entry:
+    id: Optional[str]
+    date_create: Optional[int]
+    action: Optional[str]
+    actor: Optional[Actor]
+    entity: Optional[Entity]
+    context: Optional[Context]
+    details: Optional[Details]
+    unknown_fields: Dict[str, Any]
+
+    def __init__(
+        self,
+        *,
+        id: Optional[str] = None,
+        date_create: Optional[int] = None,
+        action: Optional[str] = None,
+        actor: Optional[Union[Actor, Dict[str, Any]]] = None,
+        entity: Optional[Union[Entity, Dict[str, Any]]] = None,
+        context: Optional[Union[Context, Dict[str, Any]]] = None,
+        details: Optional[Union[Details, Dict[str, Any]]] = None,
+        **kwargs,
+    ) -> None:
+        self.id = id
+        self.date_create = date_create
+        self.action = action
+        self.actor = Actor(**actor) if isinstance(actor, dict) else actor
+        self.entity = Entity(**entity) if isinstance(entity, dict) else entity
+        self.context = Context(**context) if isinstance(context, dict) else context
+        self.details = Details(**details) if isinstance(details, dict) else details
+        self.unknown_fields = kwargs
+
+
+

Class variables

+
+
var action : str | None
+
+

The type of the None singleton.

+
+
var actorActor | None
+
+

The type of the None singleton.

+
+
var contextContext | None
+
+

The type of the None singleton.

+
+
var date_create : int | None
+
+

The type of the None singleton.

+
+
var detailsDetails | None
+
+

The type of the None singleton.

+
+
var entityEntity | None
+
+

The type of the None singleton.

+
+
var id : str | None
+
+

The type of the None singleton.

+
+
var unknown_fields : Dict[str, Any]
+
+

The type of the None singleton.

+
+
+
+
+class FeatureEnablement +(*, enabled: bool | None = None, **kwargs) +
+
+
+ +Expand source code + +
class FeatureEnablement:
+    enabled: Optional[bool]
+
+    def __init__(
+        self,
+        *,
+        enabled: Optional[bool] = None,
+        **kwargs,
+    ) -> None:
+        self.enabled = enabled
+        self.unknown_fields = kwargs
+
+
+

Class variables

+
+
var enabled : bool | None
+
+

The type of the None singleton.

+
+
+
+
+class File +(*,
id: str | None = None,
name: str | None = None,
filetype: str | None = None,
title: str | None = None,
**kwargs)
+
+
+
+ +Expand source code + +
class File:
+    id: Optional[str]
+    name: Optional[str]
+    filetype: Optional[str]
+    title: Optional[str]
+    unknown_fields: Dict[str, Any]
+
+    def __init__(
+        self,
+        *,
+        id: Optional[str] = None,
+        name: Optional[str] = None,
+        filetype: Optional[str] = None,
+        title: Optional[str] = None,
+        **kwargs,
+    ) -> None:
+        self.id = id
+        self.name = name
+        self.filetype = filetype
+        self.title = title
+        self.unknown_fields = kwargs
+
+
+

Class variables

+
+
var filetype : str | None
+
+

The type of the None singleton.

+
+
var id : str | None
+
+

The type of the None singleton.

+
+
var name : str | None
+
+

The type of the None singleton.

+
+
var title : str | None
+
+

The type of the None singleton.

+
+
var unknown_fields : Dict[str, Any]
+
+

The type of the None singleton.

+
+
+
+
+class Huddle +(*,
id: str | None = None,
date_start: int | None = None,
date_end: int | None = None,
participants: List[str] | None = None,
**kwargs)
+
+
+
+ +Expand source code + +
class Huddle:
+    id: Optional[str]
+    date_start: Optional[int]
+    date_end: Optional[int]
+    participants: Optional[List[str]]
+    unknown_fields: Dict[str, Any]
+
+    def __init__(
+        self,
+        *,
+        id: Optional[str] = None,
+        date_start: Optional[int] = None,
+        date_end: Optional[int] = None,
+        participants: Optional[List[str]] = None,
+        **kwargs,
+    ) -> None:
+        self.id = id
+        self.date_start = date_start
+        self.date_end = date_end
+        self.participants = participants
+        self.unknown_fields = kwargs
+
+
+

Class variables

+
+
var date_end : int | None
+
+

The type of the None singleton.

+
+
var date_start : int | None
+
+

The type of the None singleton.

+
+
var id : str | None
+
+

The type of the None singleton.

+
+
var participants : List[str] | None
+
+

The type of the None singleton.

+
+
var unknown_fields : Dict[str, Any]
+
+

The type of the None singleton.

+
+
+
+
+class InformationBarrier +(*,
id: str | None = None,
primary_usergroup: str | None = None,
barriered_from_usergroups: List[str] | None = None,
restricted_subjects: List[str] | None = None,
**kwargs)
+
+
+
+ +Expand source code + +
class InformationBarrier:
+    id: Optional[str]
+    primary_usergroup: Optional[str]
+    barriered_from_usergroups: Optional[List[str]]
+    restricted_subjects: Optional[List[str]]
+    unknown_fields: Dict[str, Any]
+
+    def __init__(
+        self,
+        *,
+        id: Optional[str] = None,
+        primary_usergroup: Optional[str] = None,
+        barriered_from_usergroups: Optional[List[str]] = None,
+        restricted_subjects: Optional[List[str]] = None,
+        **kwargs,
+    ) -> None:
+        self.id = id
+        self.primary_usergroup = primary_usergroup
+        self.barriered_from_usergroups = barriered_from_usergroups
+        self.restricted_subjects = restricted_subjects
+        self.unknown_fields = kwargs
+
+
+

Class variables

+
+
var barriered_from_usergroups : List[str] | None
+
+

The type of the None singleton.

+
+
var id : str | None
+
+

The type of the None singleton.

+
+
var primary_usergroup : str | None
+
+

The type of the None singleton.

+
+
var restricted_subjects : List[str] | None
+
+

The type of the None singleton.

+
+
var unknown_fields : Dict[str, Any]
+
+

The type of the None singleton.

+
+
+
+
+class Location +(*,
type: str | None = None,
id: str | None = None,
name: str | None = None,
domain: str | None = None,
**kwargs)
+
+
+
+ +Expand source code + +
class Location:
+    type: Optional[str]
+    id: Optional[str]
+    name: Optional[str]
+    domain: Optional[str]
+    unknown_fields: Dict[str, Any]
+
+    def __init__(
+        self,
+        *,
+        type: Optional[str] = None,
+        id: Optional[str] = None,
+        name: Optional[str] = None,
+        domain: Optional[str] = None,
+        **kwargs,
+    ) -> None:
+        self.type = type
+        self.id = id
+        self.name = name
+        self.domain = domain
+        self.unknown_fields = kwargs
+
+
+

Class variables

+
+
var domain : str | None
+
+

The type of the None singleton.

+
+
var id : str | None
+
+

The type of the None singleton.

+
+
var name : str | None
+
+

The type of the None singleton.

+
+
var type : str | None
+
+

The type of the None singleton.

+
+
var unknown_fields : Dict[str, Any]
+
+

The type of the None singleton.

+
+
+
+
+class LogsResponse +(*,
entries: List[Entry | Dict[str, Any]] | None = None,
response_metadata: ResponseMetadata | Dict[str, Any] | None = None,
ok: bool | None = None,
error: str | None = None,
needed: str | None = None,
provided: str | None = None,
**kwargs)
+
+
+
+ +Expand source code + +
class LogsResponse:
+    entries: Optional[List[Entry]]
+    response_metadata: Optional[ResponseMetadata]
+    ok: Optional[bool]
+    error: Optional[str]
+    needed: Optional[str]
+    provided: Optional[str]
+    unknown_fields: Dict[str, Any]
+
+    def __init__(
+        self,
+        *,
+        entries: Optional[List[Union[Entry, Dict[str, Any]]]] = None,
+        response_metadata: Optional[Union[ResponseMetadata, Dict[str, Any]]] = None,
+        ok: Optional[bool] = None,
+        error: Optional[str] = None,
+        needed: Optional[str] = None,
+        provided: Optional[str] = None,
+        **kwargs,
+    ) -> None:
+        self.entries = [Entry(**e) if isinstance(e, dict) else e for e in entries]  # type: ignore[union-attr]
+        self.response_metadata = (
+            ResponseMetadata(**response_metadata) if isinstance(response_metadata, dict) else response_metadata
+        )
+        self.ok = ok
+        self.error = error
+        self.needed = needed
+        self.provided = provided
+        self.unknown_fields = kwargs
+
+
+

Class variables

+
+
var entries : List[Entry] | None
+
+

The type of the None singleton.

+
+
var error : str | None
+
+

The type of the None singleton.

+
+
var needed : str | None
+
+

The type of the None singleton.

+
+
var ok : bool | None
+
+

The type of the None singleton.

+
+
var provided : str | None
+
+

The type of the None singleton.

+
+
var response_metadataResponseMetadata | None
+
+

The type of the None singleton.

+
+
var unknown_fields : Dict[str, Any]
+
+

The type of the None singleton.

+
+
+
+
+class Message +(*,
channel: str | None = None,
team: str | None = None,
timestamp: str | None = None,
**kwargs)
+
+
+
+ +Expand source code + +
class Message:
+    channel: Optional[str]
+    team: Optional[str]
+    timestamp: Optional[str]
+    unknown_fields: Dict[str, Any]
+
+    def __init__(
+        self,
+        *,
+        channel: Optional[str] = None,
+        team: Optional[str] = None,
+        timestamp: Optional[str] = None,
+        **kwargs,
+    ) -> None:
+        self.channel = channel
+        self.team = team
+        self.timestamp = timestamp
+        self.unknown_fields = kwargs
+
+
+

Class variables

+
+
var channel : str | None
+
+

The type of the None singleton.

+
+
var team : str | None
+
+

The type of the None singleton.

+
+
var timestamp : str | None
+
+

The type of the None singleton.

+
+
var unknown_fields : Dict[str, Any]
+
+

The type of the None singleton.

+
+
+
+
+class Profile +(*,
real_name: str | None = None,
first_name: str | None = None,
last_name: str | None = None,
display_name: str | None = None,
image_original: str | None = None,
image_24: str | None = None,
image_32: str | None = None,
image_48: str | None = None,
image_72: str | None = None,
image_192: str | None = None,
image_512: str | None = None,
image_1024: str | None = None,
**kwargs)
+
+
+
+ +Expand source code + +
class Profile:
+    real_name: Optional[str]
+    first_name: Optional[str]
+    last_name: Optional[str]
+    display_name: Optional[str]
+    image_original: Optional[str]
+    image_24: Optional[str]
+    image_32: Optional[str]
+    image_48: Optional[str]
+    image_72: Optional[str]
+    image_192: Optional[str]
+    image_512: Optional[str]
+    image_1024: Optional[str]
+
+    def __init__(
+        self,
+        *,
+        real_name: Optional[str] = None,
+        first_name: Optional[str] = None,
+        last_name: Optional[str] = None,
+        display_name: Optional[str] = None,
+        image_original: Optional[str] = None,
+        image_24: Optional[str] = None,
+        image_32: Optional[str] = None,
+        image_48: Optional[str] = None,
+        image_72: Optional[str] = None,
+        image_192: Optional[str] = None,
+        image_512: Optional[str] = None,
+        image_1024: Optional[str] = None,
+        **kwargs,
+    ) -> None:
+        self.real_name = real_name
+        self.first_name = first_name
+        self.last_name = last_name
+        self.display_name = display_name
+        self.image_original = image_original
+        self.image_24 = image_24
+        self.image_32 = image_32
+        self.image_48 = image_48
+        self.image_72 = image_72
+        self.image_192 = image_192
+        self.image_512 = image_512
+        self.image_1024 = image_1024
+
+
+

Class variables

+
+
var display_name : str | None
+
+

The type of the None singleton.

+
+
var first_name : str | None
+
+

The type of the None singleton.

+
+
var image_1024 : str | None
+
+

The type of the None singleton.

+
+
var image_192 : str | None
+
+

The type of the None singleton.

+
+
var image_24 : str | None
+
+

The type of the None singleton.

+
+
var image_32 : str | None
+
+

The type of the None singleton.

+
+
var image_48 : str | None
+
+

The type of the None singleton.

+
+
var image_512 : str | None
+
+

The type of the None singleton.

+
+
var image_72 : str | None
+
+

The type of the None singleton.

+
+
var image_original : str | None
+
+

The type of the None singleton.

+
+
var last_name : str | None
+
+

The type of the None singleton.

+
+
var real_name : str | None
+
+

The type of the None singleton.

+
+
+
+
+class ResponseMetadata +(*, next_cursor: str | None = None, **kwargs) +
+
+
+ +Expand source code + +
class ResponseMetadata:
+    next_cursor: Optional[str]
+    unknown_fields: Dict[str, Any]
+
+    def __init__(
+        self,
+        *,
+        next_cursor: Optional[str] = None,
+        **kwargs,
+    ) -> None:
+        self.next_cursor = next_cursor
+        self.unknown_fields = kwargs
+
+
+

Class variables

+
+
var next_cursor : str | None
+
+

The type of the None singleton.

+
+
var unknown_fields : Dict[str, Any]
+
+

The type of the None singleton.

+
+
+
+
+class RetentionPolicy +(*, type: str | None = None, duration_days: int | None = None, **kwargs) +
+
+
+ +Expand source code + +
class RetentionPolicy:
+    type: Optional[str]
+    duration_days: Optional[int]
+
+    def __init__(
+        self,
+        *,
+        type: Optional[str] = None,
+        duration_days: Optional[int] = None,
+        **kwargs,
+    ) -> None:
+        self.type = type
+        self.duration_days = duration_days
+        self.unknown_fields = kwargs
+
+
+

Class variables

+
+
var duration_days : int | None
+
+

The type of the None singleton.

+
+
var type : str | None
+
+

The type of the None singleton.

+
+
+
+
+class Role +(*,
id: str | None = None,
name: str | None = None,
type: str | None = None,
**kwargs)
+
+
+
+ +Expand source code + +
class Role:
+    id: Optional[str]
+    name: Optional[str]
+    type: Optional[str]
+    unknown_fields: Dict[str, Any]
+
+    def __init__(
+        self,
+        *,
+        id: Optional[str] = None,
+        name: Optional[str] = None,
+        type: Optional[str] = None,
+        **kwargs,
+    ) -> None:
+        self.id = id
+        self.name = name
+        self.type = type
+        self.unknown_fields = kwargs
+
+
+

Class variables

+
+
var id : str | None
+
+

The type of the None singleton.

+
+
var name : str | None
+
+

The type of the None singleton.

+
+
var type : str | None
+
+

The type of the None singleton.

+
+
var unknown_fields : Dict[str, Any]
+
+

The type of the None singleton.

+
+
+
+
+class SharedWith +(*, channel_id: str | None = None, access_level: str | None = None, **kwargs) +
+
+
+ +Expand source code + +
class SharedWith:
+    channel_id: Optional[str]
+    access_level: Optional[str]
+
+    def __init__(
+        self,
+        *,
+        channel_id: Optional[str] = None,
+        access_level: Optional[str] = None,
+        **kwargs,
+    ) -> None:
+        self.channel_id = channel_id
+        self.access_level = access_level
+        self.unknown_fields = kwargs
+
+
+

Class variables

+
+
var access_level : str | None
+
+

The type of the None singleton.

+
+
var channel_id : str | None
+
+

The type of the None singleton.

+
+
+
+
+class SlackList +(*, id: str | None = None, **kwargs) +
+
+
+ +Expand source code + +
class SlackList:
+    id: Optional[str]
+    unknown_fields: Dict[str, Any]
+
+    def __init__(
+        self,
+        *,
+        id: Optional[str] = None,
+        **kwargs,
+    ) -> None:
+        self.id = id
+        self.unknown_fields = kwargs
+
+
+

Class variables

+
+
var id : str | None
+
+

The type of the None singleton.

+
+
var unknown_fields : Dict[str, Any]
+
+

The type of the None singleton.

+
+
+
+
+class SpaceFileId +(*, payload: str | None = None, **kwargs) +
+
+
+ +Expand source code + +
class SpaceFileId:
+    payload: Optional[str]
+
+    def __init__(
+        self,
+        *,
+        payload: Optional[str] = None,
+        **kwargs,
+    ) -> None:
+        self.payload = payload
+
+
+

Class variables

+
+
var payload : str | None
+
+

The type of the None singleton.

+
+
+
+
+class User +(*,
id: str | None = None,
name: str | None = None,
email: str | None = None,
team: str | None = None,
**kwargs)
+
+
+
+ +Expand source code + +
class User:
+    id: Optional[str]
+    name: Optional[str]
+    email: Optional[str]
+    team: Optional[str]
+    unknown_fields: Dict[str, Any]
+
+    def __init__(
+        self,
+        *,
+        id: Optional[str] = None,
+        name: Optional[str] = None,
+        email: Optional[str] = None,
+        team: Optional[str] = None,
+        **kwargs,
+    ) -> None:
+        self.id = id
+        self.name = name
+        self.email = email
+        self.team = team
+        self.unknown_fields = kwargs
+
+
+

Class variables

+
+
var email : str | None
+
+

The type of the None singleton.

+
+
var id : str | None
+
+

The type of the None singleton.

+
+
var name : str | None
+
+

The type of the None singleton.

+
+
var team : str | None
+
+

The type of the None singleton.

+
+
var unknown_fields : Dict[str, Any]
+
+

The type of the None singleton.

+
+
+
+
+class Usergroup +(*, id: str | None = None, name: str | None = None, **kwargs) +
+
+
+ +Expand source code + +
class Usergroup:
+    id: Optional[str]
+    name: Optional[str]
+    unknown_fields: Dict[str, Any]
+
+    def __init__(
+        self,
+        *,
+        id: Optional[str] = None,
+        name: Optional[str] = None,
+        **kwargs,
+    ) -> None:
+        self.id = id
+        self.name = name
+        self.unknown_fields = kwargs
+
+
+

Class variables

+
+
var id : str | None
+
+

The type of the None singleton.

+
+
var name : str | None
+
+

The type of the None singleton.

+
+
var unknown_fields : Dict[str, Any]
+
+

The type of the None singleton.

+
+
+
+
+class Workflow +(*,
id: str | None = None,
name: str | None = None,
domain: str | None = None,
**kwargs)
+
+
+
+ +Expand source code + +
class Workflow:
+    id: Optional[str]
+    name: Optional[str]
+    domain: Optional[str]
+    unknown_fields: Dict[str, Any]
+
+    def __init__(
+        self,
+        *,
+        id: Optional[str] = None,
+        name: Optional[str] = None,
+        domain: Optional[str] = None,
+        **kwargs,
+    ) -> None:
+        self.id = id
+        self.name = name
+        self.domain = domain
+        self.unknown_fields = kwargs
+
+
+

Class variables

+
+
var domain : str | None
+
+

The type of the None singleton.

+
+
var id : str | None
+
+

The type of the None singleton.

+
+
var name : str | None
+
+

The type of the None singleton.

+
+
var unknown_fields : Dict[str, Any]
+
+

The type of the None singleton.

+
+
+
+
+class WorkflowV2 +(*,
id: str | None = None,
app_id: str | None = None,
date_updated: int | None = None,
callback_id: str | None = None,
name: str | None = None,
updated_by: str | None = None,
step_configuration: List[Dict[str, Any] | WorkflowV2StepConfiguration] | None = None,
**kwargs)
+
+
+
+ +Expand source code + +
class WorkflowV2:
+    id: Optional[str]
+    app_id: Optional[str]
+    date_updated: Optional[int]
+    callback_id: Optional[str]
+    name: Optional[str]
+    updated_by: Optional[str]
+    step_configuration: Optional[List[WorkflowV2StepConfiguration]]
+    unknown_fields: Dict[str, Any]
+
+    def __init__(
+        self,
+        *,
+        id: Optional[str] = None,
+        app_id: Optional[str] = None,
+        date_updated: Optional[int] = None,
+        callback_id: Optional[str] = None,
+        name: Optional[str] = None,
+        updated_by: Optional[str] = None,
+        step_configuration: Optional[List[Union[Dict[str, Any], WorkflowV2StepConfiguration]]] = None,
+        **kwargs,
+    ) -> None:
+        self.id = id
+        self.app_id = app_id
+        self.date_updated = date_updated
+        self.callback_id = callback_id
+        self.name = name
+        self.updated_by = updated_by
+        self.step_configuration = None
+        if step_configuration is not None:
+            self.step_configuration = []
+            for a in step_configuration:
+                if isinstance(a, dict):
+                    self.step_configuration.append(WorkflowV2StepConfiguration(**a))
+                else:
+                    self.step_configuration.append(a)
+        self.unknown_fields = kwargs
+
+
+

Class variables

+
+
var app_id : str | None
+
+

The type of the None singleton.

+
+
var callback_id : str | None
+
+

The type of the None singleton.

+
+
var date_updated : int | None
+
+

The type of the None singleton.

+
+
var id : str | None
+
+

The type of the None singleton.

+
+
var name : str | None
+
+

The type of the None singleton.

+
+
var step_configuration : List[WorkflowV2StepConfiguration] | None
+
+

The type of the None singleton.

+
+
var unknown_fields : Dict[str, Any]
+
+

The type of the None singleton.

+
+
var updated_by : str | None
+
+

The type of the None singleton.

+
+
+
+
+class WorkflowV2StepConfiguration +(*,
name: str | None = None,
step_function_type: str | None = None,
step_function_app_id: str | None = None,
**kwargs)
+
+
+
+ +Expand source code + +
class WorkflowV2StepConfiguration:
+    name: Optional[str]
+    step_function_type: Optional[str]
+    step_function_app_id: Optional[str]
+    unknown_fields: Dict[str, Any]
+
+    def __init__(
+        self,
+        *,
+        name: Optional[str] = None,
+        step_function_type: Optional[str] = None,
+        step_function_app_id: Optional[str] = None,
+        **kwargs,
+    ) -> None:
+        self.name = name
+        self.step_function_type = step_function_type
+        self.step_function_app_id = step_function_app_id
+        self.unknown_fields = kwargs
+
+
+

Class variables

+
+
var name : str | None
+
+

The type of the None singleton.

+
+
var step_function_app_id : str | None
+
+

The type of the None singleton.

+
+
var step_function_type : str | None
+
+

The type of the None singleton.

+
+
var unknown_fields : Dict[str, Any]
+
+

The type of the None singleton.

+
+
+
+
+
+
+ +
+ + + diff --git a/docs/reference/audit_logs/v1/response.html b/docs/reference/audit_logs/v1/response.html new file mode 100644 index 000000000..7d5c72853 --- /dev/null +++ b/docs/reference/audit_logs/v1/response.html @@ -0,0 +1,163 @@ + + + + + + +slack_sdk.audit_logs.v1.response API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.audit_logs.v1.response

+
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class AuditLogsResponse +(*, url: str, status_code: int, raw_body: str | None, headers: dict) +
+
+
+ +Expand source code + +
class AuditLogsResponse:
+    url: str
+    status_code: int
+    headers: Dict[str, Any]
+    raw_body: Optional[str]
+    body: Optional[Dict[str, Any]]
+    typed_body: Optional[LogsResponse]
+
+    @property  # type: ignore[no-redef]
+    def typed_body(self) -> Optional[LogsResponse]:
+        if self.body is None:
+            return None
+        return LogsResponse(**self.body)
+
+    def __init__(
+        self,
+        *,
+        url: str,
+        status_code: int,
+        raw_body: Optional[str],
+        headers: dict,
+    ):
+        self.url = url
+        self.status_code = status_code
+        self.headers = headers
+        self.raw_body = raw_body
+        self.body = json.loads(raw_body) if raw_body is not None and raw_body.startswith("{") else None
+
+
+

Class variables

+
+
var body : Dict[str, Any] | None
+
+

The type of the None singleton.

+
+
var headers : Dict[str, Any]
+
+

The type of the None singleton.

+
+
var raw_body : str | None
+
+

The type of the None singleton.

+
+
var status_code : int
+
+

The type of the None singleton.

+
+
var url : str
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
prop typed_bodyLogsResponse | None
+
+
+ +Expand source code + +
@property  # type: ignore[no-redef]
+def typed_body(self) -> Optional[LogsResponse]:
+    if self.body is None:
+        return None
+    return LogsResponse(**self.body)
+
+
+
+
+
+
+
+
+ +
+ + + diff --git a/docs/reference/errors/index.html b/docs/reference/errors/index.html new file mode 100644 index 000000000..569a2608e --- /dev/null +++ b/docs/reference/errors/index.html @@ -0,0 +1,315 @@ + + + + + + +slack_sdk.errors API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.errors

+
+
+

Errors that can be raised by this SDK

+
+
+
+
+
+
+
+
+

Classes

+
+
+class BotUserAccessError +(*args, **kwargs) +
+
+
+ +Expand source code + +
class BotUserAccessError(SlackClientError):
+    """Error raised when an 'xoxb-*' token is
+    being used for a Slack API method that only accepts 'xoxp-*' tokens.
+    """
+
+

Error raised when an 'xoxb-' token is +being used for a Slack API method that only accepts 'xoxp-' tokens.

+

Ancestors

+ +
+
+class SlackApiError +(message, response) +
+
+
+ +Expand source code + +
class SlackApiError(SlackClientError):
+    """Error raised when Slack does not send the expected response.
+
+    Attributes:
+        response (SlackResponse): The SlackResponse object containing all of the data sent back from the API.
+
+    Note:
+        The message (str) passed into the exception is used when
+        a user converts the exception to a str.
+        i.e. str(SlackApiError("This text will be sent as a string."))
+    """
+
+    def __init__(self, message, response):
+        msg = f"{message}\nThe server responded with: {response}"
+        self.response = response
+        super(SlackApiError, self).__init__(msg)
+
+

Error raised when Slack does not send the expected response.

+

Attributes

+
+
response : SlackResponse
+
The SlackResponse object containing all of the data sent back from the API.
+
+

Note

+

The message (str) passed into the exception is used when +a user converts the exception to a str. +i.e. str(SlackApiError("This text will be sent as a string."))

+

Ancestors

+ +
+
+class SlackClientConfigurationError +(*args, **kwargs) +
+
+
+ +Expand source code + +
class SlackClientConfigurationError(SlackClientError):
+    """Error raised because of invalid configuration on the client side:
+    * when attempting to send messages over the websocket when the connection is closed.
+    * when external system (e.g., Amazon S3) configuration / credentials are not correct
+    """
+
+

Error raised because of invalid configuration on the client side: +* when attempting to send messages over the websocket when the connection is closed. +* when external system (e.g., Amazon S3) configuration / credentials are not correct

+

Ancestors

+ +
+
+class SlackClientError +(*args, **kwargs) +
+
+
+ +Expand source code + +
class SlackClientError(Exception):
+    """Base class for Client errors"""
+
+

Base class for Client errors

+

Ancestors

+
    +
  • builtins.Exception
  • +
  • builtins.BaseException
  • +
+

Subclasses

+ +
+
+class SlackClientNotConnectedError +(*args, **kwargs) +
+
+
+ +Expand source code + +
class SlackClientNotConnectedError(SlackClientError):
+    """Error raised when attempting to send messages over the websocket when the
+    connection is closed."""
+
+

Error raised when attempting to send messages over the websocket when the +connection is closed.

+

Ancestors

+ +
+
+class SlackObjectFormationError +(*args, **kwargs) +
+
+
+ +Expand source code + +
class SlackObjectFormationError(SlackClientError):
+    """Error raised when a constructed object is not valid/malformed"""
+
+

Error raised when a constructed object is not valid/malformed

+

Ancestors

+ +
+
+class SlackRequestError +(*args, **kwargs) +
+
+
+ +Expand source code + +
class SlackRequestError(SlackClientError):
+    """Error raised when there's a problem with the request that's being submitted."""
+
+

Error raised when there's a problem with the request that's being submitted.

+

Ancestors

+ +
+
+class SlackTokenRotationError +(api_error: SlackApiError) +
+
+
+ +Expand source code + +
class SlackTokenRotationError(SlackClientError):
+    """Error raised when the oauth.v2.access call for token rotation fails"""
+
+    api_error: SlackApiError
+
+    def __init__(self, api_error: SlackApiError):
+        self.api_error = api_error
+
+

Error raised when the oauth.v2.access call for token rotation fails

+

Ancestors

+ +

Class variables

+
+
var api_errorSlackApiError
+
+

The type of the None singleton.

+
+
+
+
+
+
+ +
+ + + diff --git a/docs/reference/http_retry/async_handler.html b/docs/reference/http_retry/async_handler.html new file mode 100644 index 000000000..4f3889fcc --- /dev/null +++ b/docs/reference/http_retry/async_handler.html @@ -0,0 +1,431 @@ + + + + + + +slack_sdk.http_retry.async_handler API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.http_retry.async_handler

+
+
+

asyncio compatible RetryHandler interface. +You can pass an array of handlers to customize retry logics in supported API clients.

+
+
+
+
+
+
+
+
+

Classes

+
+
+class BackoffRetryIntervalCalculator +(backoff_factor: float = 0.5,
jitter: Jitter | None = None)
+
+
+
+ +Expand source code + +
class BackoffRetryIntervalCalculator(RetryIntervalCalculator):
+    """Retry interval calculator that calculates in the manner of Exponential Backoff And Jitter
+    see also: https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/
+    """
+
+    backoff_factor: float
+    jitter: Jitter
+
+    def __init__(self, backoff_factor: float = 0.5, jitter: Optional[Jitter] = None):
+        """Retry interval calculator that calculates in the manner of Exponential Backoff And Jitter
+
+        Args:
+            backoff_factor: The factor for the backoff interval calculation
+            jitter: The jitter logic implementation
+        """
+        self.backoff_factor = backoff_factor
+        self.jitter = jitter if jitter is not None else RandomJitter()
+
+    def calculate_sleep_duration(self, current_attempt: int) -> float:
+        interval = self.backoff_factor * (2 ** (current_attempt))
+        sleep_duration = self.jitter.recalculate(interval)
+        return sleep_duration
+
+

Retry interval calculator that calculates in the manner of Exponential Backoff And Jitter +see also: https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/

+

Retry interval calculator that calculates in the manner of Exponential Backoff And Jitter

+

Args

+
+
backoff_factor
+
The factor for the backoff interval calculation
+
jitter
+
The jitter logic implementation
+
+

Ancestors

+ +

Class variables

+
+
var backoff_factor : float
+
+

The type of the None singleton.

+
+
var jitterJitter
+
+

The type of the None singleton.

+
+
+

Inherited members

+ +
+
+class HttpRequest +(*,
method: str,
url: str,
headers: Dict[str, str | List[str]],
body_params: Dict[str, Any] | None = None,
data: bytes | None = None)
+
+
+
+ +Expand source code + +
class HttpRequest:
+    """HTTP request representation"""
+
+    method: str
+    url: str
+    headers: Dict[str, Union[str, List[str]]]
+    body_params: Optional[Dict[str, Any]]
+    data: Optional[bytes]
+
+    def __init__(
+        self,
+        *,
+        method: str,
+        url: str,
+        headers: Dict[str, Union[str, List[str]]],
+        body_params: Optional[Dict[str, Any]] = None,
+        data: Optional[bytes] = None,
+    ):
+        self.method = method
+        self.url = url
+        self.headers = {k: v if isinstance(v, list) else [v] for k, v in headers.items()}
+        self.body_params = body_params
+        self.data = data
+
+    @classmethod
+    def from_urllib_http_request(cls, req: Request) -> "HttpRequest":
+        return HttpRequest(
+            method=req.method,  # type: ignore[arg-type]
+            url=req.full_url,
+            headers={k: v if isinstance(v, list) else [v] for k, v in req.headers.items()},
+            data=req.data,  # type: ignore[arg-type]
+        )
+
+

HTTP request representation

+

Class variables

+
+
var body_params : Dict[str, Any] | None
+
+

The type of the None singleton.

+
+
var data : bytes | None
+
+

The type of the None singleton.

+
+
var headers : Dict[str, str | List[str]]
+
+

The type of the None singleton.

+
+
var method : str
+
+

The type of the None singleton.

+
+
var url : str
+
+

The type of the None singleton.

+
+
+

Static methods

+
+
+def from_urllib_http_request(req: urllib.request.Request) ‑> HttpRequest +
+
+
+
+
+
+
+class HttpResponse +(*,
status_code: int | str,
headers: Dict[str, str | List[str]],
body: Dict[str, Any] | None = None,
data: bytes | None = None)
+
+
+
+ +Expand source code + +
class HttpResponse:
+    """HTTP response representation"""
+
+    status_code: int
+    headers: Dict[str, Union[List[str], str]]
+    body: Optional[Dict[str, Any]]
+    data: Optional[bytes]
+
+    def __init__(
+        self,
+        *,
+        status_code: Union[int, str],
+        headers: Dict[str, Union[str, List[str]]],
+        body: Optional[Dict[str, Any]] = None,
+        data: Optional[bytes] = None,
+    ):
+        self.status_code = int(status_code)
+        self.headers = {k: v if isinstance(v, list) else [v] for k, v in headers.items()}
+        self.body = body
+        self.data = data
+
+

HTTP response representation

+

Class variables

+
+
var body : Dict[str, Any] | None
+
+

The type of the None singleton.

+
+
var data : bytes | None
+
+

The type of the None singleton.

+
+
var headers : Dict[str, str | List[str]]
+
+

The type of the None singleton.

+
+
var status_code : int
+
+

The type of the None singleton.

+
+
+
+
+class RetryIntervalCalculator +
+
+
+ +Expand source code + +
class RetryIntervalCalculator:
+    """Retry interval calculator interface."""
+
+    def calculate_sleep_duration(self, current_attempt: int) -> float:
+        """Calculates an interval duration in seconds.
+
+        Args:
+            current_attempt: the number of the current attempt (zero-origin; 0 means no retries are done so far)
+        Returns:
+            calculated interval duration in seconds
+        """
+        raise NotImplementedError()
+
+

Retry interval calculator interface.

+

Subclasses

+ +

Methods

+
+
+def calculate_sleep_duration(self, current_attempt: int) ‑> float +
+
+
+ +Expand source code + +
def calculate_sleep_duration(self, current_attempt: int) -> float:
+    """Calculates an interval duration in seconds.
+
+    Args:
+        current_attempt: the number of the current attempt (zero-origin; 0 means no retries are done so far)
+    Returns:
+        calculated interval duration in seconds
+    """
+    raise NotImplementedError()
+
+

Calculates an interval duration in seconds.

+

Args

+
+
current_attempt
+
the number of the current attempt (zero-origin; 0 means no retries are done so far)
+
+

Returns

+

calculated interval duration in seconds

+
+
+
+
+class RetryState +(*, current_attempt: int = 0, custom_values: Dict[str, Any] | None = None) +
+
+
+ +Expand source code + +
class RetryState:
+    next_attempt_requested: bool
+    current_attempt: int  # zero-origin
+    custom_values: Optional[Dict[str, Any]]
+
+    def __init__(
+        self,
+        *,
+        current_attempt: int = 0,
+        custom_values: Optional[Dict[str, Any]] = None,
+    ):
+        self.next_attempt_requested = False
+        self.current_attempt = current_attempt
+        self.custom_values = custom_values
+
+    def increment_current_attempt(self) -> int:
+        self.current_attempt += 1
+        return self.current_attempt
+
+
+

Class variables

+
+
var current_attempt : int
+
+

The type of the None singleton.

+
+
var custom_values : Dict[str, Any] | None
+
+

The type of the None singleton.

+
+
var next_attempt_requested : bool
+
+

The type of the None singleton.

+
+
+

Methods

+
+
+def increment_current_attempt(self) ‑> int +
+
+
+ +Expand source code + +
def increment_current_attempt(self) -> int:
+    self.current_attempt += 1
+    return self.current_attempt
+
+
+
+
+
+
+
+
+ +
+ + + diff --git a/docs/reference/http_retry/builtin_async_handlers.html b/docs/reference/http_retry/builtin_async_handlers.html new file mode 100644 index 000000000..206b27f53 --- /dev/null +++ b/docs/reference/http_retry/builtin_async_handlers.html @@ -0,0 +1,306 @@ + + + + + + +slack_sdk.http_retry.builtin_async_handlers API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.http_retry.builtin_async_handlers

+
+
+
+
+
+
+
+
+

Functions

+
+
+def async_default_handlers() ‑> List[slack_sdk.http_retry.async_handler.AsyncRetryHandler] +
+
+
+ +Expand source code + +
def async_default_handlers() -> List[AsyncRetryHandler]:
+    return [AsyncConnectionErrorRetryHandler()]
+
+
+
+
+
+
+

Classes

+
+
+class AsyncConnectionErrorRetryHandler +(max_retry_count: int = 1,
interval_calculator: RetryIntervalCalculator = <slack_sdk.http_retry.builtin_interval_calculators.BackoffRetryIntervalCalculator object>,
error_types: List[Type[Exception]] = [<class 'aiohttp.client_exceptions.ServerConnectionError'>, <class 'aiohttp.client_exceptions.ServerDisconnectedError'>, <class 'aiohttp.client_exceptions.ClientOSError'>])
+
+
+
+ +Expand source code + +
class AsyncConnectionErrorRetryHandler(AsyncRetryHandler):
+    """RetryHandler that does retries for connectivity issues."""
+
+    def __init__(
+        self,
+        max_retry_count: int = 1,
+        interval_calculator: RetryIntervalCalculator = default_interval_calculator,
+        error_types: List[Type[Exception]] = [
+            ServerConnectionError,
+            ServerDisconnectedError,
+            # ClientOSError: [Errno 104] Connection reset by peer
+            ClientOSError,
+        ],
+    ):
+        super().__init__(max_retry_count, interval_calculator)
+        self.error_types_to_do_retries = error_types
+
+    async def _can_retry_async(
+        self,
+        *,
+        state: RetryState,
+        request: HttpRequest,
+        response: Optional[HttpResponse] = None,
+        error: Optional[Exception] = None,
+    ) -> bool:
+        if error is None:
+            return False
+
+        for error_type in self.error_types_to_do_retries:
+            if isinstance(error, error_type):
+                return True
+        return False
+
+

RetryHandler that does retries for connectivity issues.

+

RetryHandler interface.

+

Args

+
+
max_retry_count
+
The maximum times to do retries
+
interval_calculator
+
Pass an interval calculator for customizing the logic
+
+

Ancestors

+
    +
  • slack_sdk.http_retry.async_handler.AsyncRetryHandler
  • +
+
+
+class AsyncRateLimitErrorRetryHandler +(max_retry_count: int = 1,
interval_calculator: RetryIntervalCalculator = <slack_sdk.http_retry.builtin_interval_calculators.BackoffRetryIntervalCalculator object>)
+
+
+
+ +Expand source code + +
class AsyncRateLimitErrorRetryHandler(AsyncRetryHandler):
+    """RetryHandler that does retries for rate limited errors."""
+
+    async def _can_retry_async(
+        self,
+        *,
+        state: RetryState,
+        request: HttpRequest,
+        response: Optional[HttpResponse] = None,
+        error: Optional[Exception] = None,
+    ) -> bool:
+        return response is not None and response.status_code == 429
+
+    async def prepare_for_next_attempt_async(
+        self,
+        *,
+        state: RetryState,
+        request: HttpRequest,
+        response: Optional[HttpResponse] = None,
+        error: Optional[Exception] = None,
+    ) -> None:
+        if response is None:
+            raise error  # type: ignore[misc]
+
+        state.next_attempt_requested = True
+        retry_after_header_name: Optional[str] = None
+        for k in response.headers.keys():
+            if k.lower() == "retry-after":
+                retry_after_header_name = k
+                break
+        duration = 1
+        if retry_after_header_name is None:
+            # This situation usually does not arise. Just in case.
+            duration += random.random()  # type: ignore[assignment]
+        else:
+            duration = int(response.headers.get(retry_after_header_name)[0]) + random.random()  # type: ignore[assignment, index] # noqa: E501
+        await asyncio.sleep(duration)
+        state.increment_current_attempt()
+
+

RetryHandler that does retries for rate limited errors.

+

RetryHandler interface.

+

Args

+
+
max_retry_count
+
The maximum times to do retries
+
interval_calculator
+
Pass an interval calculator for customizing the logic
+
+

Ancestors

+
    +
  • slack_sdk.http_retry.async_handler.AsyncRetryHandler
  • +
+

Methods

+
+
+async def prepare_for_next_attempt_async(self,
*,
state: RetryState,
request: HttpRequest,
response: HttpResponse | None = None,
error: Exception | None = None) ‑> None
+
+
+
+ +Expand source code + +
async def prepare_for_next_attempt_async(
+    self,
+    *,
+    state: RetryState,
+    request: HttpRequest,
+    response: Optional[HttpResponse] = None,
+    error: Optional[Exception] = None,
+) -> None:
+    if response is None:
+        raise error  # type: ignore[misc]
+
+    state.next_attempt_requested = True
+    retry_after_header_name: Optional[str] = None
+    for k in response.headers.keys():
+        if k.lower() == "retry-after":
+            retry_after_header_name = k
+            break
+    duration = 1
+    if retry_after_header_name is None:
+        # This situation usually does not arise. Just in case.
+        duration += random.random()  # type: ignore[assignment]
+    else:
+        duration = int(response.headers.get(retry_after_header_name)[0]) + random.random()  # type: ignore[assignment, index] # noqa: E501
+    await asyncio.sleep(duration)
+    state.increment_current_attempt()
+
+
+
+
+
+
+class AsyncServerErrorRetryHandler +(max_retry_count: int = 1,
interval_calculator: RetryIntervalCalculator = <slack_sdk.http_retry.builtin_interval_calculators.BackoffRetryIntervalCalculator object>)
+
+
+
+ +Expand source code + +
class AsyncServerErrorRetryHandler(AsyncRetryHandler):
+    """RetryHandler that does retries for server errors."""
+
+    def __init__(
+        self,
+        max_retry_count: int = 1,
+        interval_calculator: RetryIntervalCalculator = default_interval_calculator,
+    ):
+        super().__init__(max_retry_count, interval_calculator)
+
+    async def _can_retry_async(
+        self,
+        *,
+        state: RetryState,
+        request: HttpRequest,
+        response: Optional[HttpResponse] = None,
+        error: Optional[Exception] = None,
+    ) -> bool:
+        return response is not None and response.status_code in [500, 503]
+
+

RetryHandler that does retries for server errors.

+

RetryHandler interface.

+

Args

+
+
max_retry_count
+
The maximum times to do retries
+
interval_calculator
+
Pass an interval calculator for customizing the logic
+
+

Ancestors

+
    +
  • slack_sdk.http_retry.async_handler.AsyncRetryHandler
  • +
+
+
+
+
+ +
+ + + diff --git a/docs/reference/http_retry/builtin_handlers.html b/docs/reference/http_retry/builtin_handlers.html new file mode 100644 index 000000000..0f36601ed --- /dev/null +++ b/docs/reference/http_retry/builtin_handlers.html @@ -0,0 +1,316 @@ + + + + + + +slack_sdk.http_retry.builtin_handlers API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.http_retry.builtin_handlers

+
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class ConnectionErrorRetryHandler +(max_retry_count: int = 1,
interval_calculator: RetryIntervalCalculator = <slack_sdk.http_retry.builtin_interval_calculators.BackoffRetryIntervalCalculator object>,
error_types: List[Type[Exception]] = [<class 'urllib.error.URLError'>, <class 'ConnectionResetError'>, <class 'http.client.RemoteDisconnected'>])
+
+
+
+ +Expand source code + +
class ConnectionErrorRetryHandler(RetryHandler):
+    """RetryHandler that does retries for connectivity issues."""
+
+    def __init__(
+        self,
+        max_retry_count: int = 1,
+        interval_calculator: RetryIntervalCalculator = default_interval_calculator,
+        error_types: List[Type[Exception]] = [
+            # To cover URLError: <urlopen error [Errno 104] Connection reset by peer>
+            URLError,
+            ConnectionResetError,
+            RemoteDisconnected,
+        ],
+    ):
+        super().__init__(max_retry_count, interval_calculator)
+        self.error_types_to_do_retries = error_types
+
+    def _can_retry(
+        self,
+        *,
+        state: RetryState,
+        request: HttpRequest,
+        response: Optional[HttpResponse] = None,
+        error: Optional[Exception] = None,
+    ) -> bool:
+        if error is None:
+            return False
+
+        if isinstance(error, URLError):
+            if response is not None:
+                return False  # status 40x
+
+        for error_type in self.error_types_to_do_retries:
+            if isinstance(error, error_type):
+                return True
+        return False
+
+

RetryHandler that does retries for connectivity issues.

+

RetryHandler interface.

+

Args

+
+
max_retry_count
+
The maximum times to do retries
+
interval_calculator
+
Pass an interval calculator for customizing the logic
+
+

Ancestors

+ +

Inherited members

+ +
+
+class RateLimitErrorRetryHandler +(max_retry_count: int = 1,
interval_calculator: RetryIntervalCalculator = <slack_sdk.http_retry.builtin_interval_calculators.BackoffRetryIntervalCalculator object>)
+
+
+
+ +Expand source code + +
class RateLimitErrorRetryHandler(RetryHandler):
+    """RetryHandler that does retries for rate limited errors."""
+
+    def _can_retry(
+        self,
+        *,
+        state: RetryState,
+        request: HttpRequest,
+        response: Optional[HttpResponse] = None,
+        error: Optional[Exception] = None,
+    ) -> bool:
+        return response is not None and response.status_code == 429
+
+    def prepare_for_next_attempt(
+        self,
+        *,
+        state: RetryState,
+        request: HttpRequest,
+        response: Optional[HttpResponse] = None,
+        error: Optional[Exception] = None,
+    ) -> None:
+        if response is None:
+            raise error  # type: ignore[misc]
+
+        state.next_attempt_requested = True
+        retry_after_header_name: Optional[str] = None
+        for k in response.headers.keys():
+            if k.lower() == "retry-after":
+                retry_after_header_name = k
+                break
+        duration = 1
+        if retry_after_header_name is None:
+            # This situation usually does not arise. Just in case.
+            duration += random.random()  # type: ignore[assignment]
+        else:
+            duration = int(response.headers.get(retry_after_header_name)[0]) + random.random()  # type: ignore[index, assignment] # noqa: E501
+        time.sleep(duration)
+        state.increment_current_attempt()
+
+

RetryHandler that does retries for rate limited errors.

+

RetryHandler interface.

+

Args

+
+
max_retry_count
+
The maximum times to do retries
+
interval_calculator
+
Pass an interval calculator for customizing the logic
+
+

Ancestors

+ +

Methods

+
+
+def prepare_for_next_attempt(self,
*,
state: RetryState,
request: HttpRequest,
response: HttpResponse | None = None,
error: Exception | None = None) ‑> None
+
+
+
+ +Expand source code + +
def prepare_for_next_attempt(
+    self,
+    *,
+    state: RetryState,
+    request: HttpRequest,
+    response: Optional[HttpResponse] = None,
+    error: Optional[Exception] = None,
+) -> None:
+    if response is None:
+        raise error  # type: ignore[misc]
+
+    state.next_attempt_requested = True
+    retry_after_header_name: Optional[str] = None
+    for k in response.headers.keys():
+        if k.lower() == "retry-after":
+            retry_after_header_name = k
+            break
+    duration = 1
+    if retry_after_header_name is None:
+        # This situation usually does not arise. Just in case.
+        duration += random.random()  # type: ignore[assignment]
+    else:
+        duration = int(response.headers.get(retry_after_header_name)[0]) + random.random()  # type: ignore[index, assignment] # noqa: E501
+    time.sleep(duration)
+    state.increment_current_attempt()
+
+
+
+
+

Inherited members

+ +
+
+class ServerErrorRetryHandler +(max_retry_count: int = 1,
interval_calculator: RetryIntervalCalculator = <slack_sdk.http_retry.builtin_interval_calculators.BackoffRetryIntervalCalculator object>)
+
+
+
+ +Expand source code + +
class ServerErrorRetryHandler(RetryHandler):
+    """RetryHandler that does retries for server errors."""
+
+    def __init__(
+        self,
+        max_retry_count: int = 1,
+        interval_calculator: RetryIntervalCalculator = default_interval_calculator,
+    ):
+        super().__init__(max_retry_count, interval_calculator)
+
+    def _can_retry(
+        self,
+        *,
+        state: RetryState,
+        request: HttpRequest,
+        response: Optional[HttpResponse] = None,
+        error: Optional[Exception] = None,
+    ) -> bool:
+        return response is not None and response.status_code in [500, 503]
+
+

RetryHandler that does retries for server errors.

+

RetryHandler interface.

+

Args

+
+
max_retry_count
+
The maximum times to do retries
+
interval_calculator
+
Pass an interval calculator for customizing the logic
+
+

Ancestors

+ +

Inherited members

+ +
+
+
+
+ +
+ + + diff --git a/docs/reference/http_retry/builtin_interval_calculators.html b/docs/reference/http_retry/builtin_interval_calculators.html new file mode 100644 index 000000000..cfa90cc7c --- /dev/null +++ b/docs/reference/http_retry/builtin_interval_calculators.html @@ -0,0 +1,204 @@ + + + + + + +slack_sdk.http_retry.builtin_interval_calculators API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.http_retry.builtin_interval_calculators

+
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class BackoffRetryIntervalCalculator +(backoff_factor: float = 0.5,
jitter: Jitter | None = None)
+
+
+
+ +Expand source code + +
class BackoffRetryIntervalCalculator(RetryIntervalCalculator):
+    """Retry interval calculator that calculates in the manner of Exponential Backoff And Jitter
+    see also: https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/
+    """
+
+    backoff_factor: float
+    jitter: Jitter
+
+    def __init__(self, backoff_factor: float = 0.5, jitter: Optional[Jitter] = None):
+        """Retry interval calculator that calculates in the manner of Exponential Backoff And Jitter
+
+        Args:
+            backoff_factor: The factor for the backoff interval calculation
+            jitter: The jitter logic implementation
+        """
+        self.backoff_factor = backoff_factor
+        self.jitter = jitter if jitter is not None else RandomJitter()
+
+    def calculate_sleep_duration(self, current_attempt: int) -> float:
+        interval = self.backoff_factor * (2 ** (current_attempt))
+        sleep_duration = self.jitter.recalculate(interval)
+        return sleep_duration
+
+

Retry interval calculator that calculates in the manner of Exponential Backoff And Jitter +see also: https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/

+

Retry interval calculator that calculates in the manner of Exponential Backoff And Jitter

+

Args

+
+
backoff_factor
+
The factor for the backoff interval calculation
+
jitter
+
The jitter logic implementation
+
+

Ancestors

+ +

Class variables

+
+
var backoff_factor : float
+
+

The type of the None singleton.

+
+
var jitterJitter
+
+

The type of the None singleton.

+
+
+

Inherited members

+ +
+
+class FixedValueRetryIntervalCalculator +(fixed_internal: float = 0.5) +
+
+
+ +Expand source code + +
class FixedValueRetryIntervalCalculator(RetryIntervalCalculator):
+    """Retry interval calculator that uses a fixed value."""
+
+    fixed_interval: float
+
+    def __init__(self, fixed_internal: float = 0.5):
+        """Retry interval calculator that uses a fixed value.
+
+        Args:
+            fixed_internal: The fixed interval seconds
+        """
+        self.fixed_interval = fixed_internal
+
+    def calculate_sleep_duration(self, current_attempt: int) -> float:
+        return self.fixed_interval
+
+

Retry interval calculator that uses a fixed value.

+

Retry interval calculator that uses a fixed value.

+

Args

+
+
fixed_internal
+
The fixed interval seconds
+
+

Ancestors

+ +

Class variables

+
+
var fixed_interval : float
+
+

The type of the None singleton.

+
+
+

Inherited members

+ +
+
+
+
+ +
+ + + diff --git a/docs/reference/http_retry/handler.html b/docs/reference/http_retry/handler.html new file mode 100644 index 000000000..8f4cb0f08 --- /dev/null +++ b/docs/reference/http_retry/handler.html @@ -0,0 +1,237 @@ + + + + + + +slack_sdk.http_retry.handler API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.http_retry.handler

+
+
+

RetryHandler interface. +You can pass an array of handlers to customize retry logics in supported API clients.

+
+
+
+
+
+
+
+
+

Classes

+
+
+class RetryHandler +(max_retry_count: int = 1,
interval_calculator: RetryIntervalCalculator = <slack_sdk.http_retry.builtin_interval_calculators.BackoffRetryIntervalCalculator object>)
+
+
+
+ +Expand source code + +
class RetryHandler:
+    """RetryHandler interface.
+    You can pass an array of handlers to customize retry logics in supported API clients.
+    """
+
+    max_retry_count: int
+    interval_calculator: RetryIntervalCalculator
+
+    def __init__(
+        self,
+        max_retry_count: int = 1,
+        interval_calculator: RetryIntervalCalculator = default_interval_calculator,
+    ):
+        """RetryHandler interface.
+
+        Args:
+            max_retry_count: The maximum times to do retries
+            interval_calculator: Pass an interval calculator for customizing the logic
+        """
+        self.max_retry_count = max_retry_count
+        self.interval_calculator = interval_calculator
+
+    def can_retry(
+        self,
+        *,
+        state: RetryState,
+        request: HttpRequest,
+        response: Optional[HttpResponse] = None,
+        error: Optional[Exception] = None,
+    ) -> bool:
+        if state.current_attempt >= self.max_retry_count:
+            return False
+        return self._can_retry(
+            state=state,
+            request=request,
+            response=response,
+            error=error,
+        )
+
+    def _can_retry(
+        self,
+        *,
+        state: RetryState,
+        request: HttpRequest,
+        response: Optional[HttpResponse] = None,
+        error: Optional[Exception] = None,
+    ) -> bool:
+        raise NotImplementedError()
+
+    def prepare_for_next_attempt(
+        self,
+        *,
+        state: RetryState,
+        request: HttpRequest,
+        response: Optional[HttpResponse] = None,
+        error: Optional[Exception] = None,
+    ) -> None:
+        state.next_attempt_requested = True
+        duration = self.interval_calculator.calculate_sleep_duration(state.current_attempt)
+        time.sleep(duration)
+        state.increment_current_attempt()
+
+

RetryHandler interface. +You can pass an array of handlers to customize retry logics in supported API clients.

+

RetryHandler interface.

+

Args

+
+
max_retry_count
+
The maximum times to do retries
+
interval_calculator
+
Pass an interval calculator for customizing the logic
+
+

Subclasses

+ +

Class variables

+
+
var interval_calculatorRetryIntervalCalculator
+
+

The type of the None singleton.

+
+
var max_retry_count : int
+
+

The type of the None singleton.

+
+
+

Methods

+
+
+def can_retry(self,
*,
state: RetryState,
request: HttpRequest,
response: HttpResponse | None = None,
error: Exception | None = None) ‑> bool
+
+
+
+ +Expand source code + +
def can_retry(
+    self,
+    *,
+    state: RetryState,
+    request: HttpRequest,
+    response: Optional[HttpResponse] = None,
+    error: Optional[Exception] = None,
+) -> bool:
+    if state.current_attempt >= self.max_retry_count:
+        return False
+    return self._can_retry(
+        state=state,
+        request=request,
+        response=response,
+        error=error,
+    )
+
+
+
+
+def prepare_for_next_attempt(self,
*,
state: RetryState,
request: HttpRequest,
response: HttpResponse | None = None,
error: Exception | None = None) ‑> None
+
+
+
+ +Expand source code + +
def prepare_for_next_attempt(
+    self,
+    *,
+    state: RetryState,
+    request: HttpRequest,
+    response: Optional[HttpResponse] = None,
+    error: Optional[Exception] = None,
+) -> None:
+    state.next_attempt_requested = True
+    duration = self.interval_calculator.calculate_sleep_duration(state.current_attempt)
+    time.sleep(duration)
+    state.increment_current_attempt()
+
+
+
+
+
+
+
+
+ +
+ + + diff --git a/docs/reference/http_retry/index.html b/docs/reference/http_retry/index.html new file mode 100644 index 000000000..501a62c9e --- /dev/null +++ b/docs/reference/http_retry/index.html @@ -0,0 +1,1000 @@ + + + + + + +slack_sdk.http_retry API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.http_retry

+
+
+
+
+

Sub-modules

+
+
slack_sdk.http_retry.async_handler
+
+

asyncio compatible RetryHandler interface. +You can pass an array of handlers to customize retry logics in supported API clients.

+
+
slack_sdk.http_retry.builtin_async_handlers
+
+
+
+
slack_sdk.http_retry.builtin_handlers
+
+
+
+
slack_sdk.http_retry.builtin_interval_calculators
+
+
+
+
slack_sdk.http_retry.handler
+
+

RetryHandler interface. +You can pass an array of handlers to customize retry logics in supported API clients.

+
+
slack_sdk.http_retry.interval_calculator
+
+
+
+
slack_sdk.http_retry.jitter
+
+
+
+
slack_sdk.http_retry.request
+
+
+
+
slack_sdk.http_retry.response
+
+
+
+
slack_sdk.http_retry.state
+
+
+
+
+
+
+
+
+

Functions

+
+
+def all_builtin_retry_handlers() ‑> List[RetryHandler] +
+
+
+ +Expand source code + +
def all_builtin_retry_handlers() -> List[RetryHandler]:
+    return [
+        connect_error_retry_handler,
+        rate_limit_error_retry_handler,
+    ]
+
+
+
+
+def default_retry_handlers() ‑> List[RetryHandler] +
+
+
+ +Expand source code + +
def default_retry_handlers() -> List[RetryHandler]:
+    return [connect_error_retry_handler]
+
+
+
+
+
+
+

Classes

+
+
+class BackoffRetryIntervalCalculator +(backoff_factor: float = 0.5,
jitter: Jitter | None = None)
+
+
+
+ +Expand source code + +
class BackoffRetryIntervalCalculator(RetryIntervalCalculator):
+    """Retry interval calculator that calculates in the manner of Exponential Backoff And Jitter
+    see also: https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/
+    """
+
+    backoff_factor: float
+    jitter: Jitter
+
+    def __init__(self, backoff_factor: float = 0.5, jitter: Optional[Jitter] = None):
+        """Retry interval calculator that calculates in the manner of Exponential Backoff And Jitter
+
+        Args:
+            backoff_factor: The factor for the backoff interval calculation
+            jitter: The jitter logic implementation
+        """
+        self.backoff_factor = backoff_factor
+        self.jitter = jitter if jitter is not None else RandomJitter()
+
+    def calculate_sleep_duration(self, current_attempt: int) -> float:
+        interval = self.backoff_factor * (2 ** (current_attempt))
+        sleep_duration = self.jitter.recalculate(interval)
+        return sleep_duration
+
+

Retry interval calculator that calculates in the manner of Exponential Backoff And Jitter +see also: https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/

+

Retry interval calculator that calculates in the manner of Exponential Backoff And Jitter

+

Args

+
+
backoff_factor
+
The factor for the backoff interval calculation
+
jitter
+
The jitter logic implementation
+
+

Ancestors

+ +

Class variables

+
+
var backoff_factor : float
+
+

The type of the None singleton.

+
+
var jitterJitter
+
+

The type of the None singleton.

+
+
+

Inherited members

+ +
+
+class ConnectionErrorRetryHandler +(max_retry_count: int = 1,
interval_calculator: RetryIntervalCalculator = <slack_sdk.http_retry.builtin_interval_calculators.BackoffRetryIntervalCalculator object>,
error_types: List[Type[Exception]] = [<class 'urllib.error.URLError'>, <class 'ConnectionResetError'>, <class 'http.client.RemoteDisconnected'>])
+
+
+
+ +Expand source code + +
class ConnectionErrorRetryHandler(RetryHandler):
+    """RetryHandler that does retries for connectivity issues."""
+
+    def __init__(
+        self,
+        max_retry_count: int = 1,
+        interval_calculator: RetryIntervalCalculator = default_interval_calculator,
+        error_types: List[Type[Exception]] = [
+            # To cover URLError: <urlopen error [Errno 104] Connection reset by peer>
+            URLError,
+            ConnectionResetError,
+            RemoteDisconnected,
+        ],
+    ):
+        super().__init__(max_retry_count, interval_calculator)
+        self.error_types_to_do_retries = error_types
+
+    def _can_retry(
+        self,
+        *,
+        state: RetryState,
+        request: HttpRequest,
+        response: Optional[HttpResponse] = None,
+        error: Optional[Exception] = None,
+    ) -> bool:
+        if error is None:
+            return False
+
+        if isinstance(error, URLError):
+            if response is not None:
+                return False  # status 40x
+
+        for error_type in self.error_types_to_do_retries:
+            if isinstance(error, error_type):
+                return True
+        return False
+
+

RetryHandler that does retries for connectivity issues.

+

RetryHandler interface.

+

Args

+
+
max_retry_count
+
The maximum times to do retries
+
interval_calculator
+
Pass an interval calculator for customizing the logic
+
+

Ancestors

+ +

Inherited members

+ +
+
+class FixedValueRetryIntervalCalculator +(fixed_internal: float = 0.5) +
+
+
+ +Expand source code + +
class FixedValueRetryIntervalCalculator(RetryIntervalCalculator):
+    """Retry interval calculator that uses a fixed value."""
+
+    fixed_interval: float
+
+    def __init__(self, fixed_internal: float = 0.5):
+        """Retry interval calculator that uses a fixed value.
+
+        Args:
+            fixed_internal: The fixed interval seconds
+        """
+        self.fixed_interval = fixed_internal
+
+    def calculate_sleep_duration(self, current_attempt: int) -> float:
+        return self.fixed_interval
+
+

Retry interval calculator that uses a fixed value.

+

Retry interval calculator that uses a fixed value.

+

Args

+
+
fixed_internal
+
The fixed interval seconds
+
+

Ancestors

+ +

Class variables

+
+
var fixed_interval : float
+
+

The type of the None singleton.

+
+
+

Inherited members

+ +
+
+class HttpRequest +(*,
method: str,
url: str,
headers: Dict[str, str | List[str]],
body_params: Dict[str, Any] | None = None,
data: bytes | None = None)
+
+
+
+ +Expand source code + +
class HttpRequest:
+    """HTTP request representation"""
+
+    method: str
+    url: str
+    headers: Dict[str, Union[str, List[str]]]
+    body_params: Optional[Dict[str, Any]]
+    data: Optional[bytes]
+
+    def __init__(
+        self,
+        *,
+        method: str,
+        url: str,
+        headers: Dict[str, Union[str, List[str]]],
+        body_params: Optional[Dict[str, Any]] = None,
+        data: Optional[bytes] = None,
+    ):
+        self.method = method
+        self.url = url
+        self.headers = {k: v if isinstance(v, list) else [v] for k, v in headers.items()}
+        self.body_params = body_params
+        self.data = data
+
+    @classmethod
+    def from_urllib_http_request(cls, req: Request) -> "HttpRequest":
+        return HttpRequest(
+            method=req.method,  # type: ignore[arg-type]
+            url=req.full_url,
+            headers={k: v if isinstance(v, list) else [v] for k, v in req.headers.items()},
+            data=req.data,  # type: ignore[arg-type]
+        )
+
+

HTTP request representation

+

Class variables

+
+
var body_params : Dict[str, Any] | None
+
+

The type of the None singleton.

+
+
var data : bytes | None
+
+

The type of the None singleton.

+
+
var headers : Dict[str, str | List[str]]
+
+

The type of the None singleton.

+
+
var method : str
+
+

The type of the None singleton.

+
+
var url : str
+
+

The type of the None singleton.

+
+
+

Static methods

+
+
+def from_urllib_http_request(req: urllib.request.Request) ‑> HttpRequest +
+
+
+
+
+
+
+class HttpResponse +(*,
status_code: int | str,
headers: Dict[str, str | List[str]],
body: Dict[str, Any] | None = None,
data: bytes | None = None)
+
+
+
+ +Expand source code + +
class HttpResponse:
+    """HTTP response representation"""
+
+    status_code: int
+    headers: Dict[str, Union[List[str], str]]
+    body: Optional[Dict[str, Any]]
+    data: Optional[bytes]
+
+    def __init__(
+        self,
+        *,
+        status_code: Union[int, str],
+        headers: Dict[str, Union[str, List[str]]],
+        body: Optional[Dict[str, Any]] = None,
+        data: Optional[bytes] = None,
+    ):
+        self.status_code = int(status_code)
+        self.headers = {k: v if isinstance(v, list) else [v] for k, v in headers.items()}
+        self.body = body
+        self.data = data
+
+

HTTP response representation

+

Class variables

+
+
var body : Dict[str, Any] | None
+
+

The type of the None singleton.

+
+
var data : bytes | None
+
+

The type of the None singleton.

+
+
var headers : Dict[str, str | List[str]]
+
+

The type of the None singleton.

+
+
var status_code : int
+
+

The type of the None singleton.

+
+
+
+
+class Jitter +
+
+
+ +Expand source code + +
class Jitter:
+    """Jitter interface"""
+
+    def recalculate(self, duration: float) -> float:
+        """Recalculate the given duration.
+        see also: https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/
+
+        Args:
+            duration: the duration in seconds
+
+        Returns:
+            A new duration that the jitter amount is added
+        """
+        raise NotImplementedError()
+
+

Jitter interface

+

Subclasses

+ +

Methods

+
+
+def recalculate(self, duration: float) ‑> float +
+
+
+ +Expand source code + +
def recalculate(self, duration: float) -> float:
+    """Recalculate the given duration.
+    see also: https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/
+
+    Args:
+        duration: the duration in seconds
+
+    Returns:
+        A new duration that the jitter amount is added
+    """
+    raise NotImplementedError()
+
+

Recalculate the given duration. +see also: https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/

+

Args

+
+
duration
+
the duration in seconds
+
+

Returns

+

A new duration that the jitter amount is added

+
+
+
+
+class RateLimitErrorRetryHandler +(max_retry_count: int = 1,
interval_calculator: RetryIntervalCalculator = <slack_sdk.http_retry.builtin_interval_calculators.BackoffRetryIntervalCalculator object>)
+
+
+
+ +Expand source code + +
class RateLimitErrorRetryHandler(RetryHandler):
+    """RetryHandler that does retries for rate limited errors."""
+
+    def _can_retry(
+        self,
+        *,
+        state: RetryState,
+        request: HttpRequest,
+        response: Optional[HttpResponse] = None,
+        error: Optional[Exception] = None,
+    ) -> bool:
+        return response is not None and response.status_code == 429
+
+    def prepare_for_next_attempt(
+        self,
+        *,
+        state: RetryState,
+        request: HttpRequest,
+        response: Optional[HttpResponse] = None,
+        error: Optional[Exception] = None,
+    ) -> None:
+        if response is None:
+            raise error  # type: ignore[misc]
+
+        state.next_attempt_requested = True
+        retry_after_header_name: Optional[str] = None
+        for k in response.headers.keys():
+            if k.lower() == "retry-after":
+                retry_after_header_name = k
+                break
+        duration = 1
+        if retry_after_header_name is None:
+            # This situation usually does not arise. Just in case.
+            duration += random.random()  # type: ignore[assignment]
+        else:
+            duration = int(response.headers.get(retry_after_header_name)[0]) + random.random()  # type: ignore[index, assignment] # noqa: E501
+        time.sleep(duration)
+        state.increment_current_attempt()
+
+

RetryHandler that does retries for rate limited errors.

+

RetryHandler interface.

+

Args

+
+
max_retry_count
+
The maximum times to do retries
+
interval_calculator
+
Pass an interval calculator for customizing the logic
+
+

Ancestors

+ +

Methods

+
+
+def prepare_for_next_attempt(self,
*,
state: RetryState,
request: HttpRequest,
response: HttpResponse | None = None,
error: Exception | None = None) ‑> None
+
+
+
+ +Expand source code + +
def prepare_for_next_attempt(
+    self,
+    *,
+    state: RetryState,
+    request: HttpRequest,
+    response: Optional[HttpResponse] = None,
+    error: Optional[Exception] = None,
+) -> None:
+    if response is None:
+        raise error  # type: ignore[misc]
+
+    state.next_attempt_requested = True
+    retry_after_header_name: Optional[str] = None
+    for k in response.headers.keys():
+        if k.lower() == "retry-after":
+            retry_after_header_name = k
+            break
+    duration = 1
+    if retry_after_header_name is None:
+        # This situation usually does not arise. Just in case.
+        duration += random.random()  # type: ignore[assignment]
+    else:
+        duration = int(response.headers.get(retry_after_header_name)[0]) + random.random()  # type: ignore[index, assignment] # noqa: E501
+    time.sleep(duration)
+    state.increment_current_attempt()
+
+
+
+
+

Inherited members

+ +
+
+class RetryHandler +(max_retry_count: int = 1,
interval_calculator: RetryIntervalCalculator = <slack_sdk.http_retry.builtin_interval_calculators.BackoffRetryIntervalCalculator object>)
+
+
+
+ +Expand source code + +
class RetryHandler:
+    """RetryHandler interface.
+    You can pass an array of handlers to customize retry logics in supported API clients.
+    """
+
+    max_retry_count: int
+    interval_calculator: RetryIntervalCalculator
+
+    def __init__(
+        self,
+        max_retry_count: int = 1,
+        interval_calculator: RetryIntervalCalculator = default_interval_calculator,
+    ):
+        """RetryHandler interface.
+
+        Args:
+            max_retry_count: The maximum times to do retries
+            interval_calculator: Pass an interval calculator for customizing the logic
+        """
+        self.max_retry_count = max_retry_count
+        self.interval_calculator = interval_calculator
+
+    def can_retry(
+        self,
+        *,
+        state: RetryState,
+        request: HttpRequest,
+        response: Optional[HttpResponse] = None,
+        error: Optional[Exception] = None,
+    ) -> bool:
+        if state.current_attempt >= self.max_retry_count:
+            return False
+        return self._can_retry(
+            state=state,
+            request=request,
+            response=response,
+            error=error,
+        )
+
+    def _can_retry(
+        self,
+        *,
+        state: RetryState,
+        request: HttpRequest,
+        response: Optional[HttpResponse] = None,
+        error: Optional[Exception] = None,
+    ) -> bool:
+        raise NotImplementedError()
+
+    def prepare_for_next_attempt(
+        self,
+        *,
+        state: RetryState,
+        request: HttpRequest,
+        response: Optional[HttpResponse] = None,
+        error: Optional[Exception] = None,
+    ) -> None:
+        state.next_attempt_requested = True
+        duration = self.interval_calculator.calculate_sleep_duration(state.current_attempt)
+        time.sleep(duration)
+        state.increment_current_attempt()
+
+

RetryHandler interface. +You can pass an array of handlers to customize retry logics in supported API clients.

+

RetryHandler interface.

+

Args

+
+
max_retry_count
+
The maximum times to do retries
+
interval_calculator
+
Pass an interval calculator for customizing the logic
+
+

Subclasses

+ +

Class variables

+
+
var interval_calculatorRetryIntervalCalculator
+
+

The type of the None singleton.

+
+
var max_retry_count : int
+
+

The type of the None singleton.

+
+
+

Methods

+
+
+def can_retry(self,
*,
state: RetryState,
request: HttpRequest,
response: HttpResponse | None = None,
error: Exception | None = None) ‑> bool
+
+
+
+ +Expand source code + +
def can_retry(
+    self,
+    *,
+    state: RetryState,
+    request: HttpRequest,
+    response: Optional[HttpResponse] = None,
+    error: Optional[Exception] = None,
+) -> bool:
+    if state.current_attempt >= self.max_retry_count:
+        return False
+    return self._can_retry(
+        state=state,
+        request=request,
+        response=response,
+        error=error,
+    )
+
+
+
+
+def prepare_for_next_attempt(self,
*,
state: RetryState,
request: HttpRequest,
response: HttpResponse | None = None,
error: Exception | None = None) ‑> None
+
+
+
+ +Expand source code + +
def prepare_for_next_attempt(
+    self,
+    *,
+    state: RetryState,
+    request: HttpRequest,
+    response: Optional[HttpResponse] = None,
+    error: Optional[Exception] = None,
+) -> None:
+    state.next_attempt_requested = True
+    duration = self.interval_calculator.calculate_sleep_duration(state.current_attempt)
+    time.sleep(duration)
+    state.increment_current_attempt()
+
+
+
+
+
+
+class RetryIntervalCalculator +
+
+
+ +Expand source code + +
class RetryIntervalCalculator:
+    """Retry interval calculator interface."""
+
+    def calculate_sleep_duration(self, current_attempt: int) -> float:
+        """Calculates an interval duration in seconds.
+
+        Args:
+            current_attempt: the number of the current attempt (zero-origin; 0 means no retries are done so far)
+        Returns:
+            calculated interval duration in seconds
+        """
+        raise NotImplementedError()
+
+

Retry interval calculator interface.

+

Subclasses

+ +

Methods

+
+
+def calculate_sleep_duration(self, current_attempt: int) ‑> float +
+
+
+ +Expand source code + +
def calculate_sleep_duration(self, current_attempt: int) -> float:
+    """Calculates an interval duration in seconds.
+
+    Args:
+        current_attempt: the number of the current attempt (zero-origin; 0 means no retries are done so far)
+    Returns:
+        calculated interval duration in seconds
+    """
+    raise NotImplementedError()
+
+

Calculates an interval duration in seconds.

+

Args

+
+
current_attempt
+
the number of the current attempt (zero-origin; 0 means no retries are done so far)
+
+

Returns

+

calculated interval duration in seconds

+
+
+
+
+class RetryState +(*, current_attempt: int = 0, custom_values: Dict[str, Any] | None = None) +
+
+
+ +Expand source code + +
class RetryState:
+    next_attempt_requested: bool
+    current_attempt: int  # zero-origin
+    custom_values: Optional[Dict[str, Any]]
+
+    def __init__(
+        self,
+        *,
+        current_attempt: int = 0,
+        custom_values: Optional[Dict[str, Any]] = None,
+    ):
+        self.next_attempt_requested = False
+        self.current_attempt = current_attempt
+        self.custom_values = custom_values
+
+    def increment_current_attempt(self) -> int:
+        self.current_attempt += 1
+        return self.current_attempt
+
+
+

Class variables

+
+
var current_attempt : int
+
+

The type of the None singleton.

+
+
var custom_values : Dict[str, Any] | None
+
+

The type of the None singleton.

+
+
var next_attempt_requested : bool
+
+

The type of the None singleton.

+
+
+

Methods

+
+
+def increment_current_attempt(self) ‑> int +
+
+
+ +Expand source code + +
def increment_current_attempt(self) -> int:
+    self.current_attempt += 1
+    return self.current_attempt
+
+
+
+
+
+
+
+
+ +
+ + + diff --git a/docs/reference/http_retry/interval_calculator.html b/docs/reference/http_retry/interval_calculator.html new file mode 100644 index 000000000..938799629 --- /dev/null +++ b/docs/reference/http_retry/interval_calculator.html @@ -0,0 +1,137 @@ + + + + + + +slack_sdk.http_retry.interval_calculator API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.http_retry.interval_calculator

+
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class RetryIntervalCalculator +
+
+
+ +Expand source code + +
class RetryIntervalCalculator:
+    """Retry interval calculator interface."""
+
+    def calculate_sleep_duration(self, current_attempt: int) -> float:
+        """Calculates an interval duration in seconds.
+
+        Args:
+            current_attempt: the number of the current attempt (zero-origin; 0 means no retries are done so far)
+        Returns:
+            calculated interval duration in seconds
+        """
+        raise NotImplementedError()
+
+

Retry interval calculator interface.

+

Subclasses

+ +

Methods

+
+
+def calculate_sleep_duration(self, current_attempt: int) ‑> float +
+
+
+ +Expand source code + +
def calculate_sleep_duration(self, current_attempt: int) -> float:
+    """Calculates an interval duration in seconds.
+
+    Args:
+        current_attempt: the number of the current attempt (zero-origin; 0 means no retries are done so far)
+    Returns:
+        calculated interval duration in seconds
+    """
+    raise NotImplementedError()
+
+

Calculates an interval duration in seconds.

+

Args

+
+
current_attempt
+
the number of the current attempt (zero-origin; 0 means no retries are done so far)
+
+

Returns

+

calculated interval duration in seconds

+
+
+
+
+
+
+ +
+ + + diff --git a/docs/reference/http_retry/jitter.html b/docs/reference/http_retry/jitter.html new file mode 100644 index 000000000..0268d2cce --- /dev/null +++ b/docs/reference/http_retry/jitter.html @@ -0,0 +1,172 @@ + + + + + + +slack_sdk.http_retry.jitter API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.http_retry.jitter

+
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class Jitter +
+
+
+ +Expand source code + +
class Jitter:
+    """Jitter interface"""
+
+    def recalculate(self, duration: float) -> float:
+        """Recalculate the given duration.
+        see also: https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/
+
+        Args:
+            duration: the duration in seconds
+
+        Returns:
+            A new duration that the jitter amount is added
+        """
+        raise NotImplementedError()
+
+

Jitter interface

+

Subclasses

+ +

Methods

+
+
+def recalculate(self, duration: float) ‑> float +
+
+
+ +Expand source code + +
def recalculate(self, duration: float) -> float:
+    """Recalculate the given duration.
+    see also: https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/
+
+    Args:
+        duration: the duration in seconds
+
+    Returns:
+        A new duration that the jitter amount is added
+    """
+    raise NotImplementedError()
+
+

Recalculate the given duration. +see also: https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/

+

Args

+
+
duration
+
the duration in seconds
+
+

Returns

+

A new duration that the jitter amount is added

+
+
+
+
+class RandomJitter +
+
+
+ +Expand source code + +
class RandomJitter(Jitter):
+    """Random jitter implementation"""
+
+    def recalculate(self, duration: float) -> float:
+        return duration + random.random()
+
+

Random jitter implementation

+

Ancestors

+ +

Inherited members

+ +
+
+
+
+ +
+ + + diff --git a/docs/reference/http_retry/request.html b/docs/reference/http_retry/request.html new file mode 100644 index 000000000..e088c7443 --- /dev/null +++ b/docs/reference/http_retry/request.html @@ -0,0 +1,160 @@ + + + + + + +slack_sdk.http_retry.request API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.http_retry.request

+
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class HttpRequest +(*,
method: str,
url: str,
headers: Dict[str, str | List[str]],
body_params: Dict[str, Any] | None = None,
data: bytes | None = None)
+
+
+
+ +Expand source code + +
class HttpRequest:
+    """HTTP request representation"""
+
+    method: str
+    url: str
+    headers: Dict[str, Union[str, List[str]]]
+    body_params: Optional[Dict[str, Any]]
+    data: Optional[bytes]
+
+    def __init__(
+        self,
+        *,
+        method: str,
+        url: str,
+        headers: Dict[str, Union[str, List[str]]],
+        body_params: Optional[Dict[str, Any]] = None,
+        data: Optional[bytes] = None,
+    ):
+        self.method = method
+        self.url = url
+        self.headers = {k: v if isinstance(v, list) else [v] for k, v in headers.items()}
+        self.body_params = body_params
+        self.data = data
+
+    @classmethod
+    def from_urllib_http_request(cls, req: Request) -> "HttpRequest":
+        return HttpRequest(
+            method=req.method,  # type: ignore[arg-type]
+            url=req.full_url,
+            headers={k: v if isinstance(v, list) else [v] for k, v in req.headers.items()},
+            data=req.data,  # type: ignore[arg-type]
+        )
+
+

HTTP request representation

+

Class variables

+
+
var body_params : Dict[str, Any] | None
+
+

The type of the None singleton.

+
+
var data : bytes | None
+
+

The type of the None singleton.

+
+
var headers : Dict[str, str | List[str]]
+
+

The type of the None singleton.

+
+
var method : str
+
+

The type of the None singleton.

+
+
var url : str
+
+

The type of the None singleton.

+
+
+

Static methods

+
+
+def from_urllib_http_request(req: urllib.request.Request) ‑> HttpRequest +
+
+
+
+
+
+
+
+
+ +
+ + + diff --git a/docs/reference/http_retry/response.html b/docs/reference/http_retry/response.html new file mode 100644 index 000000000..4a786ab78 --- /dev/null +++ b/docs/reference/http_retry/response.html @@ -0,0 +1,133 @@ + + + + + + +slack_sdk.http_retry.response API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.http_retry.response

+
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class HttpResponse +(*,
status_code: int | str,
headers: Dict[str, str | List[str]],
body: Dict[str, Any] | None = None,
data: bytes | None = None)
+
+
+
+ +Expand source code + +
class HttpResponse:
+    """HTTP response representation"""
+
+    status_code: int
+    headers: Dict[str, Union[List[str], str]]
+    body: Optional[Dict[str, Any]]
+    data: Optional[bytes]
+
+    def __init__(
+        self,
+        *,
+        status_code: Union[int, str],
+        headers: Dict[str, Union[str, List[str]]],
+        body: Optional[Dict[str, Any]] = None,
+        data: Optional[bytes] = None,
+    ):
+        self.status_code = int(status_code)
+        self.headers = {k: v if isinstance(v, list) else [v] for k, v in headers.items()}
+        self.body = body
+        self.data = data
+
+

HTTP response representation

+

Class variables

+
+
var body : Dict[str, Any] | None
+
+

The type of the None singleton.

+
+
var data : bytes | None
+
+

The type of the None singleton.

+
+
var headers : Dict[str, str | List[str]]
+
+

The type of the None singleton.

+
+
var status_code : int
+
+

The type of the None singleton.

+
+
+
+
+
+
+ +
+ + + diff --git a/docs/reference/http_retry/state.html b/docs/reference/http_retry/state.html new file mode 100644 index 000000000..3096592cc --- /dev/null +++ b/docs/reference/http_retry/state.html @@ -0,0 +1,144 @@ + + + + + + +slack_sdk.http_retry.state API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.http_retry.state

+
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class RetryState +(*, current_attempt: int = 0, custom_values: Dict[str, Any] | None = None) +
+
+
+ +Expand source code + +
class RetryState:
+    next_attempt_requested: bool
+    current_attempt: int  # zero-origin
+    custom_values: Optional[Dict[str, Any]]
+
+    def __init__(
+        self,
+        *,
+        current_attempt: int = 0,
+        custom_values: Optional[Dict[str, Any]] = None,
+    ):
+        self.next_attempt_requested = False
+        self.current_attempt = current_attempt
+        self.custom_values = custom_values
+
+    def increment_current_attempt(self) -> int:
+        self.current_attempt += 1
+        return self.current_attempt
+
+
+

Class variables

+
+
var current_attempt : int
+
+

The type of the None singleton.

+
+
var custom_values : Dict[str, Any] | None
+
+

The type of the None singleton.

+
+
var next_attempt_requested : bool
+
+

The type of the None singleton.

+
+
+

Methods

+
+
+def increment_current_attempt(self) ‑> int +
+
+
+ +Expand source code + +
def increment_current_attempt(self) -> int:
+    self.current_attempt += 1
+    return self.current_attempt
+
+
+
+
+
+
+
+
+ +
+ + + diff --git a/docs/reference/index.html b/docs/reference/index.html new file mode 100644 index 000000000..7cc7723c1 --- /dev/null +++ b/docs/reference/index.html @@ -0,0 +1,16366 @@ + + + + + + +slack_sdk API documentation + + + + + + + + + + + +
+
+
+

Package slack_sdk

+
+
+ +

Here is the list of key modules in this SDK:

+

Web API Client

+ +

Webhook / response_url Client

+ +

Socket Mode Client

+ +

OAuth

+ +

Audit Logs API Client

+ +

SCIM API Client

+ +
+
+

Sub-modules

+
+
slack_sdk.aiohttp_version_checker
+
+

Internal module for checking aiohttp compatibility of async modules

+
+
slack_sdk.audit_logs
+
+

Audit Logs API is a set of APIs for monitoring what’s happening in your Enterprise Grid organization …

+
+
slack_sdk.errors
+
+

Errors that can be raised by this SDK

+
+
slack_sdk.http_retry
+
+
+
+
slack_sdk.models
+
+

Classes for constructing Slack-specific data structure

+
+
slack_sdk.oauth
+
+

Modules for implementing the Slack OAuth flow …

+
+
slack_sdk.proxy_env_variable_loader
+
+

Internal module for loading proxy-related env variables

+
+
slack_sdk.rtm
+
+

A Python module for interacting with Slack's RTM API.

+
+
slack_sdk.rtm_v2
+
+

A Python module for interacting with Slack's RTM API.

+
+
slack_sdk.scim
+
+

SCIM API is a set of APIs for provisioning and managing user accounts and groups. +SCIM is used by Single Sign-On (SSO) services and identity providers …

+
+
slack_sdk.signature
+
+

Slack request signature verifier

+
+
slack_sdk.socket_mode
+
+

Socket Mode is a method of connecting your app to Slack’s APIs using WebSockets instead of HTTP. +You can use slack_sdk.socket_mode.SocketModeClient …

+
+
slack_sdk.version
+
+

Check the latest version at https://pypi.org/project/slack-sdk/

+
+
slack_sdk.web
+
+

The Slack Web API allows you to build applications that interact with Slack +in more complex ways than the integrations we provide out of the box.

+
+
slack_sdk.webhook
+
+

You can use slack_sdk.webhook.WebhookClient for Incoming Webhooks +and message responses using response_url in payloads.

+
+
+
+
+
+
+
+
+

Classes

+
+
+class WebClient +(token: str | None = None,
base_url: str = 'https://slack.com/api/',
timeout: int = 30,
ssl: ssl.SSLContext | None = None,
proxy: str | None = None,
headers: dict | None = None,
user_agent_prefix: str | None = None,
user_agent_suffix: str | None = None,
team_id: str | None = None,
logger: logging.Logger | None = None,
retry_handlers: List[RetryHandler] | None = None)
+
+
+
+ +Expand source code + +
class WebClient(BaseClient):
+    """A WebClient allows apps to communicate with the Slack Platform's Web API.
+
+    https://docs.slack.dev/reference/methods
+
+    The Slack Web API is an interface for querying information from
+    and enacting change in a Slack workspace.
+
+    This client handles constructing and sending HTTP requests to Slack
+    as well as parsing any responses received into a `SlackResponse`.
+
+    Attributes:
+        token (str): A string specifying an `xoxp-*` or `xoxb-*` token.
+        base_url (str): A string representing the Slack API base URL.
+            Default is `'https://slack.com/api/'`
+        timeout (int): The maximum number of seconds the client will wait
+            to connect and receive a response from Slack.
+            Default is 30 seconds.
+        ssl (SSLContext): An [`ssl.SSLContext`][1] instance, helpful for specifying
+            your own custom certificate chain.
+        proxy (str): String representing a fully-qualified URL to a proxy through
+            which to route all requests to the Slack API. Even if this parameter
+            is not specified, if any of the following environment variables are
+            present, they will be loaded into this parameter: `HTTPS_PROXY`,
+            `https_proxy`, `HTTP_PROXY` or `http_proxy`.
+        headers (dict): Additional request headers to attach to all requests.
+
+    Methods:
+        `api_call`: Constructs a request and executes the API call to Slack.
+
+    Example of recommended usage:
+    ```python
+        import os
+        from slack_sdk import WebClient
+
+        client = WebClient(token=os.environ['SLACK_API_TOKEN'])
+        response = client.chat_postMessage(
+            channel='#random',
+            text="Hello world!")
+        assert response["ok"]
+        assert response["message"]["text"] == "Hello world!"
+    ```
+
+    Example manually creating an API request:
+    ```python
+        import os
+        from slack_sdk import WebClient
+
+        client = WebClient(token=os.environ['SLACK_API_TOKEN'])
+        response = client.api_call(
+            api_method='chat.postMessage',
+            json={'channel': '#random','text': "Hello world!"}
+        )
+        assert response["ok"]
+        assert response["message"]["text"] == "Hello world!"
+    ```
+
+    Note:
+        Any attributes or methods prefixed with _underscores are
+        intended to be "private" internal use only. They may be changed or
+        removed at anytime.
+
+    [1]: https://docs.python.org/3/library/ssl.html#ssl.SSLContext
+    """
+
+    def admin_analytics_getFile(
+        self,
+        *,
+        type: str,
+        date: Optional[str] = None,
+        metadata_only: Optional[bool] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Retrieve analytics data for a given date, presented as a compressed JSON file
+        https://docs.slack.dev/reference/methods/admin.analytics.getFile
+        """
+        kwargs.update({"type": type})
+        if date is not None:
+            kwargs.update({"date": date})
+        if metadata_only is not None:
+            kwargs.update({"metadata_only": metadata_only})
+        return self.api_call("admin.analytics.getFile", params=kwargs)
+
+    def admin_apps_approve(
+        self,
+        *,
+        app_id: Optional[str] = None,
+        request_id: Optional[str] = None,
+        enterprise_id: Optional[str] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Approve an app for installation on a workspace.
+        Either app_id or request_id is required.
+        These IDs can be obtained either directly via the app_requested event,
+        or by the admin.apps.requests.list method.
+        https://docs.slack.dev/reference/methods/admin.apps.approve
+        """
+        if app_id:
+            kwargs.update({"app_id": app_id})
+        elif request_id:
+            kwargs.update({"request_id": request_id})
+        else:
+            raise e.SlackRequestError("The app_id or request_id argument must be specified.")
+
+        kwargs.update(
+            {
+                "enterprise_id": enterprise_id,
+                "team_id": team_id,
+            }
+        )
+        return self.api_call("admin.apps.approve", params=kwargs)
+
+    def admin_apps_approved_list(
+        self,
+        *,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        enterprise_id: Optional[str] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """List approved apps for an org or workspace.
+        https://docs.slack.dev/reference/methods/admin.apps.approved.list
+        """
+        kwargs.update(
+            {
+                "cursor": cursor,
+                "limit": limit,
+                "enterprise_id": enterprise_id,
+                "team_id": team_id,
+            }
+        )
+        return self.api_call("admin.apps.approved.list", http_verb="GET", params=kwargs)
+
+    def admin_apps_clearResolution(
+        self,
+        *,
+        app_id: str,
+        enterprise_id: Optional[str] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Clear an app resolution
+        https://docs.slack.dev/reference/methods/admin.apps.clearResolution
+        """
+        kwargs.update(
+            {
+                "app_id": app_id,
+                "enterprise_id": enterprise_id,
+                "team_id": team_id,
+            }
+        )
+        return self.api_call("admin.apps.clearResolution", http_verb="POST", params=kwargs)
+
+    def admin_apps_requests_cancel(
+        self,
+        *,
+        request_id: str,
+        enterprise_id: Optional[str] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """List app requests for a team/workspace.
+        https://docs.slack.dev/reference/methods/admin.apps.requests.cancel
+        """
+        kwargs.update(
+            {
+                "request_id": request_id,
+                "enterprise_id": enterprise_id,
+                "team_id": team_id,
+            }
+        )
+        return self.api_call("admin.apps.requests.cancel", http_verb="POST", params=kwargs)
+
+    def admin_apps_requests_list(
+        self,
+        *,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """List app requests for a team/workspace.
+        https://docs.slack.dev/reference/methods/admin.apps.requests.list
+        """
+        kwargs.update(
+            {
+                "cursor": cursor,
+                "limit": limit,
+                "team_id": team_id,
+            }
+        )
+        return self.api_call("admin.apps.requests.list", http_verb="GET", params=kwargs)
+
+    def admin_apps_restrict(
+        self,
+        *,
+        app_id: Optional[str] = None,
+        request_id: Optional[str] = None,
+        enterprise_id: Optional[str] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Restrict an app for installation on a workspace.
+        Exactly one of the team_id or enterprise_id arguments is required, not both.
+        Either app_id or request_id is required. These IDs can be obtained either directly
+        via the app_requested event, or by the admin.apps.requests.list method.
+        https://docs.slack.dev/reference/methods/admin.apps.restrict
+        """
+        if app_id:
+            kwargs.update({"app_id": app_id})
+        elif request_id:
+            kwargs.update({"request_id": request_id})
+        else:
+            raise e.SlackRequestError("The app_id or request_id argument must be specified.")
+
+        kwargs.update(
+            {
+                "enterprise_id": enterprise_id,
+                "team_id": team_id,
+            }
+        )
+        return self.api_call("admin.apps.restrict", params=kwargs)
+
+    def admin_apps_restricted_list(
+        self,
+        *,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        enterprise_id: Optional[str] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """List restricted apps for an org or workspace.
+        https://docs.slack.dev/reference/methods/admin.apps.restricted.list
+        """
+        kwargs.update(
+            {
+                "cursor": cursor,
+                "limit": limit,
+                "enterprise_id": enterprise_id,
+                "team_id": team_id,
+            }
+        )
+        return self.api_call("admin.apps.restricted.list", http_verb="GET", params=kwargs)
+
+    def admin_apps_uninstall(
+        self,
+        *,
+        app_id: str,
+        enterprise_id: Optional[str] = None,
+        team_ids: Optional[Union[str, Sequence[str]]] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Uninstall an app from one or many workspaces, or an entire enterprise organization.
+        With an org-level token, enterprise_id or team_ids is required.
+        https://docs.slack.dev/reference/methods/admin.apps.uninstall
+        """
+        kwargs.update({"app_id": app_id})
+        if enterprise_id is not None:
+            kwargs.update({"enterprise_id": enterprise_id})
+        if team_ids is not None:
+            if isinstance(team_ids, (list, tuple)):
+                kwargs.update({"team_ids": ",".join(team_ids)})
+            else:
+                kwargs.update({"team_ids": team_ids})
+        return self.api_call("admin.apps.uninstall", http_verb="POST", params=kwargs)
+
+    def admin_apps_activities_list(
+        self,
+        *,
+        app_id: Optional[str] = None,
+        component_id: Optional[str] = None,
+        component_type: Optional[str] = None,
+        log_event_type: Optional[str] = None,
+        max_date_created: Optional[int] = None,
+        min_date_created: Optional[int] = None,
+        min_log_level: Optional[str] = None,
+        sort_direction: Optional[str] = None,
+        source: Optional[str] = None,
+        team_id: Optional[str] = None,
+        trace_id: Optional[str] = None,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Get logs for a specified team/org
+        https://docs.slack.dev/reference/methods/admin.apps.activities.list
+        """
+        kwargs.update(
+            {
+                "app_id": app_id,
+                "component_id": component_id,
+                "component_type": component_type,
+                "log_event_type": log_event_type,
+                "max_date_created": max_date_created,
+                "min_date_created": min_date_created,
+                "min_log_level": min_log_level,
+                "sort_direction": sort_direction,
+                "source": source,
+                "team_id": team_id,
+                "trace_id": trace_id,
+                "cursor": cursor,
+                "limit": limit,
+            }
+        )
+        return self.api_call("admin.apps.activities.list", params=kwargs)
+
+    def admin_apps_config_lookup(
+        self,
+        *,
+        app_ids: Union[str, Sequence[str]],
+        **kwargs,
+    ) -> SlackResponse:
+        """Look up the app config for connectors by their IDs
+        https://docs.slack.dev/reference/methods/admin.apps.config.lookup
+        """
+        if isinstance(app_ids, (list, tuple)):
+            kwargs.update({"app_ids": ",".join(app_ids)})
+        else:
+            kwargs.update({"app_ids": app_ids})
+        return self.api_call("admin.apps.config.lookup", params=kwargs)
+
+    def admin_apps_config_set(
+        self,
+        *,
+        app_id: str,
+        domain_restrictions: Optional[Dict[str, Any]] = None,
+        workflow_auth_strategy: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Set the app config for a connector
+        https://docs.slack.dev/reference/methods/admin.apps.config.set
+        """
+        kwargs.update(
+            {
+                "app_id": app_id,
+                "workflow_auth_strategy": workflow_auth_strategy,
+            }
+        )
+        if domain_restrictions is not None:
+            kwargs.update({"domain_restrictions": json.dumps(domain_restrictions)})
+        return self.api_call("admin.apps.config.set", params=kwargs)
+
+    def admin_auth_policy_getEntities(
+        self,
+        *,
+        policy_name: str,
+        cursor: Optional[str] = None,
+        entity_type: Optional[str] = None,
+        limit: Optional[int] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Fetch all the entities assigned to a particular authentication policy by name.
+        https://docs.slack.dev/reference/methods/admin.auth.policy.getEntities
+        """
+        kwargs.update({"policy_name": policy_name})
+        if cursor is not None:
+            kwargs.update({"cursor": cursor})
+        if entity_type is not None:
+            kwargs.update({"entity_type": entity_type})
+        if limit is not None:
+            kwargs.update({"limit": limit})
+        return self.api_call("admin.auth.policy.getEntities", http_verb="POST", params=kwargs)
+
+    def admin_auth_policy_assignEntities(
+        self,
+        *,
+        entity_ids: Union[str, Sequence[str]],
+        policy_name: str,
+        entity_type: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Assign entities to a particular authentication policy.
+        https://docs.slack.dev/reference/methods/admin.auth.policy.assignEntities
+        """
+        if isinstance(entity_ids, (list, tuple)):
+            kwargs.update({"entity_ids": ",".join(entity_ids)})
+        else:
+            kwargs.update({"entity_ids": entity_ids})
+        kwargs.update({"policy_name": policy_name})
+        kwargs.update({"entity_type": entity_type})
+        return self.api_call("admin.auth.policy.assignEntities", http_verb="POST", params=kwargs)
+
+    def admin_auth_policy_removeEntities(
+        self,
+        *,
+        entity_ids: Union[str, Sequence[str]],
+        policy_name: str,
+        entity_type: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Remove specified entities from a specified authentication policy.
+        https://docs.slack.dev/reference/methods/admin.auth.policy.removeEntities
+        """
+        if isinstance(entity_ids, (list, tuple)):
+            kwargs.update({"entity_ids": ",".join(entity_ids)})
+        else:
+            kwargs.update({"entity_ids": entity_ids})
+        kwargs.update({"policy_name": policy_name})
+        kwargs.update({"entity_type": entity_type})
+        return self.api_call("admin.auth.policy.removeEntities", http_verb="POST", params=kwargs)
+
+    def admin_conversations_createForObjects(
+        self,
+        *,
+        object_id: str,
+        salesforce_org_id: str,
+        invite_object_team: Optional[bool] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Create a Salesforce channel for the corresponding object provided.
+        https://docs.slack.dev/reference/methods/admin.conversations.createForObjects
+        """
+        kwargs.update(
+            {"object_id": object_id, "salesforce_org_id": salesforce_org_id, "invite_object_team": invite_object_team}
+        )
+        return self.api_call("admin.conversations.createForObjects", params=kwargs)
+
+    def admin_conversations_linkObjects(
+        self,
+        *,
+        channel: str,
+        record_id: str,
+        salesforce_org_id: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Link a Salesforce record to a channel.
+        https://docs.slack.dev/reference/methods/admin.conversations.linkObjects
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "record_id": record_id,
+                "salesforce_org_id": salesforce_org_id,
+            }
+        )
+        return self.api_call("admin.conversations.linkObjects", params=kwargs)
+
+    def admin_conversations_unlinkObjects(
+        self,
+        *,
+        channel: str,
+        new_name: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Unlink a Salesforce record from a channel.
+        https://docs.slack.dev/reference/methods/admin.conversations.unlinkObjects
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "new_name": new_name,
+            }
+        )
+        return self.api_call("admin.conversations.unlinkObjects", params=kwargs)
+
+    def admin_barriers_create(
+        self,
+        *,
+        barriered_from_usergroup_ids: Union[str, Sequence[str]],
+        primary_usergroup_id: str,
+        restricted_subjects: Union[str, Sequence[str]],
+        **kwargs,
+    ) -> SlackResponse:
+        """Create an Information Barrier
+        https://docs.slack.dev/reference/methods/admin.barriers.create
+        """
+        kwargs.update({"primary_usergroup_id": primary_usergroup_id})
+        if isinstance(barriered_from_usergroup_ids, (list, tuple)):
+            kwargs.update({"barriered_from_usergroup_ids": ",".join(barriered_from_usergroup_ids)})
+        else:
+            kwargs.update({"barriered_from_usergroup_ids": barriered_from_usergroup_ids})
+        if isinstance(restricted_subjects, (list, tuple)):
+            kwargs.update({"restricted_subjects": ",".join(restricted_subjects)})
+        else:
+            kwargs.update({"restricted_subjects": restricted_subjects})
+        return self.api_call("admin.barriers.create", http_verb="POST", params=kwargs)
+
+    def admin_barriers_delete(
+        self,
+        *,
+        barrier_id: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Delete an existing Information Barrier
+        https://docs.slack.dev/reference/methods/admin.barriers.delete
+        """
+        kwargs.update({"barrier_id": barrier_id})
+        return self.api_call("admin.barriers.delete", http_verb="POST", params=kwargs)
+
+    def admin_barriers_update(
+        self,
+        *,
+        barrier_id: str,
+        barriered_from_usergroup_ids: Union[str, Sequence[str]],
+        primary_usergroup_id: str,
+        restricted_subjects: Union[str, Sequence[str]],
+        **kwargs,
+    ) -> SlackResponse:
+        """Update an existing Information Barrier
+        https://docs.slack.dev/reference/methods/admin.barriers.update
+        """
+        kwargs.update({"barrier_id": barrier_id, "primary_usergroup_id": primary_usergroup_id})
+        if isinstance(barriered_from_usergroup_ids, (list, tuple)):
+            kwargs.update({"barriered_from_usergroup_ids": ",".join(barriered_from_usergroup_ids)})
+        else:
+            kwargs.update({"barriered_from_usergroup_ids": barriered_from_usergroup_ids})
+        if isinstance(restricted_subjects, (list, tuple)):
+            kwargs.update({"restricted_subjects": ",".join(restricted_subjects)})
+        else:
+            kwargs.update({"restricted_subjects": restricted_subjects})
+        return self.api_call("admin.barriers.update", http_verb="POST", params=kwargs)
+
+    def admin_barriers_list(
+        self,
+        *,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Get all Information Barriers for your organization
+        https://docs.slack.dev/reference/methods/admin.barriers.list"""
+        kwargs.update(
+            {
+                "cursor": cursor,
+                "limit": limit,
+            }
+        )
+        return self.api_call("admin.barriers.list", http_verb="GET", params=kwargs)
+
+    def admin_conversations_create(
+        self,
+        *,
+        is_private: bool,
+        name: str,
+        description: Optional[str] = None,
+        org_wide: Optional[bool] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Create a public or private channel-based conversation.
+        https://docs.slack.dev/reference/methods/admin.conversations.create
+        """
+        kwargs.update(
+            {
+                "is_private": is_private,
+                "name": name,
+                "description": description,
+                "org_wide": org_wide,
+                "team_id": team_id,
+            }
+        )
+        return self.api_call("admin.conversations.create", params=kwargs)
+
+    def admin_conversations_delete(
+        self,
+        *,
+        channel_id: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Delete a public or private channel.
+        https://docs.slack.dev/reference/methods/admin.conversations.delete
+        """
+        kwargs.update({"channel_id": channel_id})
+        return self.api_call("admin.conversations.delete", params=kwargs)
+
+    def admin_conversations_invite(
+        self,
+        *,
+        channel_id: str,
+        user_ids: Union[str, Sequence[str]],
+        **kwargs,
+    ) -> SlackResponse:
+        """Invite a user to a public or private channel.
+        https://docs.slack.dev/reference/methods/admin.conversations.invite
+        """
+        kwargs.update({"channel_id": channel_id})
+        if isinstance(user_ids, (list, tuple)):
+            kwargs.update({"user_ids": ",".join(user_ids)})
+        else:
+            kwargs.update({"user_ids": user_ids})
+        # NOTE: the endpoint is unable to handle Content-Type: application/json as of Sep 3, 2020.
+        return self.api_call("admin.conversations.invite", params=kwargs)
+
+    def admin_conversations_archive(
+        self,
+        *,
+        channel_id: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Archive a public or private channel.
+        https://docs.slack.dev/reference/methods/admin.conversations.archive
+        """
+        kwargs.update({"channel_id": channel_id})
+        return self.api_call("admin.conversations.archive", params=kwargs)
+
+    def admin_conversations_unarchive(
+        self,
+        *,
+        channel_id: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Unarchive a public or private channel.
+        https://docs.slack.dev/reference/methods/admin.conversations.archive
+        """
+        kwargs.update({"channel_id": channel_id})
+        return self.api_call("admin.conversations.unarchive", params=kwargs)
+
+    def admin_conversations_rename(
+        self,
+        *,
+        channel_id: str,
+        name: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Rename a public or private channel.
+        https://docs.slack.dev/reference/methods/admin.conversations.rename
+        """
+        kwargs.update({"channel_id": channel_id, "name": name})
+        return self.api_call("admin.conversations.rename", params=kwargs)
+
+    def admin_conversations_search(
+        self,
+        *,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        query: Optional[str] = None,
+        search_channel_types: Optional[Union[str, Sequence[str]]] = None,
+        sort: Optional[str] = None,
+        sort_dir: Optional[str] = None,
+        team_ids: Optional[Union[str, Sequence[str]]] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Search for public or private channels in an Enterprise organization.
+        https://docs.slack.dev/reference/methods/admin.conversations.search
+        """
+        kwargs.update(
+            {
+                "cursor": cursor,
+                "limit": limit,
+                "query": query,
+                "sort": sort,
+                "sort_dir": sort_dir,
+            }
+        )
+
+        if isinstance(search_channel_types, (list, tuple)):
+            kwargs.update({"search_channel_types": ",".join(search_channel_types)})
+        else:
+            kwargs.update({"search_channel_types": search_channel_types})
+
+        if isinstance(team_ids, (list, tuple)):
+            kwargs.update({"team_ids": ",".join(team_ids)})
+        else:
+            kwargs.update({"team_ids": team_ids})
+
+        return self.api_call("admin.conversations.search", params=kwargs)
+
+    def admin_conversations_convertToPrivate(
+        self,
+        *,
+        channel_id: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Convert a public channel to a private channel.
+        https://docs.slack.dev/reference/methods/admin.conversations.convertToPrivate
+        """
+        kwargs.update({"channel_id": channel_id})
+        return self.api_call("admin.conversations.convertToPrivate", params=kwargs)
+
+    def admin_conversations_convertToPublic(
+        self,
+        *,
+        channel_id: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Convert a privte channel to a public channel.
+        https://docs.slack.dev/reference/methods/admin.conversations.convertToPublic
+        """
+        kwargs.update({"channel_id": channel_id})
+        return self.api_call("admin.conversations.convertToPublic", params=kwargs)
+
+    def admin_conversations_setConversationPrefs(
+        self,
+        *,
+        channel_id: str,
+        prefs: Union[str, Dict[str, str]],
+        **kwargs,
+    ) -> SlackResponse:
+        """Set the posting permissions for a public or private channel.
+        https://docs.slack.dev/reference/methods/admin.conversations.setConversationPrefs
+        """
+        kwargs.update({"channel_id": channel_id})
+        if isinstance(prefs, dict):
+            kwargs.update({"prefs": json.dumps(prefs)})
+        else:
+            kwargs.update({"prefs": prefs})
+        return self.api_call("admin.conversations.setConversationPrefs", params=kwargs)
+
+    def admin_conversations_getConversationPrefs(
+        self,
+        *,
+        channel_id: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Get conversation preferences for a public or private channel.
+        https://docs.slack.dev/reference/methods/admin.conversations.getConversationPrefs
+        """
+        kwargs.update({"channel_id": channel_id})
+        return self.api_call("admin.conversations.getConversationPrefs", params=kwargs)
+
+    def admin_conversations_disconnectShared(
+        self,
+        *,
+        channel_id: str,
+        leaving_team_ids: Optional[Union[str, Sequence[str]]] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Disconnect a connected channel from one or more workspaces.
+        https://docs.slack.dev/reference/methods/admin.conversations.disconnectShared
+        """
+        kwargs.update({"channel_id": channel_id})
+        if isinstance(leaving_team_ids, (list, tuple)):
+            kwargs.update({"leaving_team_ids": ",".join(leaving_team_ids)})
+        else:
+            kwargs.update({"leaving_team_ids": leaving_team_ids})
+        return self.api_call("admin.conversations.disconnectShared", params=kwargs)
+
+    def admin_conversations_lookup(
+        self,
+        *,
+        last_message_activity_before: int,
+        team_ids: Union[str, Sequence[str]],
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        max_member_count: Optional[int] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Returns channels on the given team using the filters.
+        https://docs.slack.dev/reference/methods/admin.conversations.lookup
+        """
+        kwargs.update(
+            {
+                "last_message_activity_before": last_message_activity_before,
+                "cursor": cursor,
+                "limit": limit,
+                "max_member_count": max_member_count,
+            }
+        )
+        if isinstance(team_ids, (list, tuple)):
+            kwargs.update({"team_ids": ",".join(team_ids)})
+        else:
+            kwargs.update({"team_ids": team_ids})
+        return self.api_call("admin.conversations.lookup", params=kwargs)
+
+    def admin_conversations_ekm_listOriginalConnectedChannelInfo(
+        self,
+        *,
+        channel_ids: Optional[Union[str, Sequence[str]]] = None,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        team_ids: Optional[Union[str, Sequence[str]]] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """List all disconnected channels—i.e.,
+        channels that were once connected to other workspaces and then disconnected—and
+        the corresponding original channel IDs for key revocation with EKM.
+        https://docs.slack.dev/reference/methods/admin.conversations.ekm.listOriginalConnectedChannelInfo
+        """
+        kwargs.update(
+            {
+                "cursor": cursor,
+                "limit": limit,
+            }
+        )
+        if isinstance(channel_ids, (list, tuple)):
+            kwargs.update({"channel_ids": ",".join(channel_ids)})
+        else:
+            kwargs.update({"channel_ids": channel_ids})
+        if isinstance(team_ids, (list, tuple)):
+            kwargs.update({"team_ids": ",".join(team_ids)})
+        else:
+            kwargs.update({"team_ids": team_ids})
+        return self.api_call("admin.conversations.ekm.listOriginalConnectedChannelInfo", params=kwargs)
+
+    def admin_conversations_restrictAccess_addGroup(
+        self,
+        *,
+        channel_id: str,
+        group_id: str,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Add an allowlist of IDP groups for accessing a channel.
+        https://docs.slack.dev/reference/methods/admin.conversations.restrictAccess.addGroup
+        """
+        kwargs.update(
+            {
+                "channel_id": channel_id,
+                "group_id": group_id,
+                "team_id": team_id,
+            }
+        )
+        return self.api_call(
+            "admin.conversations.restrictAccess.addGroup",
+            http_verb="GET",
+            params=kwargs,
+        )
+
+    def admin_conversations_restrictAccess_listGroups(
+        self,
+        *,
+        channel_id: str,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """List all IDP Groups linked to a channel.
+        https://docs.slack.dev/reference/methods/admin.conversations.restrictAccess.listGroups
+        """
+        kwargs.update(
+            {
+                "channel_id": channel_id,
+                "team_id": team_id,
+            }
+        )
+        return self.api_call(
+            "admin.conversations.restrictAccess.listGroups",
+            http_verb="GET",
+            params=kwargs,
+        )
+
+    def admin_conversations_restrictAccess_removeGroup(
+        self,
+        *,
+        channel_id: str,
+        group_id: str,
+        team_id: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Remove a linked IDP group linked from a private channel.
+        https://docs.slack.dev/reference/methods/admin.conversations.restrictAccess.removeGroup
+        """
+        kwargs.update(
+            {
+                "channel_id": channel_id,
+                "group_id": group_id,
+                "team_id": team_id,
+            }
+        )
+        return self.api_call(
+            "admin.conversations.restrictAccess.removeGroup",
+            http_verb="GET",
+            params=kwargs,
+        )
+
+    def admin_conversations_setTeams(
+        self,
+        *,
+        channel_id: str,
+        org_channel: Optional[bool] = None,
+        target_team_ids: Optional[Union[str, Sequence[str]]] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Set the workspaces in an Enterprise grid org that connect to a public or private channel.
+        https://docs.slack.dev/reference/methods/admin.conversations.setTeams
+        """
+        kwargs.update(
+            {
+                "channel_id": channel_id,
+                "org_channel": org_channel,
+                "team_id": team_id,
+            }
+        )
+        if isinstance(target_team_ids, (list, tuple)):
+            kwargs.update({"target_team_ids": ",".join(target_team_ids)})
+        else:
+            kwargs.update({"target_team_ids": target_team_ids})
+        return self.api_call("admin.conversations.setTeams", params=kwargs)
+
+    def admin_conversations_getTeams(
+        self,
+        *,
+        channel_id: str,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Set the workspaces in an Enterprise grid org that connect to a channel.
+        https://docs.slack.dev/reference/methods/admin.conversations.getTeams
+        """
+        kwargs.update(
+            {
+                "channel_id": channel_id,
+                "cursor": cursor,
+                "limit": limit,
+            }
+        )
+        return self.api_call("admin.conversations.getTeams", params=kwargs)
+
+    def admin_conversations_getCustomRetention(
+        self,
+        *,
+        channel_id: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Get a channel's retention policy
+        https://docs.slack.dev/reference/methods/admin.conversations.getCustomRetention
+        """
+        kwargs.update({"channel_id": channel_id})
+        return self.api_call("admin.conversations.getCustomRetention", params=kwargs)
+
+    def admin_conversations_removeCustomRetention(
+        self,
+        *,
+        channel_id: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Remove a channel's retention policy
+        https://docs.slack.dev/reference/methods/admin.conversations.removeCustomRetention
+        """
+        kwargs.update({"channel_id": channel_id})
+        return self.api_call("admin.conversations.removeCustomRetention", params=kwargs)
+
+    def admin_conversations_setCustomRetention(
+        self,
+        *,
+        channel_id: str,
+        duration_days: int,
+        **kwargs,
+    ) -> SlackResponse:
+        """Set a channel's retention policy
+        https://docs.slack.dev/reference/methods/admin.conversations.setCustomRetention
+        """
+        kwargs.update({"channel_id": channel_id, "duration_days": duration_days})
+        return self.api_call("admin.conversations.setCustomRetention", params=kwargs)
+
+    def admin_conversations_bulkArchive(
+        self,
+        *,
+        channel_ids: Union[Sequence[str], str],
+        **kwargs,
+    ) -> SlackResponse:
+        """Archive public or private channels in bulk.
+        https://docs.slack.dev/reference/methods/admin.conversations.bulkArchive
+        """
+        kwargs.update({"channel_ids": ",".join(channel_ids) if isinstance(channel_ids, (list, tuple)) else channel_ids})
+        return self.api_call("admin.conversations.bulkArchive", params=kwargs)
+
+    def admin_conversations_bulkDelete(
+        self,
+        *,
+        channel_ids: Union[Sequence[str], str],
+        **kwargs,
+    ) -> SlackResponse:
+        """Delete public or private channels in bulk.
+        https://slack.com/api/admin.conversations.bulkDelete
+        """
+        kwargs.update({"channel_ids": ",".join(channel_ids) if isinstance(channel_ids, (list, tuple)) else channel_ids})
+        return self.api_call("admin.conversations.bulkDelete", params=kwargs)
+
+    def admin_conversations_bulkMove(
+        self,
+        *,
+        channel_ids: Union[Sequence[str], str],
+        target_team_id: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Move public or private channels in bulk.
+        https://docs.slack.dev/reference/methods/admin.conversations.bulkMove
+        """
+        kwargs.update(
+            {
+                "target_team_id": target_team_id,
+                "channel_ids": ",".join(channel_ids) if isinstance(channel_ids, (list, tuple)) else channel_ids,
+            }
+        )
+        return self.api_call("admin.conversations.bulkMove", params=kwargs)
+
+    def admin_emoji_add(
+        self,
+        *,
+        name: str,
+        url: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Add an emoji.
+        https://docs.slack.dev/reference/methods/admin.emoji.add
+        """
+        kwargs.update({"name": name, "url": url})
+        return self.api_call("admin.emoji.add", http_verb="GET", params=kwargs)
+
+    def admin_emoji_addAlias(
+        self,
+        *,
+        alias_for: str,
+        name: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Add an emoji alias.
+        https://docs.slack.dev/reference/methods/admin.emoji.addAlias
+        """
+        kwargs.update({"alias_for": alias_for, "name": name})
+        return self.api_call("admin.emoji.addAlias", http_verb="GET", params=kwargs)
+
+    def admin_emoji_list(
+        self,
+        *,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """List emoji for an Enterprise Grid organization.
+        https://docs.slack.dev/reference/methods/admin.emoji.list
+        """
+        kwargs.update({"cursor": cursor, "limit": limit})
+        return self.api_call("admin.emoji.list", http_verb="GET", params=kwargs)
+
+    def admin_emoji_remove(
+        self,
+        *,
+        name: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Remove an emoji across an Enterprise Grid organization.
+        https://docs.slack.dev/reference/methods/admin.emoji.remove
+        """
+        kwargs.update({"name": name})
+        return self.api_call("admin.emoji.remove", http_verb="GET", params=kwargs)
+
+    def admin_emoji_rename(
+        self,
+        *,
+        name: str,
+        new_name: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Rename an emoji.
+        https://docs.slack.dev/reference/methods/admin.emoji.rename
+        """
+        kwargs.update({"name": name, "new_name": new_name})
+        return self.api_call("admin.emoji.rename", http_verb="GET", params=kwargs)
+
+    def admin_functions_list(
+        self,
+        *,
+        app_ids: Union[str, Sequence[str]],
+        team_id: Optional[str] = None,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Look up functions by a set of apps
+        https://docs.slack.dev/reference/methods/admin.functions.list
+        """
+        if isinstance(app_ids, (list, tuple)):
+            kwargs.update({"app_ids": ",".join(app_ids)})
+        else:
+            kwargs.update({"app_ids": app_ids})
+        kwargs.update(
+            {
+                "team_id": team_id,
+                "cursor": cursor,
+                "limit": limit,
+            }
+        )
+        return self.api_call("admin.functions.list", params=kwargs)
+
+    def admin_functions_permissions_lookup(
+        self,
+        *,
+        function_ids: Union[str, Sequence[str]],
+        **kwargs,
+    ) -> SlackResponse:
+        """Lookup the visibility of multiple Slack functions
+        and include the users if it is limited to particular named entities.
+        https://docs.slack.dev/reference/methods/admin.functions.permissions.lookup
+        """
+        if isinstance(function_ids, (list, tuple)):
+            kwargs.update({"function_ids": ",".join(function_ids)})
+        else:
+            kwargs.update({"function_ids": function_ids})
+        return self.api_call("admin.functions.permissions.lookup", params=kwargs)
+
+    def admin_functions_permissions_set(
+        self,
+        *,
+        function_id: str,
+        visibility: str,
+        user_ids: Optional[Union[str, Sequence[str]]] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Set the visibility of a Slack function
+        and define the users or workspaces if it is set to named_entities
+        https://docs.slack.dev/reference/methods/admin.functions.permissions.set
+        """
+        kwargs.update(
+            {
+                "function_id": function_id,
+                "visibility": visibility,
+            }
+        )
+        if user_ids is not None:
+            if isinstance(user_ids, (list, tuple)):
+                kwargs.update({"user_ids": ",".join(user_ids)})
+            else:
+                kwargs.update({"user_ids": user_ids})
+        return self.api_call("admin.functions.permissions.set", params=kwargs)
+
+    def admin_roles_addAssignments(
+        self,
+        *,
+        role_id: str,
+        entity_ids: Union[str, Sequence[str]],
+        user_ids: Union[str, Sequence[str]],
+        **kwargs,
+    ) -> SlackResponse:
+        """Adds members to the specified role with the specified scopes
+        https://docs.slack.dev/reference/methods/admin.roles.addAssignments
+        """
+        kwargs.update({"role_id": role_id})
+        if isinstance(entity_ids, (list, tuple)):
+            kwargs.update({"entity_ids": ",".join(entity_ids)})
+        else:
+            kwargs.update({"entity_ids": entity_ids})
+        if isinstance(user_ids, (list, tuple)):
+            kwargs.update({"user_ids": ",".join(user_ids)})
+        else:
+            kwargs.update({"user_ids": user_ids})
+        return self.api_call("admin.roles.addAssignments", params=kwargs)
+
+    def admin_roles_listAssignments(
+        self,
+        *,
+        role_ids: Optional[Union[str, Sequence[str]]] = None,
+        entity_ids: Optional[Union[str, Sequence[str]]] = None,
+        cursor: Optional[str] = None,
+        limit: Optional[Union[str, int]] = None,
+        sort_dir: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Lists assignments for all roles across entities.
+            Options to scope results by any combination of roles or entities
+        https://docs.slack.dev/reference/methods/admin.roles.listAssignments
+        """
+        kwargs.update({"cursor": cursor, "limit": limit, "sort_dir": sort_dir})
+        if isinstance(entity_ids, (list, tuple)):
+            kwargs.update({"entity_ids": ",".join(entity_ids)})
+        else:
+            kwargs.update({"entity_ids": entity_ids})
+        if isinstance(role_ids, (list, tuple)):
+            kwargs.update({"role_ids": ",".join(role_ids)})
+        else:
+            kwargs.update({"role_ids": role_ids})
+        return self.api_call("admin.roles.listAssignments", params=kwargs)
+
+    def admin_roles_removeAssignments(
+        self,
+        *,
+        role_id: str,
+        entity_ids: Union[str, Sequence[str]],
+        user_ids: Union[str, Sequence[str]],
+        **kwargs,
+    ) -> SlackResponse:
+        """Removes a set of users from a role for the given scopes and entities
+        https://docs.slack.dev/reference/methods/admin.roles.removeAssignments
+        """
+        kwargs.update({"role_id": role_id})
+        if isinstance(entity_ids, (list, tuple)):
+            kwargs.update({"entity_ids": ",".join(entity_ids)})
+        else:
+            kwargs.update({"entity_ids": entity_ids})
+        if isinstance(user_ids, (list, tuple)):
+            kwargs.update({"user_ids": ",".join(user_ids)})
+        else:
+            kwargs.update({"user_ids": user_ids})
+        return self.api_call("admin.roles.removeAssignments", params=kwargs)
+
+    def admin_users_session_reset(
+        self,
+        *,
+        user_id: str,
+        mobile_only: Optional[bool] = None,
+        web_only: Optional[bool] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Wipes all valid sessions on all devices for a given user.
+        https://docs.slack.dev/reference/methods/admin.users.session.reset
+        """
+        kwargs.update(
+            {
+                "user_id": user_id,
+                "mobile_only": mobile_only,
+                "web_only": web_only,
+            }
+        )
+        return self.api_call("admin.users.session.reset", params=kwargs)
+
+    def admin_users_session_resetBulk(
+        self,
+        *,
+        user_ids: Union[str, Sequence[str]],
+        mobile_only: Optional[bool] = None,
+        web_only: Optional[bool] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Enqueues an asynchronous job to wipe all valid sessions on all devices for a given list of users
+        https://docs.slack.dev/reference/methods/admin.users.session.resetBulk
+        """
+        if isinstance(user_ids, (list, tuple)):
+            kwargs.update({"user_ids": ",".join(user_ids)})
+        else:
+            kwargs.update({"user_ids": user_ids})
+        kwargs.update(
+            {
+                "mobile_only": mobile_only,
+                "web_only": web_only,
+            }
+        )
+        return self.api_call("admin.users.session.resetBulk", params=kwargs)
+
+    def admin_users_session_invalidate(
+        self,
+        *,
+        session_id: str,
+        team_id: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Invalidate a single session for a user by session_id.
+        https://docs.slack.dev/reference/methods/admin.users.session.invalidate
+        """
+        kwargs.update({"session_id": session_id, "team_id": team_id})
+        return self.api_call("admin.users.session.invalidate", params=kwargs)
+
+    def admin_users_session_list(
+        self,
+        *,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        team_id: Optional[str] = None,
+        user_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Lists all active user sessions for an organization
+        https://docs.slack.dev/reference/methods/admin.users.session.list
+        """
+        kwargs.update(
+            {
+                "cursor": cursor,
+                "limit": limit,
+                "team_id": team_id,
+                "user_id": user_id,
+            }
+        )
+        return self.api_call("admin.users.session.list", params=kwargs)
+
+    def admin_teams_settings_setDefaultChannels(
+        self,
+        *,
+        team_id: str,
+        channel_ids: Union[str, Sequence[str]],
+        **kwargs,
+    ) -> SlackResponse:
+        """Set the default channels of a workspace.
+        https://docs.slack.dev/reference/methods/admin.teams.settings.setDefaultChannels
+        """
+        kwargs.update({"team_id": team_id})
+        if isinstance(channel_ids, (list, tuple)):
+            kwargs.update({"channel_ids": ",".join(channel_ids)})
+        else:
+            kwargs.update({"channel_ids": channel_ids})
+        return self.api_call("admin.teams.settings.setDefaultChannels", http_verb="GET", params=kwargs)
+
+    def admin_users_session_getSettings(
+        self,
+        *,
+        user_ids: Union[str, Sequence[str]],
+        **kwargs,
+    ) -> SlackResponse:
+        """Get user-specific session settings—the session duration
+        and what happens when the client closes—given a list of users.
+        https://docs.slack.dev/reference/methods/admin.users.session.getSettings
+        """
+        if isinstance(user_ids, (list, tuple)):
+            kwargs.update({"user_ids": ",".join(user_ids)})
+        else:
+            kwargs.update({"user_ids": user_ids})
+        return self.api_call("admin.users.session.getSettings", params=kwargs)
+
+    def admin_users_session_setSettings(
+        self,
+        *,
+        user_ids: Union[str, Sequence[str]],
+        desktop_app_browser_quit: Optional[bool] = None,
+        duration: Optional[int] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Configure the user-level session settings—the session duration
+        and what happens when the client closes—for one or more users.
+        https://docs.slack.dev/reference/methods/admin.users.session.setSettings
+        """
+        if isinstance(user_ids, (list, tuple)):
+            kwargs.update({"user_ids": ",".join(user_ids)})
+        else:
+            kwargs.update({"user_ids": user_ids})
+        kwargs.update(
+            {
+                "desktop_app_browser_quit": desktop_app_browser_quit,
+                "duration": duration,
+            }
+        )
+        return self.api_call("admin.users.session.setSettings", params=kwargs)
+
+    def admin_users_session_clearSettings(
+        self,
+        *,
+        user_ids: Union[str, Sequence[str]],
+        **kwargs,
+    ) -> SlackResponse:
+        """Clear user-specific session settings—the session duration
+        and what happens when the client closes—for a list of users.
+        https://docs.slack.dev/reference/methods/admin.users.session.clearSettings
+        """
+        if isinstance(user_ids, (list, tuple)):
+            kwargs.update({"user_ids": ",".join(user_ids)})
+        else:
+            kwargs.update({"user_ids": user_ids})
+        return self.api_call("admin.users.session.clearSettings", params=kwargs)
+
+    def admin_users_unsupportedVersions_export(
+        self,
+        *,
+        date_end_of_support: Optional[Union[str, int]] = None,
+        date_sessions_started: Optional[Union[str, int]] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Ask Slackbot to send you an export listing all workspace members using unsupported software,
+        presented as a zipped CSV file.
+        https://docs.slack.dev/reference/methods/admin.users.unsupportedVersions.export
+        """
+        kwargs.update(
+            {
+                "date_end_of_support": date_end_of_support,
+                "date_sessions_started": date_sessions_started,
+            }
+        )
+        return self.api_call("admin.users.unsupportedVersions.export", params=kwargs)
+
+    def admin_inviteRequests_approve(
+        self,
+        *,
+        invite_request_id: str,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Approve a workspace invite request.
+        https://docs.slack.dev/reference/methods/admin.inviteRequests.approve
+        """
+        kwargs.update({"invite_request_id": invite_request_id, "team_id": team_id})
+        return self.api_call("admin.inviteRequests.approve", params=kwargs)
+
+    def admin_inviteRequests_approved_list(
+        self,
+        *,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """List all approved workspace invite requests.
+        https://docs.slack.dev/reference/methods/admin.inviteRequests.approved.list
+        """
+        kwargs.update(
+            {
+                "cursor": cursor,
+                "limit": limit,
+                "team_id": team_id,
+            }
+        )
+        return self.api_call("admin.inviteRequests.approved.list", params=kwargs)
+
+    def admin_inviteRequests_denied_list(
+        self,
+        *,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """List all denied workspace invite requests.
+        https://docs.slack.dev/reference/methods/admin.inviteRequests.denied.list
+        """
+        kwargs.update(
+            {
+                "cursor": cursor,
+                "limit": limit,
+                "team_id": team_id,
+            }
+        )
+        return self.api_call("admin.inviteRequests.denied.list", params=kwargs)
+
+    def admin_inviteRequests_deny(
+        self,
+        *,
+        invite_request_id: str,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Deny a workspace invite request.
+        https://docs.slack.dev/reference/methods/admin.inviteRequests.deny
+        """
+        kwargs.update({"invite_request_id": invite_request_id, "team_id": team_id})
+        return self.api_call("admin.inviteRequests.deny", params=kwargs)
+
+    def admin_inviteRequests_list(
+        self,
+        **kwargs,
+    ) -> SlackResponse:
+        """List all pending workspace invite requests."""
+        return self.api_call("admin.inviteRequests.list", params=kwargs)
+
+    def admin_teams_admins_list(
+        self,
+        *,
+        team_id: str,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """List all of the admins on a given workspace.
+        https://docs.slack.dev/reference/methods/admin.inviteRequests.list
+        """
+        kwargs.update(
+            {
+                "cursor": cursor,
+                "limit": limit,
+                "team_id": team_id,
+            }
+        )
+        return self.api_call("admin.teams.admins.list", http_verb="GET", params=kwargs)
+
+    def admin_teams_create(
+        self,
+        *,
+        team_domain: str,
+        team_name: str,
+        team_description: Optional[str] = None,
+        team_discoverability: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Create an Enterprise team.
+        https://docs.slack.dev/reference/methods/admin.teams.create
+        """
+        kwargs.update(
+            {
+                "team_domain": team_domain,
+                "team_name": team_name,
+                "team_description": team_description,
+                "team_discoverability": team_discoverability,
+            }
+        )
+        return self.api_call("admin.teams.create", params=kwargs)
+
+    def admin_teams_list(
+        self,
+        *,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """List all teams on an Enterprise organization.
+        https://docs.slack.dev/reference/methods/admin.teams.list
+        """
+        kwargs.update({"cursor": cursor, "limit": limit})
+        return self.api_call("admin.teams.list", params=kwargs)
+
+    def admin_teams_owners_list(
+        self,
+        *,
+        team_id: str,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """List all of the admins on a given workspace.
+        https://docs.slack.dev/reference/methods/admin.teams.owners.list
+        """
+        kwargs.update({"team_id": team_id, "cursor": cursor, "limit": limit})
+        return self.api_call("admin.teams.owners.list", http_verb="GET", params=kwargs)
+
+    def admin_teams_settings_info(
+        self,
+        *,
+        team_id: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Fetch information about settings in a workspace
+        https://docs.slack.dev/reference/methods/admin.teams.settings.info
+        """
+        kwargs.update({"team_id": team_id})
+        return self.api_call("admin.teams.settings.info", params=kwargs)
+
+    def admin_teams_settings_setDescription(
+        self,
+        *,
+        team_id: str,
+        description: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Set the description of a given workspace.
+        https://docs.slack.dev/reference/methods/admin.teams.settings.setDescription
+        """
+        kwargs.update({"team_id": team_id, "description": description})
+        return self.api_call("admin.teams.settings.setDescription", params=kwargs)
+
+    def admin_teams_settings_setDiscoverability(
+        self,
+        *,
+        team_id: str,
+        discoverability: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Sets the icon of a workspace.
+        https://docs.slack.dev/reference/methods/admin.teams.settings.setDiscoverability
+        """
+        kwargs.update({"team_id": team_id, "discoverability": discoverability})
+        return self.api_call("admin.teams.settings.setDiscoverability", params=kwargs)
+
+    def admin_teams_settings_setIcon(
+        self,
+        *,
+        team_id: str,
+        image_url: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Sets the icon of a workspace.
+        https://docs.slack.dev/reference/methods/admin.teams.settings.setIcon
+        """
+        kwargs.update({"team_id": team_id, "image_url": image_url})
+        return self.api_call("admin.teams.settings.setIcon", http_verb="GET", params=kwargs)
+
+    def admin_teams_settings_setName(
+        self,
+        *,
+        team_id: str,
+        name: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Sets the icon of a workspace.
+        https://docs.slack.dev/reference/methods/admin.teams.settings.setName
+        """
+        kwargs.update({"team_id": team_id, "name": name})
+        return self.api_call("admin.teams.settings.setName", params=kwargs)
+
+    def admin_usergroups_addChannels(
+        self,
+        *,
+        channel_ids: Union[str, Sequence[str]],
+        usergroup_id: str,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Add one or more default channels to an IDP group.
+        https://docs.slack.dev/reference/methods/admin.usergroups.addChannels
+        """
+        kwargs.update({"team_id": team_id, "usergroup_id": usergroup_id})
+        if isinstance(channel_ids, (list, tuple)):
+            kwargs.update({"channel_ids": ",".join(channel_ids)})
+        else:
+            kwargs.update({"channel_ids": channel_ids})
+        return self.api_call("admin.usergroups.addChannels", params=kwargs)
+
+    def admin_usergroups_addTeams(
+        self,
+        *,
+        usergroup_id: str,
+        team_ids: Union[str, Sequence[str]],
+        auto_provision: Optional[bool] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Associate one or more default workspaces with an organization-wide IDP group.
+        https://docs.slack.dev/reference/methods/admin.usergroups.addTeams
+        """
+        kwargs.update({"usergroup_id": usergroup_id, "auto_provision": auto_provision})
+        if isinstance(team_ids, (list, tuple)):
+            kwargs.update({"team_ids": ",".join(team_ids)})
+        else:
+            kwargs.update({"team_ids": team_ids})
+        return self.api_call("admin.usergroups.addTeams", params=kwargs)
+
+    def admin_usergroups_listChannels(
+        self,
+        *,
+        usergroup_id: str,
+        include_num_members: Optional[bool] = None,
+        team_id: Optional[bool] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Add one or more default channels to an IDP group.
+        https://docs.slack.dev/reference/methods/admin.usergroups.listChannels
+        """
+        kwargs.update(
+            {
+                "usergroup_id": usergroup_id,
+                "include_num_members": include_num_members,
+                "team_id": team_id,
+            }
+        )
+        return self.api_call("admin.usergroups.listChannels", params=kwargs)
+
+    def admin_usergroups_removeChannels(
+        self,
+        *,
+        usergroup_id: str,
+        channel_ids: Union[str, Sequence[str]],
+        **kwargs,
+    ) -> SlackResponse:
+        """Add one or more default channels to an IDP group.
+        https://docs.slack.dev/reference/methods/admin.usergroups.removeChannels
+        """
+        kwargs.update({"usergroup_id": usergroup_id})
+        if isinstance(channel_ids, (list, tuple)):
+            kwargs.update({"channel_ids": ",".join(channel_ids)})
+        else:
+            kwargs.update({"channel_ids": channel_ids})
+        return self.api_call("admin.usergroups.removeChannels", params=kwargs)
+
+    def admin_users_assign(
+        self,
+        *,
+        team_id: str,
+        user_id: str,
+        channel_ids: Optional[Union[str, Sequence[str]]] = None,
+        is_restricted: Optional[bool] = None,
+        is_ultra_restricted: Optional[bool] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Add an Enterprise user to a workspace.
+        https://docs.slack.dev/reference/methods/admin.users.assign
+        """
+        kwargs.update(
+            {
+                "team_id": team_id,
+                "user_id": user_id,
+                "is_restricted": is_restricted,
+                "is_ultra_restricted": is_ultra_restricted,
+            }
+        )
+        if isinstance(channel_ids, (list, tuple)):
+            kwargs.update({"channel_ids": ",".join(channel_ids)})
+        else:
+            kwargs.update({"channel_ids": channel_ids})
+        return self.api_call("admin.users.assign", params=kwargs)
+
+    def admin_users_invite(
+        self,
+        *,
+        team_id: str,
+        email: str,
+        channel_ids: Union[str, Sequence[str]],
+        custom_message: Optional[str] = None,
+        email_password_policy_enabled: Optional[bool] = None,
+        guest_expiration_ts: Optional[Union[str, float]] = None,
+        is_restricted: Optional[bool] = None,
+        is_ultra_restricted: Optional[bool] = None,
+        real_name: Optional[str] = None,
+        resend: Optional[bool] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Invite a user to a workspace.
+        https://docs.slack.dev/reference/methods/admin.users.invite
+        """
+        kwargs.update(
+            {
+                "team_id": team_id,
+                "email": email,
+                "custom_message": custom_message,
+                "email_password_policy_enabled": email_password_policy_enabled,
+                "guest_expiration_ts": str(guest_expiration_ts) if guest_expiration_ts is not None else None,
+                "is_restricted": is_restricted,
+                "is_ultra_restricted": is_ultra_restricted,
+                "real_name": real_name,
+                "resend": resend,
+            }
+        )
+        if isinstance(channel_ids, (list, tuple)):
+            kwargs.update({"channel_ids": ",".join(channel_ids)})
+        else:
+            kwargs.update({"channel_ids": channel_ids})
+        return self.api_call("admin.users.invite", params=kwargs)
+
+    def admin_users_list(
+        self,
+        *,
+        team_id: Optional[str] = None,
+        include_deactivated_user_workspaces: Optional[bool] = None,
+        is_active: Optional[bool] = None,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """List users on a workspace
+        https://docs.slack.dev/reference/methods/admin.users.list
+        """
+        kwargs.update(
+            {
+                "team_id": team_id,
+                "include_deactivated_user_workspaces": include_deactivated_user_workspaces,
+                "is_active": is_active,
+                "cursor": cursor,
+                "limit": limit,
+            }
+        )
+        return self.api_call("admin.users.list", params=kwargs)
+
+    def admin_users_remove(
+        self,
+        *,
+        team_id: str,
+        user_id: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Remove a user from a workspace.
+        https://docs.slack.dev/reference/methods/admin.users.remove
+        """
+        kwargs.update({"team_id": team_id, "user_id": user_id})
+        return self.api_call("admin.users.remove", params=kwargs)
+
+    def admin_users_setAdmin(
+        self,
+        *,
+        team_id: str,
+        user_id: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Set an existing guest, regular user, or owner to be an admin user.
+        https://docs.slack.dev/reference/methods/admin.users.setAdmin
+        """
+        kwargs.update({"team_id": team_id, "user_id": user_id})
+        return self.api_call("admin.users.setAdmin", params=kwargs)
+
+    def admin_users_setExpiration(
+        self,
+        *,
+        expiration_ts: int,
+        user_id: str,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Set an expiration for a guest user.
+        https://docs.slack.dev/reference/methods/admin.users.setExpiration
+        """
+        kwargs.update({"expiration_ts": expiration_ts, "team_id": team_id, "user_id": user_id})
+        return self.api_call("admin.users.setExpiration", params=kwargs)
+
+    def admin_users_setOwner(
+        self,
+        *,
+        team_id: str,
+        user_id: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Set an existing guest, regular user, or admin user to be a workspace owner.
+        https://docs.slack.dev/reference/methods/admin.users.setOwner
+        """
+        kwargs.update({"team_id": team_id, "user_id": user_id})
+        return self.api_call("admin.users.setOwner", params=kwargs)
+
+    def admin_users_setRegular(
+        self,
+        *,
+        team_id: str,
+        user_id: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Set an existing guest user, admin user, or owner to be a regular user.
+        https://docs.slack.dev/reference/methods/admin.users.setRegular
+        """
+        kwargs.update({"team_id": team_id, "user_id": user_id})
+        return self.api_call("admin.users.setRegular", params=kwargs)
+
+    def admin_workflows_search(
+        self,
+        *,
+        app_id: Optional[str] = None,
+        collaborator_ids: Optional[Union[str, Sequence[str]]] = None,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        no_collaborators: Optional[bool] = None,
+        num_trigger_ids: Optional[int] = None,
+        query: Optional[str] = None,
+        sort: Optional[str] = None,
+        sort_dir: Optional[str] = None,
+        source: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Search workflows within the team or enterprise
+        https://docs.slack.dev/reference/methods/admin.workflows.search
+        """
+        if collaborator_ids is not None:
+            if isinstance(collaborator_ids, (list, tuple)):
+                kwargs.update({"collaborator_ids": ",".join(collaborator_ids)})
+            else:
+                kwargs.update({"collaborator_ids": collaborator_ids})
+        kwargs.update(
+            {
+                "app_id": app_id,
+                "cursor": cursor,
+                "limit": limit,
+                "no_collaborators": no_collaborators,
+                "num_trigger_ids": num_trigger_ids,
+                "query": query,
+                "sort": sort,
+                "sort_dir": sort_dir,
+                "source": source,
+            }
+        )
+        return self.api_call("admin.workflows.search", params=kwargs)
+
+    def admin_workflows_permissions_lookup(
+        self,
+        *,
+        workflow_ids: Union[str, Sequence[str]],
+        max_workflow_triggers: Optional[int] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Look up the permissions for a set of workflows
+        https://docs.slack.dev/reference/methods/admin.workflows.permissions.lookup
+        """
+        if isinstance(workflow_ids, (list, tuple)):
+            kwargs.update({"workflow_ids": ",".join(workflow_ids)})
+        else:
+            kwargs.update({"workflow_ids": workflow_ids})
+        kwargs.update(
+            {
+                "max_workflow_triggers": max_workflow_triggers,
+            }
+        )
+        return self.api_call("admin.workflows.permissions.lookup", params=kwargs)
+
+    def admin_workflows_collaborators_add(
+        self,
+        *,
+        collaborator_ids: Union[str, Sequence[str]],
+        workflow_ids: Union[str, Sequence[str]],
+        **kwargs,
+    ) -> SlackResponse:
+        """Add collaborators to workflows within the team or enterprise
+        https://docs.slack.dev/reference/methods/admin.workflows.collaborators.add
+        """
+        if isinstance(collaborator_ids, (list, tuple)):
+            kwargs.update({"collaborator_ids": ",".join(collaborator_ids)})
+        else:
+            kwargs.update({"collaborator_ids": collaborator_ids})
+        if isinstance(workflow_ids, (list, tuple)):
+            kwargs.update({"workflow_ids": ",".join(workflow_ids)})
+        else:
+            kwargs.update({"workflow_ids": workflow_ids})
+        return self.api_call("admin.workflows.collaborators.add", params=kwargs)
+
+    def admin_workflows_collaborators_remove(
+        self,
+        *,
+        collaborator_ids: Union[str, Sequence[str]],
+        workflow_ids: Union[str, Sequence[str]],
+        **kwargs,
+    ) -> SlackResponse:
+        """Remove collaborators from workflows within the team or enterprise
+        https://docs.slack.dev/reference/methods/admin.workflows.collaborators.remove
+        """
+        if isinstance(collaborator_ids, (list, tuple)):
+            kwargs.update({"collaborator_ids": ",".join(collaborator_ids)})
+        else:
+            kwargs.update({"collaborator_ids": collaborator_ids})
+        if isinstance(workflow_ids, (list, tuple)):
+            kwargs.update({"workflow_ids": ",".join(workflow_ids)})
+        else:
+            kwargs.update({"workflow_ids": workflow_ids})
+        return self.api_call("admin.workflows.collaborators.remove", params=kwargs)
+
+    def admin_workflows_unpublish(
+        self,
+        *,
+        workflow_ids: Union[str, Sequence[str]],
+        **kwargs,
+    ) -> SlackResponse:
+        """Unpublish workflows within the team or enterprise
+        https://docs.slack.dev/reference/methods/admin.workflows.unpublish
+        """
+        if isinstance(workflow_ids, (list, tuple)):
+            kwargs.update({"workflow_ids": ",".join(workflow_ids)})
+        else:
+            kwargs.update({"workflow_ids": workflow_ids})
+        return self.api_call("admin.workflows.unpublish", params=kwargs)
+
+    def api_test(
+        self,
+        *,
+        error: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Checks API calling code.
+        https://docs.slack.dev/reference/methods/api.test
+        """
+        kwargs.update({"error": error})
+        return self.api_call("api.test", params=kwargs)
+
+    def apps_connections_open(
+        self,
+        *,
+        app_token: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Generate a temporary Socket Mode WebSocket URL that your app can connect to
+        in order to receive events and interactive payloads
+        https://docs.slack.dev/reference/methods/apps.connections.open
+        """
+        kwargs.update({"token": app_token})
+        return self.api_call("apps.connections.open", http_verb="POST", params=kwargs)
+
+    def apps_event_authorizations_list(
+        self,
+        *,
+        event_context: str,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Get a list of authorizations for the given event context.
+        Each authorization represents an app installation that the event is visible to.
+        https://docs.slack.dev/reference/methods/apps.event.authorizations.list
+        """
+        kwargs.update({"event_context": event_context, "cursor": cursor, "limit": limit})
+        return self.api_call("apps.event.authorizations.list", params=kwargs)
+
+    def apps_uninstall(
+        self,
+        *,
+        client_id: str,
+        client_secret: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Uninstalls your app from a workspace.
+        https://docs.slack.dev/reference/methods/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://docs.slack.dev/reference/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://docs.slack.dev/reference/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://docs.slack.dev/reference/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://docs.slack.dev/reference/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://docs.slack.dev/reference/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://docs.slack.dev/reference/methods/tooling.tokens.rotate
+        """
+        kwargs.update({"refresh_token": refresh_token})
+        return self.api_call("tooling.tokens.rotate", params=kwargs)
+
+    def assistant_threads_setStatus(
+        self,
+        *,
+        channel_id: str,
+        thread_ts: str,
+        status: str,
+        loading_messages: Optional[List[str]] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Set the status for an AI assistant thread.
+        https://docs.slack.dev/reference/methods/assistant.threads.setStatus
+        """
+        kwargs.update(
+            {"channel_id": channel_id, "thread_ts": thread_ts, "status": status, "loading_messages": loading_messages}
+        )
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("assistant.threads.setStatus", json=kwargs)
+
+    def assistant_threads_setTitle(
+        self,
+        *,
+        channel_id: str,
+        thread_ts: str,
+        title: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Set the title for the given assistant thread.
+        https://docs.slack.dev/reference/methods/assistant.threads.setTitle
+        """
+        kwargs.update({"channel_id": channel_id, "thread_ts": thread_ts, "title": title})
+        return self.api_call("assistant.threads.setTitle", params=kwargs)
+
+    def assistant_threads_setSuggestedPrompts(
+        self,
+        *,
+        channel_id: str,
+        thread_ts: str,
+        title: Optional[str] = None,
+        prompts: List[Dict[str, str]],
+        **kwargs,
+    ) -> SlackResponse:
+        """Set suggested prompts for the given assistant thread.
+        https://docs.slack.dev/reference/methods/assistant.threads.setSuggestedPrompts
+        """
+        kwargs.update({"channel_id": channel_id, "thread_ts": thread_ts, "prompts": prompts})
+        if title is not None:
+            kwargs.update({"title": title})
+        return self.api_call("assistant.threads.setSuggestedPrompts", json=kwargs)
+
+    def auth_revoke(
+        self,
+        *,
+        test: Optional[bool] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Revokes a token.
+        https://docs.slack.dev/reference/methods/auth.revoke
+        """
+        kwargs.update({"test": test})
+        return self.api_call("auth.revoke", http_verb="GET", params=kwargs)
+
+    def auth_test(
+        self,
+        **kwargs,
+    ) -> SlackResponse:
+        """Checks authentication & identity.
+        https://docs.slack.dev/reference/methods/auth.test
+        """
+        return self.api_call("auth.test", params=kwargs)
+
+    def auth_teams_list(
+        self,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        include_icon: Optional[bool] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """List the workspaces a token can access.
+        https://docs.slack.dev/reference/methods/auth.teams.list
+        """
+        kwargs.update({"cursor": cursor, "limit": limit, "include_icon": include_icon})
+        return self.api_call("auth.teams.list", params=kwargs)
+
+    def bookmarks_add(
+        self,
+        *,
+        channel_id: str,
+        title: str,
+        type: str,
+        emoji: Optional[str] = None,
+        entity_id: Optional[str] = None,
+        link: Optional[str] = None,  # include when type is 'link'
+        parent_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Add bookmark to a channel.
+        https://docs.slack.dev/reference/methods/bookmarks.add
+        """
+        kwargs.update(
+            {
+                "channel_id": channel_id,
+                "title": title,
+                "type": type,
+                "emoji": emoji,
+                "entity_id": entity_id,
+                "link": link,
+                "parent_id": parent_id,
+            }
+        )
+        return self.api_call("bookmarks.add", http_verb="POST", params=kwargs)
+
+    def bookmarks_edit(
+        self,
+        *,
+        bookmark_id: str,
+        channel_id: str,
+        emoji: Optional[str] = None,
+        link: Optional[str] = None,
+        title: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Edit bookmark.
+        https://docs.slack.dev/reference/methods/bookmarks.edit
+        """
+        kwargs.update(
+            {
+                "bookmark_id": bookmark_id,
+                "channel_id": channel_id,
+                "emoji": emoji,
+                "link": link,
+                "title": title,
+            }
+        )
+        return self.api_call("bookmarks.edit", http_verb="POST", params=kwargs)
+
+    def bookmarks_list(
+        self,
+        *,
+        channel_id: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """List bookmark for the channel.
+        https://docs.slack.dev/reference/methods/bookmarks.list
+        """
+        kwargs.update({"channel_id": channel_id})
+        return self.api_call("bookmarks.list", http_verb="POST", params=kwargs)
+
+    def bookmarks_remove(
+        self,
+        *,
+        bookmark_id: str,
+        channel_id: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Remove bookmark from the channel.
+        https://docs.slack.dev/reference/methods/bookmarks.remove
+        """
+        kwargs.update({"bookmark_id": bookmark_id, "channel_id": channel_id})
+        return self.api_call("bookmarks.remove", http_verb="POST", params=kwargs)
+
+    def bots_info(
+        self,
+        *,
+        bot: Optional[str] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Gets information about a bot user.
+        https://docs.slack.dev/reference/methods/bots.info
+        """
+        kwargs.update({"bot": bot, "team_id": team_id})
+        return self.api_call("bots.info", http_verb="GET", params=kwargs)
+
+    def calls_add(
+        self,
+        *,
+        external_unique_id: str,
+        join_url: str,
+        created_by: Optional[str] = None,
+        date_start: Optional[int] = None,
+        desktop_app_join_url: Optional[str] = None,
+        external_display_id: Optional[str] = None,
+        title: Optional[str] = None,
+        users: Optional[Union[str, Sequence[Dict[str, str]]]] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Registers a new Call.
+        https://docs.slack.dev/reference/methods/calls.add
+        """
+        kwargs.update(
+            {
+                "external_unique_id": external_unique_id,
+                "join_url": join_url,
+                "created_by": created_by,
+                "date_start": date_start,
+                "desktop_app_join_url": desktop_app_join_url,
+                "external_display_id": external_display_id,
+                "title": title,
+            }
+        )
+        _update_call_participants(
+            kwargs,
+            users if users is not None else kwargs.get("users"),  # type: ignore[arg-type]
+        )
+        return self.api_call("calls.add", http_verb="POST", params=kwargs)
+
+    def calls_end(
+        self,
+        *,
+        id: str,
+        duration: Optional[int] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Ends a Call.
+        https://docs.slack.dev/reference/methods/calls.end
+        """
+        kwargs.update({"id": id, "duration": duration})
+        return self.api_call("calls.end", http_verb="POST", params=kwargs)
+
+    def calls_info(
+        self,
+        *,
+        id: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Returns information about a Call.
+        https://docs.slack.dev/reference/methods/calls.info
+        """
+        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, Sequence[Dict[str, str]]],
+        **kwargs,
+    ) -> SlackResponse:
+        """Registers new participants added to a Call.
+        https://docs.slack.dev/reference/methods/calls.participants.add
+        """
+        kwargs.update({"id": id})
+        _update_call_participants(kwargs, users)
+        return self.api_call("calls.participants.add", http_verb="POST", params=kwargs)
+
+    def calls_participants_remove(
+        self,
+        *,
+        id: str,
+        users: Union[str, Sequence[Dict[str, str]]],
+        **kwargs,
+    ) -> SlackResponse:
+        """Registers participants removed from a Call.
+        https://docs.slack.dev/reference/methods/calls.participants.remove
+        """
+        kwargs.update({"id": id})
+        _update_call_participants(kwargs, users)
+        return self.api_call("calls.participants.remove", http_verb="POST", params=kwargs)
+
+    def calls_update(
+        self,
+        *,
+        id: str,
+        desktop_app_join_url: Optional[str] = None,
+        join_url: Optional[str] = None,
+        title: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Updates information about a Call.
+        https://docs.slack.dev/reference/methods/calls.update
+        """
+        kwargs.update(
+            {
+                "id": id,
+                "desktop_app_join_url": desktop_app_join_url,
+                "join_url": join_url,
+                "title": title,
+            }
+        )
+        return self.api_call("calls.update", http_verb="POST", params=kwargs)
+
+    def canvases_create(
+        self,
+        *,
+        title: Optional[str] = None,
+        document_content: Dict[str, str],
+        **kwargs,
+    ) -> SlackResponse:
+        """Create Canvas for a user
+        https://docs.slack.dev/reference/methods/canvases.create
+        """
+        kwargs.update({"title": title, "document_content": document_content})
+        return self.api_call("canvases.create", json=kwargs)
+
+    def canvases_edit(
+        self,
+        *,
+        canvas_id: str,
+        changes: Sequence[Dict[str, Any]],
+        **kwargs,
+    ) -> SlackResponse:
+        """Update an existing canvas
+        https://docs.slack.dev/reference/methods/canvases.edit
+        """
+        kwargs.update({"canvas_id": canvas_id, "changes": changes})
+        return self.api_call("canvases.edit", json=kwargs)
+
+    def canvases_delete(
+        self,
+        *,
+        canvas_id: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Deletes a canvas
+        https://docs.slack.dev/reference/methods/canvases.delete
+        """
+        kwargs.update({"canvas_id": canvas_id})
+        return self.api_call("canvases.delete", params=kwargs)
+
+    def canvases_access_set(
+        self,
+        *,
+        canvas_id: str,
+        access_level: str,
+        channel_ids: Optional[Union[Sequence[str], str]] = None,
+        user_ids: Optional[Union[Sequence[str], str]] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Sets the access level to a canvas for specified entities
+        https://docs.slack.dev/reference/methods/canvases.access.set
+        """
+        kwargs.update({"canvas_id": canvas_id, "access_level": access_level})
+        if channel_ids is not None:
+            if isinstance(channel_ids, (list, tuple)):
+                kwargs.update({"channel_ids": ",".join(channel_ids)})
+            else:
+                kwargs.update({"channel_ids": channel_ids})
+        if user_ids is not None:
+            if isinstance(user_ids, (list, tuple)):
+                kwargs.update({"user_ids": ",".join(user_ids)})
+            else:
+                kwargs.update({"user_ids": user_ids})
+
+        return self.api_call("canvases.access.set", params=kwargs)
+
+    def canvases_access_delete(
+        self,
+        *,
+        canvas_id: str,
+        channel_ids: Optional[Union[Sequence[str], str]] = None,
+        user_ids: Optional[Union[Sequence[str], str]] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Create a Channel Canvas for a channel
+        https://docs.slack.dev/reference/methods/canvases.access.delete
+        """
+        kwargs.update({"canvas_id": canvas_id})
+        if channel_ids is not None:
+            if isinstance(channel_ids, (list, tuple)):
+                kwargs.update({"channel_ids": ",".join(channel_ids)})
+            else:
+                kwargs.update({"channel_ids": channel_ids})
+        if user_ids is not None:
+            if isinstance(user_ids, (list, tuple)):
+                kwargs.update({"user_ids": ",".join(user_ids)})
+            else:
+                kwargs.update({"user_ids": user_ids})
+        return self.api_call("canvases.access.delete", params=kwargs)
+
+    def canvases_sections_lookup(
+        self,
+        *,
+        canvas_id: str,
+        criteria: Dict[str, Any],
+        **kwargs,
+    ) -> SlackResponse:
+        """Find sections matching the provided criteria
+        https://docs.slack.dev/reference/methods/canvases.sections.lookup
+        """
+        kwargs.update({"canvas_id": canvas_id, "criteria": json.dumps(criteria)})
+        return self.api_call("canvases.sections.lookup", params=kwargs)
+
+    # --------------------------
+    # Deprecated: channels.*
+    # You can use conversations.* APIs instead.
+    # https://docs.slack.dev/changelog/2020-01-deprecating-antecedents-to-the-conversations-api/
+    # --------------------------
+
+    def channels_archive(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Archives a channel."""
+        kwargs.update({"channel": channel})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("channels.archive", json=kwargs)
+
+    def channels_create(
+        self,
+        *,
+        name: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Creates a channel."""
+        kwargs.update({"name": name})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("channels.create", json=kwargs)
+
+    def channels_history(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Fetches history of messages and events from a channel."""
+        kwargs.update({"channel": channel})
+        return self.api_call("channels.history", http_verb="GET", params=kwargs)
+
+    def channels_info(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Gets information about a channel."""
+        kwargs.update({"channel": channel})
+        return self.api_call("channels.info", http_verb="GET", params=kwargs)
+
+    def channels_invite(
+        self,
+        *,
+        channel: str,
+        user: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Invites a user to a channel."""
+        kwargs.update({"channel": channel, "user": user})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("channels.invite", json=kwargs)
+
+    def channels_join(
+        self,
+        *,
+        name: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Joins a channel, creating it if needed."""
+        kwargs.update({"name": name})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("channels.join", json=kwargs)
+
+    def channels_kick(
+        self,
+        *,
+        channel: str,
+        user: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Removes a user from a channel."""
+        kwargs.update({"channel": channel, "user": user})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("channels.kick", json=kwargs)
+
+    def channels_leave(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Leaves a channel."""
+        kwargs.update({"channel": channel})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("channels.leave", json=kwargs)
+
+    def channels_list(
+        self,
+        **kwargs,
+    ) -> SlackResponse:
+        """Lists all channels in a Slack team."""
+        return self.api_call("channels.list", http_verb="GET", params=kwargs)
+
+    def channels_mark(
+        self,
+        *,
+        channel: str,
+        ts: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Sets the read cursor in a channel."""
+        kwargs.update({"channel": channel, "ts": ts})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("channels.mark", json=kwargs)
+
+    def channels_rename(
+        self,
+        *,
+        channel: str,
+        name: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Renames a channel."""
+        kwargs.update({"channel": channel, "name": name})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("channels.rename", json=kwargs)
+
+    def channels_replies(
+        self,
+        *,
+        channel: str,
+        thread_ts: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Retrieve a thread of messages posted to a channel"""
+        kwargs.update({"channel": channel, "thread_ts": thread_ts})
+        return self.api_call("channels.replies", http_verb="GET", params=kwargs)
+
+    def channels_setPurpose(
+        self,
+        *,
+        channel: str,
+        purpose: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Sets the purpose for a channel."""
+        kwargs.update({"channel": channel, "purpose": purpose})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("channels.setPurpose", json=kwargs)
+
+    def channels_setTopic(
+        self,
+        *,
+        channel: str,
+        topic: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Sets the topic for a channel."""
+        kwargs.update({"channel": channel, "topic": topic})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("channels.setTopic", json=kwargs)
+
+    def channels_unarchive(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Unarchives a channel."""
+        kwargs.update({"channel": channel})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("channels.unarchive", json=kwargs)
+
+    # --------------------------
+
+    def chat_appendStream(
+        self,
+        *,
+        channel: str,
+        ts: str,
+        markdown_text: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Appends text to an existing streaming conversation.
+        https://docs.slack.dev/reference/methods/chat.appendStream
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "ts": ts,
+                "markdown_text": markdown_text,
+            }
+        )
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("chat.appendStream", json=kwargs)
+
+    def chat_delete(
+        self,
+        *,
+        channel: str,
+        ts: str,
+        as_user: Optional[bool] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Deletes a message.
+        https://docs.slack.dev/reference/methods/chat.delete
+        """
+        kwargs.update({"channel": channel, "ts": ts, "as_user": as_user})
+        return self.api_call("chat.delete", params=kwargs)
+
+    def chat_deleteScheduledMessage(
+        self,
+        *,
+        channel: str,
+        scheduled_message_id: str,
+        as_user: Optional[bool] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Deletes a scheduled message.
+        https://docs.slack.dev/reference/methods/chat.deleteScheduledMessage
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "scheduled_message_id": scheduled_message_id,
+                "as_user": as_user,
+            }
+        )
+        return self.api_call("chat.deleteScheduledMessage", params=kwargs)
+
+    def chat_getPermalink(
+        self,
+        *,
+        channel: str,
+        message_ts: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Retrieve a permalink URL for a specific extant message
+        https://docs.slack.dev/reference/methods/chat.getPermalink
+        """
+        kwargs.update({"channel": channel, "message_ts": message_ts})
+        return self.api_call("chat.getPermalink", http_verb="GET", params=kwargs)
+
+    def chat_meMessage(
+        self,
+        *,
+        channel: str,
+        text: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Share a me message into a channel.
+        https://docs.slack.dev/reference/methods/chat.meMessage
+        """
+        kwargs.update({"channel": channel, "text": text})
+        return self.api_call("chat.meMessage", params=kwargs)
+
+    def chat_postEphemeral(
+        self,
+        *,
+        channel: str,
+        user: str,
+        text: Optional[str] = None,
+        as_user: Optional[bool] = None,
+        attachments: Optional[Union[str, Sequence[Union[Dict, Attachment]]]] = None,
+        blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None,
+        thread_ts: Optional[str] = None,
+        icon_emoji: Optional[str] = None,
+        icon_url: Optional[str] = None,
+        link_names: Optional[bool] = None,
+        username: Optional[str] = None,
+        parse: Optional[str] = None,
+        markdown_text: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Sends an ephemeral message to a user in a channel.
+        https://docs.slack.dev/reference/methods/chat.postEphemeral
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "user": user,
+                "text": text,
+                "as_user": as_user,
+                "attachments": attachments,
+                "blocks": blocks,
+                "thread_ts": thread_ts,
+                "icon_emoji": icon_emoji,
+                "icon_url": icon_url,
+                "link_names": link_names,
+                "username": username,
+                "parse": parse,
+                "markdown_text": markdown_text,
+            }
+        )
+        _parse_web_class_objects(kwargs)
+        kwargs = _remove_none_values(kwargs)
+        _warn_if_message_text_content_is_missing("chat.postEphemeral", kwargs)
+        # NOTE: intentionally using json over params for the API methods using blocks/attachments
+        return self.api_call("chat.postEphemeral", json=kwargs)
+
+    def chat_postMessage(
+        self,
+        *,
+        channel: str,
+        text: Optional[str] = None,
+        as_user: Optional[bool] = None,
+        attachments: Optional[Union[str, Sequence[Union[Dict, Attachment]]]] = None,
+        blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None,
+        thread_ts: Optional[str] = None,
+        reply_broadcast: Optional[bool] = None,
+        unfurl_links: Optional[bool] = None,
+        unfurl_media: Optional[bool] = None,
+        container_id: Optional[str] = None,
+        icon_emoji: Optional[str] = None,
+        icon_url: Optional[str] = None,
+        mrkdwn: Optional[bool] = None,
+        link_names: Optional[bool] = None,
+        username: Optional[str] = None,
+        parse: Optional[str] = None,  # none, full
+        metadata: Optional[Union[Dict, Metadata, EventAndEntityMetadata]] = None,
+        markdown_text: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Sends a message to a channel.
+        https://docs.slack.dev/reference/methods/chat.postMessage
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "text": text,
+                "as_user": as_user,
+                "attachments": attachments,
+                "blocks": blocks,
+                "thread_ts": thread_ts,
+                "reply_broadcast": reply_broadcast,
+                "unfurl_links": unfurl_links,
+                "unfurl_media": unfurl_media,
+                "container_id": container_id,
+                "icon_emoji": icon_emoji,
+                "icon_url": icon_url,
+                "mrkdwn": mrkdwn,
+                "link_names": link_names,
+                "username": username,
+                "parse": parse,
+                "metadata": metadata,
+                "markdown_text": markdown_text,
+            }
+        )
+        _parse_web_class_objects(kwargs)
+        kwargs = _remove_none_values(kwargs)
+        _warn_if_message_text_content_is_missing("chat.postMessage", kwargs)
+        # NOTE: intentionally using json over params for the API methods using blocks/attachments
+        return self.api_call("chat.postMessage", json=kwargs)
+
+    def chat_scheduleMessage(
+        self,
+        *,
+        channel: str,
+        post_at: Union[str, int],
+        text: Optional[str] = None,
+        as_user: Optional[bool] = None,
+        attachments: Optional[Union[str, Sequence[Union[Dict, Attachment]]]] = None,
+        blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None,
+        thread_ts: Optional[str] = None,
+        parse: Optional[str] = None,
+        reply_broadcast: Optional[bool] = None,
+        unfurl_links: Optional[bool] = None,
+        unfurl_media: Optional[bool] = None,
+        link_names: Optional[bool] = None,
+        metadata: Optional[Union[Dict, Metadata]] = None,
+        markdown_text: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Schedules a message.
+        https://docs.slack.dev/reference/methods/chat.scheduleMessage
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "post_at": post_at,
+                "text": text,
+                "as_user": as_user,
+                "attachments": attachments,
+                "blocks": blocks,
+                "thread_ts": thread_ts,
+                "reply_broadcast": reply_broadcast,
+                "parse": parse,
+                "unfurl_links": unfurl_links,
+                "unfurl_media": unfurl_media,
+                "link_names": link_names,
+                "metadata": metadata,
+                "markdown_text": markdown_text,
+            }
+        )
+        _parse_web_class_objects(kwargs)
+        kwargs = _remove_none_values(kwargs)
+        _warn_if_message_text_content_is_missing("chat.scheduleMessage", kwargs)
+        # NOTE: intentionally using json over params for the API methods using blocks/attachments
+        return self.api_call("chat.scheduleMessage", json=kwargs)
+
+    def chat_scheduledMessages_list(
+        self,
+        *,
+        channel: Optional[str] = None,
+        cursor: Optional[str] = None,
+        latest: Optional[str] = None,
+        limit: Optional[int] = None,
+        oldest: Optional[str] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Lists all scheduled messages.
+        https://docs.slack.dev/reference/methods/chat.scheduledMessages.list
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "cursor": cursor,
+                "latest": latest,
+                "limit": limit,
+                "oldest": oldest,
+                "team_id": team_id,
+            }
+        )
+        return self.api_call("chat.scheduledMessages.list", params=kwargs)
+
+    def chat_startStream(
+        self,
+        *,
+        channel: str,
+        thread_ts: str,
+        markdown_text: Optional[str] = None,
+        recipient_team_id: Optional[str] = None,
+        recipient_user_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Starts a new streaming conversation.
+        https://docs.slack.dev/reference/methods/chat.startStream
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "thread_ts": thread_ts,
+                "markdown_text": markdown_text,
+                "recipient_team_id": recipient_team_id,
+                "recipient_user_id": recipient_user_id,
+            }
+        )
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("chat.startStream", json=kwargs)
+
+    def chat_stopStream(
+        self,
+        *,
+        channel: str,
+        ts: str,
+        markdown_text: Optional[str] = None,
+        blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None,
+        metadata: Optional[Union[Dict, Metadata]] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Stops a streaming conversation.
+        https://docs.slack.dev/reference/methods/chat.stopStream
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "ts": ts,
+                "markdown_text": markdown_text,
+                "blocks": blocks,
+                "metadata": metadata,
+            }
+        )
+        _parse_web_class_objects(kwargs)
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("chat.stopStream", json=kwargs)
+
+    def chat_stream(
+        self,
+        *,
+        buffer_size: int = 256,
+        channel: str,
+        thread_ts: str,
+        recipient_team_id: Optional[str] = None,
+        recipient_user_id: Optional[str] = None,
+        **kwargs,
+    ) -> ChatStream:
+        """Stream markdown text into a conversation.
+
+        This method starts a new chat stream in a conversation that can be appended to. After appending an entire message,
+        the stream can be stopped with concluding arguments such as "blocks" for gathering feedback.
+
+        The following methods are used:
+
+        - chat.startStream: Starts a new streaming conversation.
+          [Reference](https://docs.slack.dev/reference/methods/chat.startStream).
+        - chat.appendStream: Appends text to an existing streaming conversation.
+          [Reference](https://docs.slack.dev/reference/methods/chat.appendStream).
+        - chat.stopStream: Stops a streaming conversation.
+          [Reference](https://docs.slack.dev/reference/methods/chat.stopStream).
+
+        Args:
+            buffer_size: The length of markdown_text to buffer in-memory before calling a stream method. Increasing this
+              value decreases the number of method calls made for the same amount of text, which is useful to avoid rate
+              limits. Default: 256.
+            channel: An encoded ID that represents a channel, private group, or DM.
+            thread_ts: Provide another message's ts value to reply to. Streamed messages should always be replies to a user
+              request.
+            recipient_team_id: The encoded ID of the team the user receiving the streaming text belongs to. Required when
+              streaming to channels.
+            recipient_user_id: The encoded ID of the user to receive the streaming text. Required when streaming to channels.
+            **kwargs: Additional arguments passed to the underlying API calls.
+
+        Returns:
+            ChatStream instance for managing the stream
+
+        Example:
+            ```python
+            streamer = client.chat_stream(
+                channel="C0123456789",
+                thread_ts="1700000001.123456",
+                recipient_team_id="T0123456789",
+                recipient_user_id="U0123456789",
+            )
+            streamer.append(markdown_text="**hello wo")
+            streamer.append(markdown_text="rld!**")
+            streamer.stop()
+            ```
+        """
+        return ChatStream(
+            self,
+            logger=self._logger,
+            channel=channel,
+            thread_ts=thread_ts,
+            recipient_team_id=recipient_team_id,
+            recipient_user_id=recipient_user_id,
+            buffer_size=buffer_size,
+            **kwargs,
+        )
+
+    def chat_unfurl(
+        self,
+        *,
+        channel: Optional[str] = None,
+        ts: Optional[str] = None,
+        source: Optional[str] = None,
+        unfurl_id: Optional[str] = None,
+        unfurls: Optional[Dict[str, Dict]] = None,  # or user_auth_*
+        metadata: Optional[Union[Dict, EventAndEntityMetadata]] = None,
+        user_auth_blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None,
+        user_auth_message: Optional[str] = None,
+        user_auth_required: Optional[bool] = None,
+        user_auth_url: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Provide custom unfurl behavior for user-posted URLs.
+        https://docs.slack.dev/reference/methods/chat.unfurl
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "ts": ts,
+                "source": source,
+                "unfurl_id": unfurl_id,
+                "unfurls": unfurls,
+                "metadata": metadata,
+                "user_auth_blocks": user_auth_blocks,
+                "user_auth_message": user_auth_message,
+                "user_auth_required": user_auth_required,
+                "user_auth_url": user_auth_url,
+            }
+        )
+        _parse_web_class_objects(kwargs)  # for user_auth_blocks
+        kwargs = _remove_none_values(kwargs)
+        # NOTE: intentionally using json over params for API methods using blocks/attachments
+        return self.api_call("chat.unfurl", json=kwargs)
+
+    def chat_update(
+        self,
+        *,
+        channel: str,
+        ts: str,
+        text: Optional[str] = None,
+        attachments: Optional[Union[str, Sequence[Union[Dict, Attachment]]]] = None,
+        blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None,
+        as_user: Optional[bool] = None,
+        file_ids: Optional[Union[str, Sequence[str]]] = None,
+        link_names: Optional[bool] = None,
+        parse: Optional[str] = None,  # none, full
+        reply_broadcast: Optional[bool] = None,
+        metadata: Optional[Union[Dict, Metadata]] = None,
+        markdown_text: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Updates a message in a channel.
+        https://docs.slack.dev/reference/methods/chat.update
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "ts": ts,
+                "text": text,
+                "attachments": attachments,
+                "blocks": blocks,
+                "as_user": as_user,
+                "link_names": link_names,
+                "parse": parse,
+                "reply_broadcast": reply_broadcast,
+                "metadata": metadata,
+                "markdown_text": markdown_text,
+            }
+        )
+        if isinstance(file_ids, (list, tuple)):
+            kwargs.update({"file_ids": ",".join(file_ids)})
+        else:
+            kwargs.update({"file_ids": file_ids})
+        _parse_web_class_objects(kwargs)
+        kwargs = _remove_none_values(kwargs)
+        _warn_if_message_text_content_is_missing("chat.update", kwargs)
+        # NOTE: intentionally using json over params for API methods using blocks/attachments
+        return self.api_call("chat.update", json=kwargs)
+
+    def conversations_acceptSharedInvite(
+        self,
+        *,
+        channel_name: str,
+        channel_id: Optional[str] = None,
+        invite_id: Optional[str] = None,
+        free_trial_accepted: Optional[bool] = None,
+        is_private: Optional[bool] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Accepts an invitation to a Slack Connect channel.
+        https://docs.slack.dev/reference/methods/conversations.acceptSharedInvite
+        """
+        if channel_id is None and invite_id is None:
+            raise e.SlackRequestError("Either channel_id or invite_id must be provided.")
+        kwargs.update(
+            {
+                "channel_name": channel_name,
+                "channel_id": channel_id,
+                "invite_id": invite_id,
+                "free_trial_accepted": free_trial_accepted,
+                "is_private": is_private,
+                "team_id": team_id,
+            }
+        )
+        return self.api_call("conversations.acceptSharedInvite", http_verb="POST", params=kwargs)
+
+    def conversations_approveSharedInvite(
+        self,
+        *,
+        invite_id: str,
+        target_team: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Approves an invitation to a Slack Connect channel.
+        https://docs.slack.dev/reference/methods/conversations.approveSharedInvite
+        """
+        kwargs.update({"invite_id": invite_id, "target_team": target_team})
+        return self.api_call("conversations.approveSharedInvite", http_verb="POST", params=kwargs)
+
+    def conversations_archive(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Archives a conversation.
+        https://docs.slack.dev/reference/methods/conversations.archive
+        """
+        kwargs.update({"channel": channel})
+        return self.api_call("conversations.archive", params=kwargs)
+
+    def conversations_close(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Closes a direct message or multi-person direct message.
+        https://docs.slack.dev/reference/methods/conversations.close
+        """
+        kwargs.update({"channel": channel})
+        return self.api_call("conversations.close", params=kwargs)
+
+    def conversations_create(
+        self,
+        *,
+        name: str,
+        is_private: Optional[bool] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Initiates a public or private channel-based conversation
+        https://docs.slack.dev/reference/methods/conversations.create
+        """
+        kwargs.update({"name": name, "is_private": is_private, "team_id": team_id})
+        return self.api_call("conversations.create", params=kwargs)
+
+    def conversations_declineSharedInvite(
+        self,
+        *,
+        invite_id: str,
+        target_team: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Declines a Slack Connect channel invite.
+        https://docs.slack.dev/reference/methods/conversations.declineSharedInvite
+        """
+        kwargs.update({"invite_id": invite_id, "target_team": target_team})
+        return self.api_call("conversations.declineSharedInvite", http_verb="GET", params=kwargs)
+
+    def conversations_externalInvitePermissions_set(
+        self, *, action: str, channel: str, target_team: str, **kwargs
+    ) -> SlackResponse:
+        """Sets a team in a shared External Limited channel to a shared Slack Connect channel or vice versa.
+        https://docs.slack.dev/reference/methods/conversations.externalInvitePermissions.set
+        """
+        kwargs.update(
+            {
+                "action": action,
+                "channel": channel,
+                "target_team": target_team,
+            }
+        )
+        return self.api_call("conversations.externalInvitePermissions.set", params=kwargs)
+
+    def conversations_history(
+        self,
+        *,
+        channel: str,
+        cursor: Optional[str] = None,
+        inclusive: Optional[bool] = None,
+        include_all_metadata: Optional[bool] = None,
+        latest: Optional[str] = None,
+        limit: Optional[int] = None,
+        oldest: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Fetches a conversation's history of messages and events.
+        https://docs.slack.dev/reference/methods/conversations.history
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "cursor": cursor,
+                "inclusive": inclusive,
+                "include_all_metadata": include_all_metadata,
+                "limit": limit,
+                "latest": latest,
+                "oldest": oldest,
+            }
+        )
+        return self.api_call("conversations.history", http_verb="GET", params=kwargs)
+
+    def conversations_info(
+        self,
+        *,
+        channel: str,
+        include_locale: Optional[bool] = None,
+        include_num_members: Optional[bool] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Retrieve information about a conversation.
+        https://docs.slack.dev/reference/methods/conversations.info
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "include_locale": include_locale,
+                "include_num_members": include_num_members,
+            }
+        )
+        return self.api_call("conversations.info", http_verb="GET", params=kwargs)
+
+    def conversations_invite(
+        self,
+        *,
+        channel: str,
+        users: Union[str, Sequence[str]],
+        force: Optional[bool] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Invites users to a channel.
+        https://docs.slack.dev/reference/methods/conversations.invite
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "force": force,
+            }
+        )
+        if isinstance(users, (list, tuple)):
+            kwargs.update({"users": ",".join(users)})
+        else:
+            kwargs.update({"users": users})
+        return self.api_call("conversations.invite", params=kwargs)
+
+    def conversations_inviteShared(
+        self,
+        *,
+        channel: str,
+        emails: Optional[Union[str, Sequence[str]]] = None,
+        user_ids: Optional[Union[str, Sequence[str]]] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Sends an invitation to a Slack Connect channel.
+        https://docs.slack.dev/reference/methods/conversations.inviteShared
+        """
+        if emails is None and user_ids is None:
+            raise e.SlackRequestError("Either emails or user ids must be provided.")
+        kwargs.update({"channel": channel})
+        if isinstance(emails, (list, tuple)):
+            kwargs.update({"emails": ",".join(emails)})
+        else:
+            kwargs.update({"emails": emails})
+        if isinstance(user_ids, (list, tuple)):
+            kwargs.update({"user_ids": ",".join(user_ids)})
+        else:
+            kwargs.update({"user_ids": user_ids})
+        return self.api_call("conversations.inviteShared", http_verb="GET", params=kwargs)
+
+    def conversations_join(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Joins an existing conversation.
+        https://docs.slack.dev/reference/methods/conversations.join
+        """
+        kwargs.update({"channel": channel})
+        return self.api_call("conversations.join", params=kwargs)
+
+    def conversations_kick(
+        self,
+        *,
+        channel: str,
+        user: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Removes a user from a conversation.
+        https://docs.slack.dev/reference/methods/conversations.kick
+        """
+        kwargs.update({"channel": channel, "user": user})
+        return self.api_call("conversations.kick", params=kwargs)
+
+    def conversations_leave(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Leaves a conversation.
+        https://docs.slack.dev/reference/methods/conversations.leave
+        """
+        kwargs.update({"channel": channel})
+        return self.api_call("conversations.leave", params=kwargs)
+
+    def conversations_list(
+        self,
+        *,
+        cursor: Optional[str] = None,
+        exclude_archived: Optional[bool] = None,
+        limit: Optional[int] = None,
+        team_id: Optional[str] = None,
+        types: Optional[Union[str, Sequence[str]]] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Lists all channels in a Slack team.
+        https://docs.slack.dev/reference/methods/conversations.list
+        """
+        kwargs.update(
+            {
+                "cursor": cursor,
+                "exclude_archived": exclude_archived,
+                "limit": limit,
+                "team_id": team_id,
+            }
+        )
+        if isinstance(types, (list, tuple)):
+            kwargs.update({"types": ",".join(types)})
+        else:
+            kwargs.update({"types": types})
+        return self.api_call("conversations.list", http_verb="GET", params=kwargs)
+
+    def conversations_listConnectInvites(
+        self,
+        *,
+        count: Optional[int] = None,
+        cursor: Optional[str] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """List shared channel invites that have been generated
+        or received but have not yet been approved by all parties.
+        https://docs.slack.dev/reference/methods/conversations.listConnectInvites
+        """
+        kwargs.update({"count": count, "cursor": cursor, "team_id": team_id})
+        return self.api_call("conversations.listConnectInvites", params=kwargs)
+
+    def conversations_mark(
+        self,
+        *,
+        channel: str,
+        ts: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Sets the read cursor in a channel.
+        https://docs.slack.dev/reference/methods/conversations.mark
+        """
+        kwargs.update({"channel": channel, "ts": ts})
+        return self.api_call("conversations.mark", params=kwargs)
+
+    def conversations_members(
+        self,
+        *,
+        channel: str,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Retrieve members of a conversation.
+        https://docs.slack.dev/reference/methods/conversations.members
+        """
+        kwargs.update({"channel": channel, "cursor": cursor, "limit": limit})
+        return self.api_call("conversations.members", http_verb="GET", params=kwargs)
+
+    def conversations_open(
+        self,
+        *,
+        channel: Optional[str] = None,
+        return_im: Optional[bool] = None,
+        users: Optional[Union[str, Sequence[str]]] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Opens or resumes a direct message or multi-person direct message.
+        https://docs.slack.dev/reference/methods/conversations.open
+        """
+        if channel is None and users is None:
+            raise e.SlackRequestError("Either channel or users must be provided.")
+        kwargs.update({"channel": channel, "return_im": return_im})
+        if isinstance(users, (list, tuple)):
+            kwargs.update({"users": ",".join(users)})
+        else:
+            kwargs.update({"users": users})
+        return self.api_call("conversations.open", params=kwargs)
+
+    def conversations_rename(
+        self,
+        *,
+        channel: str,
+        name: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Renames a conversation.
+        https://docs.slack.dev/reference/methods/conversations.rename
+        """
+        kwargs.update({"channel": channel, "name": name})
+        return self.api_call("conversations.rename", params=kwargs)
+
+    def conversations_replies(
+        self,
+        *,
+        channel: str,
+        ts: str,
+        cursor: Optional[str] = None,
+        inclusive: Optional[bool] = None,
+        include_all_metadata: Optional[bool] = None,
+        latest: Optional[str] = None,
+        limit: Optional[int] = None,
+        oldest: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Retrieve a thread of messages posted to a conversation
+        https://docs.slack.dev/reference/methods/conversations.replies
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "ts": ts,
+                "cursor": cursor,
+                "inclusive": inclusive,
+                "include_all_metadata": include_all_metadata,
+                "limit": limit,
+                "latest": latest,
+                "oldest": oldest,
+            }
+        )
+        return self.api_call("conversations.replies", http_verb="GET", params=kwargs)
+
+    def conversations_requestSharedInvite_approve(
+        self,
+        *,
+        invite_id: str,
+        channel_id: Optional[str] = None,
+        is_external_limited: Optional[str] = None,
+        message: Optional[Dict[str, Any]] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Approve a request to add an external user to a channel. This also sends them a Slack Connect invite.
+        https://docs.slack.dev/reference/methods/conversations.requestSharedInvite.approve
+        """
+        kwargs.update(
+            {
+                "invite_id": invite_id,
+                "channel_id": channel_id,
+                "is_external_limited": is_external_limited,
+            }
+        )
+        if message is not None:
+            kwargs.update({"message": json.dumps(message)})
+        return self.api_call("conversations.requestSharedInvite.approve", params=kwargs)
+
+    def conversations_requestSharedInvite_deny(
+        self,
+        *,
+        invite_id: str,
+        message: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Deny a request to invite an external user to a channel.
+        https://docs.slack.dev/reference/methods/conversations.requestSharedInvite.deny
+        """
+        kwargs.update({"invite_id": invite_id, "message": message})
+        return self.api_call("conversations.requestSharedInvite.deny", params=kwargs)
+
+    def conversations_requestSharedInvite_list(
+        self,
+        *,
+        cursor: Optional[str] = None,
+        include_approved: Optional[bool] = None,
+        include_denied: Optional[bool] = None,
+        include_expired: Optional[bool] = None,
+        invite_ids: Optional[Union[str, Sequence[str]]] = None,
+        limit: Optional[int] = None,
+        user_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Lists requests to add external users to channels with ability to filter.
+        https://docs.slack.dev/reference/methods/conversations.requestSharedInvite.list
+        """
+        kwargs.update(
+            {
+                "cursor": cursor,
+                "include_approved": include_approved,
+                "include_denied": include_denied,
+                "include_expired": include_expired,
+                "limit": limit,
+                "user_id": user_id,
+            }
+        )
+        if invite_ids is not None:
+            if isinstance(invite_ids, (list, tuple)):
+                kwargs.update({"invite_ids": ",".join(invite_ids)})
+            else:
+                kwargs.update({"invite_ids": invite_ids})
+        return self.api_call("conversations.requestSharedInvite.list", params=kwargs)
+
+    def conversations_setPurpose(
+        self,
+        *,
+        channel: str,
+        purpose: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Sets the purpose for a conversation.
+        https://docs.slack.dev/reference/methods/conversations.setPurpose
+        """
+        kwargs.update({"channel": channel, "purpose": purpose})
+        return self.api_call("conversations.setPurpose", params=kwargs)
+
+    def conversations_setTopic(
+        self,
+        *,
+        channel: str,
+        topic: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Sets the topic for a conversation.
+        https://docs.slack.dev/reference/methods/conversations.setTopic
+        """
+        kwargs.update({"channel": channel, "topic": topic})
+        return self.api_call("conversations.setTopic", params=kwargs)
+
+    def conversations_unarchive(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Reverses conversation archival.
+        https://docs.slack.dev/reference/methods/conversations.unarchive
+        """
+        kwargs.update({"channel": channel})
+        return self.api_call("conversations.unarchive", params=kwargs)
+
+    def conversations_canvases_create(
+        self,
+        *,
+        channel_id: str,
+        document_content: Dict[str, str],
+        **kwargs,
+    ) -> SlackResponse:
+        """Create a Channel Canvas for a channel
+        https://docs.slack.dev/reference/methods/conversations.canvases.create
+        """
+        kwargs.update({"channel_id": channel_id, "document_content": document_content})
+        return self.api_call("conversations.canvases.create", json=kwargs)
+
+    def dialog_open(
+        self,
+        *,
+        dialog: Dict[str, Any],
+        trigger_id: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Open a dialog with a user.
+        https://docs.slack.dev/reference/methods/dialog.open
+        """
+        kwargs.update({"dialog": dialog, "trigger_id": trigger_id})
+        kwargs = _remove_none_values(kwargs)
+        # NOTE: As the dialog can be a dict, this API call works only with json format.
+        return self.api_call("dialog.open", json=kwargs)
+
+    def dnd_endDnd(
+        self,
+        **kwargs,
+    ) -> SlackResponse:
+        """Ends the current user's Do Not Disturb session immediately.
+        https://docs.slack.dev/reference/methods/dnd.endDnd
+        """
+        return self.api_call("dnd.endDnd", params=kwargs)
+
+    def dnd_endSnooze(
+        self,
+        **kwargs,
+    ) -> SlackResponse:
+        """Ends the current user's snooze mode immediately.
+        https://docs.slack.dev/reference/methods/dnd.endSnooze
+        """
+        return self.api_call("dnd.endSnooze", params=kwargs)
+
+    def dnd_info(
+        self,
+        *,
+        team_id: Optional[str] = None,
+        user: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Retrieves a user's current Do Not Disturb status.
+        https://docs.slack.dev/reference/methods/dnd.info
+        """
+        kwargs.update({"team_id": team_id, "user": user})
+        return self.api_call("dnd.info", http_verb="GET", params=kwargs)
+
+    def dnd_setSnooze(
+        self,
+        *,
+        num_minutes: Union[int, str],
+        **kwargs,
+    ) -> SlackResponse:
+        """Turns on Do Not Disturb mode for the current user, or changes its duration.
+        https://docs.slack.dev/reference/methods/dnd.setSnooze
+        """
+        kwargs.update({"num_minutes": num_minutes})
+        return self.api_call("dnd.setSnooze", http_verb="GET", params=kwargs)
+
+    def dnd_teamInfo(
+        self,
+        users: Union[str, Sequence[str]],
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Retrieves the Do Not Disturb status for users on a team.
+        https://docs.slack.dev/reference/methods/dnd.teamInfo
+        """
+        if isinstance(users, (list, tuple)):
+            kwargs.update({"users": ",".join(users)})
+        else:
+            kwargs.update({"users": users})
+        kwargs.update({"team_id": team_id})
+        return self.api_call("dnd.teamInfo", http_verb="GET", params=kwargs)
+
+    def emoji_list(
+        self,
+        include_categories: Optional[bool] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Lists custom emoji for a team.
+        https://docs.slack.dev/reference/methods/emoji.list
+        """
+        kwargs.update({"include_categories": include_categories})
+        return self.api_call("emoji.list", http_verb="GET", params=kwargs)
+
+    def entity_presentDetails(
+        self,
+        trigger_id: str,
+        metadata: Optional[Union[Dict, EntityMetadata]] = None,
+        user_auth_required: Optional[bool] = None,
+        user_auth_url: Optional[str] = None,
+        error: Optional[Dict[str, Any]] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Provides entity details for the flexpane.
+        https://docs.slack.dev/reference/methods/entity.presentDetails/
+        """
+        kwargs.update({"trigger_id": trigger_id})
+        if metadata is not None:
+            kwargs.update({"metadata": metadata})
+        if user_auth_required is not None:
+            kwargs.update({"user_auth_required": user_auth_required})
+        if user_auth_url is not None:
+            kwargs.update({"user_auth_url": user_auth_url})
+        if error is not None:
+            kwargs.update({"error": error})
+        _parse_web_class_objects(kwargs)
+        return self.api_call("entity.presentDetails", json=kwargs)
+
+    def files_comments_delete(
+        self,
+        *,
+        file: str,
+        id: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Deletes an existing comment on a file.
+        https://docs.slack.dev/reference/methods/files.comments.delete
+        """
+        kwargs.update({"file": file, "id": id})
+        return self.api_call("files.comments.delete", params=kwargs)
+
+    def files_delete(
+        self,
+        *,
+        file: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Deletes a file.
+        https://docs.slack.dev/reference/methods/files.delete
+        """
+        kwargs.update({"file": file})
+        return self.api_call("files.delete", params=kwargs)
+
+    def files_info(
+        self,
+        *,
+        file: str,
+        count: Optional[int] = None,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        page: Optional[int] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Gets information about a team file.
+        https://docs.slack.dev/reference/methods/files.info
+        """
+        kwargs.update(
+            {
+                "file": file,
+                "count": count,
+                "cursor": cursor,
+                "limit": limit,
+                "page": page,
+            }
+        )
+        return self.api_call("files.info", http_verb="GET", params=kwargs)
+
+    def files_list(
+        self,
+        *,
+        channel: Optional[str] = None,
+        count: Optional[int] = None,
+        page: Optional[int] = None,
+        show_files_hidden_by_limit: Optional[bool] = None,
+        team_id: Optional[str] = None,
+        ts_from: Optional[str] = None,
+        ts_to: Optional[str] = None,
+        types: Optional[Union[str, Sequence[str]]] = None,
+        user: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Lists & filters team files.
+        https://docs.slack.dev/reference/methods/files.list
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "count": count,
+                "page": page,
+                "show_files_hidden_by_limit": show_files_hidden_by_limit,
+                "team_id": team_id,
+                "ts_from": ts_from,
+                "ts_to": ts_to,
+                "user": user,
+            }
+        )
+        if isinstance(types, (list, tuple)):
+            kwargs.update({"types": ",".join(types)})
+        else:
+            kwargs.update({"types": types})
+        return self.api_call("files.list", http_verb="GET", params=kwargs)
+
+    def files_remote_info(
+        self,
+        *,
+        external_id: Optional[str] = None,
+        file: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Retrieve information about a remote file added to Slack.
+        https://docs.slack.dev/reference/methods/files.remote.info
+        """
+        kwargs.update({"external_id": external_id, "file": file})
+        return self.api_call("files.remote.info", http_verb="GET", params=kwargs)
+
+    def files_remote_list(
+        self,
+        *,
+        channel: Optional[str] = None,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        ts_from: Optional[str] = None,
+        ts_to: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Retrieve information about a remote file added to Slack.
+        https://docs.slack.dev/reference/methods/files.remote.list
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "cursor": cursor,
+                "limit": limit,
+                "ts_from": ts_from,
+                "ts_to": ts_to,
+            }
+        )
+        return self.api_call("files.remote.list", http_verb="GET", params=kwargs)
+
+    def files_remote_add(
+        self,
+        *,
+        external_id: str,
+        external_url: str,
+        title: str,
+        filetype: Optional[str] = None,
+        indexable_file_contents: Optional[Union[str, bytes, IOBase]] = None,
+        preview_image: Optional[Union[str, bytes, IOBase]] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Adds a file from a remote service.
+        https://docs.slack.dev/reference/methods/files.remote.add
+        """
+        kwargs.update(
+            {
+                "external_id": external_id,
+                "external_url": external_url,
+                "title": title,
+                "filetype": filetype,
+            }
+        )
+        files = None
+        # preview_image (file): Preview of the document via multipart/form-data.
+        if preview_image is not None or indexable_file_contents is not None:
+            files = {
+                "preview_image": preview_image,
+                "indexable_file_contents": indexable_file_contents,
+            }
+
+        return self.api_call(
+            # Intentionally using "POST" method over "GET" here
+            "files.remote.add",
+            http_verb="POST",
+            data=kwargs,
+            files=files,
+        )
+
+    def files_remote_update(
+        self,
+        *,
+        external_id: Optional[str] = None,
+        external_url: Optional[str] = None,
+        file: Optional[str] = None,
+        title: Optional[str] = None,
+        filetype: Optional[str] = None,
+        indexable_file_contents: Optional[str] = None,
+        preview_image: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Updates an existing remote file.
+        https://docs.slack.dev/reference/methods/files.remote.update
+        """
+        kwargs.update(
+            {
+                "external_id": external_id,
+                "external_url": external_url,
+                "file": file,
+                "title": title,
+                "filetype": filetype,
+            }
+        )
+        files = None
+        # preview_image (file): Preview of the document via multipart/form-data.
+        if preview_image is not None or indexable_file_contents is not None:
+            files = {
+                "preview_image": preview_image,
+                "indexable_file_contents": indexable_file_contents,
+            }
+
+        return self.api_call(
+            # Intentionally using "POST" method over "GET" here
+            "files.remote.update",
+            http_verb="POST",
+            data=kwargs,
+            files=files,
+        )
+
+    def files_remote_remove(
+        self,
+        *,
+        external_id: Optional[str] = None,
+        file: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Remove a remote file.
+        https://docs.slack.dev/reference/methods/files.remote.remove
+        """
+        kwargs.update({"external_id": external_id, "file": file})
+        return self.api_call("files.remote.remove", http_verb="POST", params=kwargs)
+
+    def files_remote_share(
+        self,
+        *,
+        channels: Union[str, Sequence[str]],
+        external_id: Optional[str] = None,
+        file: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Share a remote file into a channel.
+        https://docs.slack.dev/reference/methods/files.remote.share
+        """
+        if external_id is None and file is None:
+            raise e.SlackRequestError("Either external_id or file must be provided.")
+        if isinstance(channels, (list, tuple)):
+            kwargs.update({"channels": ",".join(channels)})
+        else:
+            kwargs.update({"channels": channels})
+        kwargs.update({"external_id": external_id, "file": file})
+        return self.api_call("files.remote.share", http_verb="GET", params=kwargs)
+
+    def files_revokePublicURL(
+        self,
+        *,
+        file: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Revokes public/external sharing access for a file
+        https://docs.slack.dev/reference/methods/files.revokePublicURL
+        """
+        kwargs.update({"file": file})
+        return self.api_call("files.revokePublicURL", params=kwargs)
+
+    def files_sharedPublicURL(
+        self,
+        *,
+        file: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Enables a file for public/external sharing.
+        https://docs.slack.dev/reference/methods/files.sharedPublicURL
+        """
+        kwargs.update({"file": file})
+        return self.api_call("files.sharedPublicURL", params=kwargs)
+
+    def files_upload(
+        self,
+        *,
+        file: Optional[Union[str, bytes, IOBase]] = None,
+        content: Optional[Union[str, bytes]] = None,
+        filename: Optional[str] = None,
+        filetype: Optional[str] = None,
+        initial_comment: Optional[str] = None,
+        thread_ts: Optional[str] = None,
+        title: Optional[str] = None,
+        channels: Optional[Union[str, Sequence[str]]] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Uploads or creates a file.
+        https://docs.slack.dev/reference/methods/files.upload
+        """
+        _print_files_upload_v2_suggestion()
+
+        if file is None and content is None:
+            raise e.SlackRequestError("The file or content argument must be specified.")
+        if file is not None and content is not None:
+            raise e.SlackRequestError("You cannot specify both the file and the content argument.")
+
+        if isinstance(channels, (list, tuple)):
+            kwargs.update({"channels": ",".join(channels)})
+        else:
+            kwargs.update({"channels": channels})
+        kwargs.update(
+            {
+                "filename": filename,
+                "filetype": filetype,
+                "initial_comment": initial_comment,
+                "thread_ts": thread_ts,
+                "title": title,
+            }
+        )
+        if file:
+            if kwargs.get("filename") is None and isinstance(file, str):
+                # use the local filename if filename is missing
+                if kwargs.get("filename") is None:
+                    kwargs["filename"] = file.split(os.path.sep)[-1]
+            return self.api_call("files.upload", files={"file": file}, data=kwargs)
+        else:
+            kwargs["content"] = content
+            return self.api_call("files.upload", data=kwargs)
+
+    def files_upload_v2(
+        self,
+        *,
+        # for sending a single file
+        filename: Optional[str] = None,  # you can skip this only when sending along with content parameter
+        file: Optional[Union[str, bytes, IOBase, os.PathLike]] = None,
+        content: Optional[Union[str, bytes]] = None,
+        title: Optional[str] = None,
+        alt_txt: Optional[str] = None,
+        snippet_type: Optional[str] = None,
+        # To upload multiple files at a time
+        file_uploads: Optional[List[Dict[str, Any]]] = None,
+        channel: Optional[str] = None,
+        channels: Optional[List[str]] = None,
+        initial_comment: Optional[str] = None,
+        thread_ts: Optional[str] = None,
+        request_file_info: bool = True,  # since v3.23, this flag is no longer necessary
+        **kwargs,
+    ) -> SlackResponse:
+        """This wrapper method provides an easy way to upload files using the following endpoints:
+
+        - step1: https://docs.slack.dev/reference/methods/files.getUploadURLExternal
+
+        - step2: "https://files.slack.com/upload/v1/..." URLs returned from files.getUploadURLExternal API
+
+        - step3: https://docs.slack.dev/reference/methods/files.completeUploadExternal
+            and https://docs.slack.dev/reference/methods/files.info
+
+        """
+        if file is None and content is None and file_uploads is None:
+            raise e.SlackRequestError("Any of file, content, and file_uploads must be specified.")
+        if file is not None and content is not None:
+            raise e.SlackRequestError("You cannot specify both the file and the content argument.")
+
+        # deprecated arguments:
+        filetype = kwargs.get("filetype")
+
+        if filetype is not None:
+            warnings.warn("The filetype parameter is no longer supported. Please remove it from the arguments.")
+
+        # step1: files.getUploadURLExternal per file
+        files: List[Dict[str, Any]] = []
+        if file_uploads is not None:
+            for f in file_uploads:
+                files.append(_to_v2_file_upload_item(f))
+        else:
+            f = _to_v2_file_upload_item(
+                {
+                    "filename": filename,
+                    "file": file,
+                    "content": content,
+                    "title": title,
+                    "alt_txt": alt_txt,
+                    "snippet_type": snippet_type,
+                }
+            )
+            files.append(f)
+
+        for f in files:
+            url_response = self.files_getUploadURLExternal(
+                filename=f.get("filename"),  # type: ignore[arg-type]
+                length=f.get("length"),  # type: ignore[arg-type]
+                alt_txt=f.get("alt_txt"),
+                snippet_type=f.get("snippet_type"),
+                token=kwargs.get("token"),
+            )
+            _validate_for_legacy_client(url_response)
+            f["file_id"] = url_response.get("file_id")  # type: ignore[union-attr, unused-ignore]
+            f["upload_url"] = url_response.get("upload_url")  # type: ignore[union-attr, unused-ignore]
+
+        # step2: "https://files.slack.com/upload/v1/..." per file
+        for f in files:
+            upload_result = self._upload_file(
+                url=f["upload_url"],
+                data=f["data"],
+                logger=self._logger,
+                timeout=self.timeout,
+                proxy=self.proxy,
+                ssl=self.ssl,
+            )
+            if upload_result.status != 200:
+                status = upload_result.status
+                body = upload_result.body
+                message = (
+                    "Failed to upload a file "
+                    f"(status: {status}, body: {body}, filename: {f.get('filename')}, title: {f.get('title')})"
+                )
+                raise e.SlackRequestError(message)
+
+        # step3: files.completeUploadExternal with all the sets of (file_id + title)
+        completion = self.files_completeUploadExternal(
+            files=[{"id": f["file_id"], "title": f["title"]} for f in files],
+            channel_id=channel,
+            channels=channels,
+            initial_comment=initial_comment,
+            thread_ts=thread_ts,
+            **kwargs,
+        )
+        if len(completion.get("files")) == 1:  # type: ignore[arg-type, union-attr, unused-ignore]
+            completion.data["file"] = completion.get("files")[0]  # type: ignore[index, union-attr, unused-ignore]
+        return completion
+
+    def files_getUploadURLExternal(
+        self,
+        *,
+        filename: str,
+        length: int,
+        alt_txt: Optional[str] = None,
+        snippet_type: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Gets a URL for an edge external upload.
+        https://docs.slack.dev/reference/methods/files.getUploadURLExternal
+        """
+        kwargs.update(
+            {
+                "filename": filename,
+                "length": length,
+                "alt_txt": alt_txt,
+                "snippet_type": snippet_type,
+            }
+        )
+        return self.api_call("files.getUploadURLExternal", params=kwargs)
+
+    def files_completeUploadExternal(
+        self,
+        *,
+        files: List[Dict[str, str]],
+        channel_id: Optional[str] = None,
+        channels: Optional[List[str]] = None,
+        initial_comment: Optional[str] = None,
+        thread_ts: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Finishes an upload started with files.getUploadURLExternal.
+        https://docs.slack.dev/reference/methods/files.completeUploadExternal
+        """
+        _files = [{k: v for k, v in f.items() if v is not None} for f in files]
+        kwargs.update(
+            {
+                "files": json.dumps(_files),
+                "channel_id": channel_id,
+                "initial_comment": initial_comment,
+                "thread_ts": thread_ts,
+            }
+        )
+        if channels:
+            kwargs["channels"] = ",".join(channels)
+        return self.api_call("files.completeUploadExternal", params=kwargs)
+
+    def functions_completeSuccess(
+        self,
+        *,
+        function_execution_id: str,
+        outputs: Dict[str, Any],
+        **kwargs,
+    ) -> SlackResponse:
+        """Signal the successful completion of a function
+        https://docs.slack.dev/reference/methods/functions.completeSuccess
+        """
+        kwargs.update({"function_execution_id": function_execution_id, "outputs": json.dumps(outputs)})
+        return self.api_call("functions.completeSuccess", params=kwargs)
+
+    def functions_completeError(
+        self,
+        *,
+        function_execution_id: str,
+        error: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Signal the failure to execute a function
+        https://docs.slack.dev/reference/methods/functions.completeError
+        """
+        kwargs.update({"function_execution_id": function_execution_id, "error": error})
+        return self.api_call("functions.completeError", params=kwargs)
+
+    # --------------------------
+    # Deprecated: groups.*
+    # You can use conversations.* APIs instead.
+    # https://docs.slack.dev/changelog/2020-01-deprecating-antecedents-to-the-conversations-api/
+    # --------------------------
+
+    def groups_archive(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Archives a private channel."""
+        kwargs.update({"channel": channel})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("groups.archive", json=kwargs)
+
+    def groups_create(
+        self,
+        *,
+        name: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Creates a private channel."""
+        kwargs.update({"name": name})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("groups.create", json=kwargs)
+
+    def groups_createChild(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Clones and archives a private channel."""
+        kwargs.update({"channel": channel})
+        return self.api_call("groups.createChild", http_verb="GET", params=kwargs)
+
+    def groups_history(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Fetches history of messages and events from a private channel."""
+        kwargs.update({"channel": channel})
+        return self.api_call("groups.history", http_verb="GET", params=kwargs)
+
+    def groups_info(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Gets information about a private channel."""
+        kwargs.update({"channel": channel})
+        return self.api_call("groups.info", http_verb="GET", params=kwargs)
+
+    def groups_invite(
+        self,
+        *,
+        channel: str,
+        user: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Invites a user to a private channel."""
+        kwargs.update({"channel": channel, "user": user})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("groups.invite", json=kwargs)
+
+    def groups_kick(
+        self,
+        *,
+        channel: str,
+        user: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Removes a user from a private channel."""
+        kwargs.update({"channel": channel, "user": user})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("groups.kick", json=kwargs)
+
+    def groups_leave(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Leaves a private channel."""
+        kwargs.update({"channel": channel})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("groups.leave", json=kwargs)
+
+    def groups_list(
+        self,
+        **kwargs,
+    ) -> SlackResponse:
+        """Lists private channels that the calling user has access to."""
+        return self.api_call("groups.list", http_verb="GET", params=kwargs)
+
+    def groups_mark(
+        self,
+        *,
+        channel: str,
+        ts: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Sets the read cursor in a private channel."""
+        kwargs.update({"channel": channel, "ts": ts})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("groups.mark", json=kwargs)
+
+    def groups_open(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Opens a private channel."""
+        kwargs.update({"channel": channel})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("groups.open", json=kwargs)
+
+    def groups_rename(
+        self,
+        *,
+        channel: str,
+        name: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Renames a private channel."""
+        kwargs.update({"channel": channel, "name": name})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("groups.rename", json=kwargs)
+
+    def groups_replies(
+        self,
+        *,
+        channel: str,
+        thread_ts: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Retrieve a thread of messages posted to a private channel"""
+        kwargs.update({"channel": channel, "thread_ts": thread_ts})
+        return self.api_call("groups.replies", http_verb="GET", params=kwargs)
+
+    def groups_setPurpose(
+        self,
+        *,
+        channel: str,
+        purpose: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Sets the purpose for a private channel."""
+        kwargs.update({"channel": channel, "purpose": purpose})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("groups.setPurpose", json=kwargs)
+
+    def groups_setTopic(
+        self,
+        *,
+        channel: str,
+        topic: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Sets the topic for a private channel."""
+        kwargs.update({"channel": channel, "topic": topic})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("groups.setTopic", json=kwargs)
+
+    def groups_unarchive(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Unarchives a private channel."""
+        kwargs.update({"channel": channel})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("groups.unarchive", json=kwargs)
+
+    # --------------------------
+    # Deprecated: im.*
+    # You can use conversations.* APIs instead.
+    # https://docs.slack.dev/changelog/2020-01-deprecating-antecedents-to-the-conversations-api/
+    # --------------------------
+
+    def im_close(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Close a direct message channel."""
+        kwargs.update({"channel": channel})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("im.close", json=kwargs)
+
+    def im_history(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Fetches history of messages and events from direct message channel."""
+        kwargs.update({"channel": channel})
+        return self.api_call("im.history", http_verb="GET", params=kwargs)
+
+    def im_list(
+        self,
+        **kwargs,
+    ) -> SlackResponse:
+        """Lists direct message channels for the calling user."""
+        return self.api_call("im.list", http_verb="GET", params=kwargs)
+
+    def im_mark(
+        self,
+        *,
+        channel: str,
+        ts: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Sets the read cursor in a direct message channel."""
+        kwargs.update({"channel": channel, "ts": ts})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("im.mark", json=kwargs)
+
+    def im_open(
+        self,
+        *,
+        user: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Opens a direct message channel."""
+        kwargs.update({"user": user})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("im.open", json=kwargs)
+
+    def im_replies(
+        self,
+        *,
+        channel: str,
+        thread_ts: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Retrieve a thread of messages posted to a direct message conversation"""
+        kwargs.update({"channel": channel, "thread_ts": thread_ts})
+        return self.api_call("im.replies", http_verb="GET", params=kwargs)
+
+    # --------------------------
+
+    def migration_exchange(
+        self,
+        *,
+        users: Union[str, Sequence[str]],
+        team_id: Optional[str] = None,
+        to_old: Optional[bool] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """For Enterprise Grid workspaces, map local user IDs to global user IDs
+        https://docs.slack.dev/reference/methods/migration.exchange
+        """
+        if isinstance(users, (list, tuple)):
+            kwargs.update({"users": ",".join(users)})
+        else:
+            kwargs.update({"users": users})
+        kwargs.update({"team_id": team_id, "to_old": to_old})
+        return self.api_call("migration.exchange", http_verb="GET", params=kwargs)
+
+    # --------------------------
+    # Deprecated: mpim.*
+    # You can use conversations.* APIs instead.
+    # https://docs.slack.dev/changelog/2020-01-deprecating-antecedents-to-the-conversations-api/
+    # --------------------------
+
+    def mpim_close(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Closes a multiparty direct message channel."""
+        kwargs.update({"channel": channel})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("mpim.close", json=kwargs)
+
+    def mpim_history(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Fetches history of messages and events from a multiparty direct message."""
+        kwargs.update({"channel": channel})
+        return self.api_call("mpim.history", http_verb="GET", params=kwargs)
+
+    def mpim_list(
+        self,
+        **kwargs,
+    ) -> SlackResponse:
+        """Lists multiparty direct message channels for the calling user."""
+        return self.api_call("mpim.list", http_verb="GET", params=kwargs)
+
+    def mpim_mark(
+        self,
+        *,
+        channel: str,
+        ts: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Sets the read cursor in a multiparty direct message channel."""
+        kwargs.update({"channel": channel, "ts": ts})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("mpim.mark", json=kwargs)
+
+    def mpim_open(
+        self,
+        *,
+        users: Union[str, Sequence[str]],
+        **kwargs,
+    ) -> SlackResponse:
+        """This method opens a multiparty direct message."""
+        if isinstance(users, (list, tuple)):
+            kwargs.update({"users": ",".join(users)})
+        else:
+            kwargs.update({"users": users})
+        return self.api_call("mpim.open", params=kwargs)
+
+    def mpim_replies(
+        self,
+        *,
+        channel: str,
+        thread_ts: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Retrieve a thread of messages posted to a direct message conversation from a
+        multiparty direct message.
+        """
+        kwargs.update({"channel": channel, "thread_ts": thread_ts})
+        return self.api_call("mpim.replies", http_verb="GET", params=kwargs)
+
+    # --------------------------
+
+    def oauth_v2_access(
+        self,
+        *,
+        client_id: str,
+        client_secret: str,
+        # This field is required when processing the OAuth redirect URL requests
+        # while it's absent for token rotation
+        code: Optional[str] = None,
+        redirect_uri: Optional[str] = None,
+        # This field is required for token rotation
+        grant_type: Optional[str] = None,
+        # This field is required for token rotation
+        refresh_token: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Exchanges a temporary OAuth verifier code for an access token.
+        https://docs.slack.dev/reference/methods/oauth.v2.access
+        """
+        if redirect_uri is not None:
+            kwargs.update({"redirect_uri": redirect_uri})
+        if code is not None:
+            kwargs.update({"code": code})
+        if grant_type is not None:
+            kwargs.update({"grant_type": grant_type})
+        if refresh_token is not None:
+            kwargs.update({"refresh_token": refresh_token})
+        return self.api_call(
+            "oauth.v2.access",
+            data=kwargs,
+            auth={"client_id": client_id, "client_secret": client_secret},
+        )
+
+    def oauth_access(
+        self,
+        *,
+        client_id: str,
+        client_secret: str,
+        code: str,
+        redirect_uri: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Exchanges a temporary OAuth verifier code for an access token.
+        https://docs.slack.dev/reference/methods/oauth.access
+        """
+        if redirect_uri is not None:
+            kwargs.update({"redirect_uri": redirect_uri})
+        kwargs.update({"code": code})
+        return self.api_call(
+            "oauth.access",
+            data=kwargs,
+            auth={"client_id": client_id, "client_secret": client_secret},
+        )
+
+    def oauth_v2_exchange(
+        self,
+        *,
+        token: str,
+        client_id: str,
+        client_secret: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Exchanges a legacy access token for a new expiring access token and refresh token
+        https://docs.slack.dev/reference/methods/oauth.v2.exchange
+        """
+        kwargs.update({"client_id": client_id, "client_secret": client_secret, "token": token})
+        return self.api_call("oauth.v2.exchange", params=kwargs)
+
+    def openid_connect_token(
+        self,
+        client_id: str,
+        client_secret: str,
+        code: Optional[str] = None,
+        redirect_uri: Optional[str] = None,
+        grant_type: Optional[str] = None,
+        refresh_token: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Exchanges a temporary OAuth verifier code for an access token for Sign in with Slack.
+        https://docs.slack.dev/reference/methods/openid.connect.token
+        """
+        if redirect_uri is not None:
+            kwargs.update({"redirect_uri": redirect_uri})
+        if code is not None:
+            kwargs.update({"code": code})
+        if grant_type is not None:
+            kwargs.update({"grant_type": grant_type})
+        if refresh_token is not None:
+            kwargs.update({"refresh_token": refresh_token})
+        return self.api_call(
+            "openid.connect.token",
+            data=kwargs,
+            auth={"client_id": client_id, "client_secret": client_secret},
+        )
+
+    def openid_connect_userInfo(
+        self,
+        **kwargs,
+    ) -> SlackResponse:
+        """Get the identity of a user who has authorized Sign in with Slack.
+        https://docs.slack.dev/reference/methods/openid.connect.userInfo
+        """
+        return self.api_call("openid.connect.userInfo", params=kwargs)
+
+    def pins_add(
+        self,
+        *,
+        channel: str,
+        timestamp: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Pins an item to a channel.
+        https://docs.slack.dev/reference/methods/pins.add
+        """
+        kwargs.update({"channel": channel, "timestamp": timestamp})
+        return self.api_call("pins.add", params=kwargs)
+
+    def pins_list(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Lists items pinned to a channel.
+        https://docs.slack.dev/reference/methods/pins.list
+        """
+        kwargs.update({"channel": channel})
+        return self.api_call("pins.list", http_verb="GET", params=kwargs)
+
+    def pins_remove(
+        self,
+        *,
+        channel: str,
+        timestamp: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Un-pins an item from a channel.
+        https://docs.slack.dev/reference/methods/pins.remove
+        """
+        kwargs.update({"channel": channel, "timestamp": timestamp})
+        return self.api_call("pins.remove", params=kwargs)
+
+    def reactions_add(
+        self,
+        *,
+        channel: str,
+        name: str,
+        timestamp: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Adds a reaction to an item.
+        https://docs.slack.dev/reference/methods/reactions.add
+        """
+        kwargs.update({"channel": channel, "name": name, "timestamp": timestamp})
+        return self.api_call("reactions.add", params=kwargs)
+
+    def reactions_get(
+        self,
+        *,
+        channel: Optional[str] = None,
+        file: Optional[str] = None,
+        file_comment: Optional[str] = None,
+        full: Optional[bool] = None,
+        timestamp: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Gets reactions for an item.
+        https://docs.slack.dev/reference/methods/reactions.get
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "file": file,
+                "file_comment": file_comment,
+                "full": full,
+                "timestamp": timestamp,
+            }
+        )
+        return self.api_call("reactions.get", http_verb="GET", params=kwargs)
+
+    def reactions_list(
+        self,
+        *,
+        count: Optional[int] = None,
+        cursor: Optional[str] = None,
+        full: Optional[bool] = None,
+        limit: Optional[int] = None,
+        page: Optional[int] = None,
+        team_id: Optional[str] = None,
+        user: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Lists reactions made by a user.
+        https://docs.slack.dev/reference/methods/reactions.list
+        """
+        kwargs.update(
+            {
+                "count": count,
+                "cursor": cursor,
+                "full": full,
+                "limit": limit,
+                "page": page,
+                "team_id": team_id,
+                "user": user,
+            }
+        )
+        return self.api_call("reactions.list", http_verb="GET", params=kwargs)
+
+    def reactions_remove(
+        self,
+        *,
+        name: str,
+        channel: Optional[str] = None,
+        file: Optional[str] = None,
+        file_comment: Optional[str] = None,
+        timestamp: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Removes a reaction from an item.
+        https://docs.slack.dev/reference/methods/reactions.remove
+        """
+        kwargs.update(
+            {
+                "name": name,
+                "channel": channel,
+                "file": file,
+                "file_comment": file_comment,
+                "timestamp": timestamp,
+            }
+        )
+        return self.api_call("reactions.remove", params=kwargs)
+
+    def reminders_add(
+        self,
+        *,
+        text: str,
+        time: str,
+        team_id: Optional[str] = None,
+        user: Optional[str] = None,
+        recurrence: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Creates a reminder.
+        https://docs.slack.dev/reference/methods/reminders.add
+        """
+        kwargs.update(
+            {
+                "text": text,
+                "time": time,
+                "team_id": team_id,
+                "user": user,
+                "recurrence": recurrence,
+            }
+        )
+        return self.api_call("reminders.add", params=kwargs)
+
+    def reminders_complete(
+        self,
+        *,
+        reminder: str,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Marks a reminder as complete.
+        https://docs.slack.dev/reference/methods/reminders.complete
+        """
+        kwargs.update({"reminder": reminder, "team_id": team_id})
+        return self.api_call("reminders.complete", params=kwargs)
+
+    def reminders_delete(
+        self,
+        *,
+        reminder: str,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Deletes a reminder.
+        https://docs.slack.dev/reference/methods/reminders.delete
+        """
+        kwargs.update({"reminder": reminder, "team_id": team_id})
+        return self.api_call("reminders.delete", params=kwargs)
+
+    def reminders_info(
+        self,
+        *,
+        reminder: str,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Gets information about a reminder.
+        https://docs.slack.dev/reference/methods/reminders.info
+        """
+        kwargs.update({"reminder": reminder, "team_id": team_id})
+        return self.api_call("reminders.info", http_verb="GET", params=kwargs)
+
+    def reminders_list(
+        self,
+        *,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Lists all reminders created by or for a given user.
+        https://docs.slack.dev/reference/methods/reminders.list
+        """
+        kwargs.update({"team_id": team_id})
+        return self.api_call("reminders.list", http_verb="GET", params=kwargs)
+
+    def rtm_connect(
+        self,
+        *,
+        batch_presence_aware: Optional[bool] = None,
+        presence_sub: Optional[bool] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Starts a Real Time Messaging session.
+        https://docs.slack.dev/reference/methods/rtm.connect
+        """
+        kwargs.update({"batch_presence_aware": batch_presence_aware, "presence_sub": presence_sub})
+        return self.api_call("rtm.connect", http_verb="GET", params=kwargs)
+
+    def rtm_start(
+        self,
+        *,
+        batch_presence_aware: Optional[bool] = None,
+        include_locale: Optional[bool] = None,
+        mpim_aware: Optional[bool] = None,
+        no_latest: Optional[bool] = None,
+        no_unreads: Optional[bool] = None,
+        presence_sub: Optional[bool] = None,
+        simple_latest: Optional[bool] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Starts a Real Time Messaging session.
+        https://docs.slack.dev/reference/methods/rtm.start
+        """
+        kwargs.update(
+            {
+                "batch_presence_aware": batch_presence_aware,
+                "include_locale": include_locale,
+                "mpim_aware": mpim_aware,
+                "no_latest": no_latest,
+                "no_unreads": no_unreads,
+                "presence_sub": presence_sub,
+                "simple_latest": simple_latest,
+            }
+        )
+        return self.api_call("rtm.start", http_verb="GET", params=kwargs)
+
+    def search_all(
+        self,
+        *,
+        query: str,
+        count: Optional[int] = None,
+        highlight: Optional[bool] = None,
+        page: Optional[int] = None,
+        sort: Optional[str] = None,
+        sort_dir: Optional[str] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Searches for messages and files matching a query.
+        https://docs.slack.dev/reference/methods/search.all
+        """
+        kwargs.update(
+            {
+                "query": query,
+                "count": count,
+                "highlight": highlight,
+                "page": page,
+                "sort": sort,
+                "sort_dir": sort_dir,
+                "team_id": team_id,
+            }
+        )
+        return self.api_call("search.all", http_verb="GET", params=kwargs)
+
+    def search_files(
+        self,
+        *,
+        query: str,
+        count: Optional[int] = None,
+        highlight: Optional[bool] = None,
+        page: Optional[int] = None,
+        sort: Optional[str] = None,
+        sort_dir: Optional[str] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Searches for files matching a query.
+        https://docs.slack.dev/reference/methods/search.files
+        """
+        kwargs.update(
+            {
+                "query": query,
+                "count": count,
+                "highlight": highlight,
+                "page": page,
+                "sort": sort,
+                "sort_dir": sort_dir,
+                "team_id": team_id,
+            }
+        )
+        return self.api_call("search.files", http_verb="GET", params=kwargs)
+
+    def search_messages(
+        self,
+        *,
+        query: str,
+        count: Optional[int] = None,
+        cursor: Optional[str] = None,
+        highlight: Optional[bool] = None,
+        page: Optional[int] = None,
+        sort: Optional[str] = None,
+        sort_dir: Optional[str] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Searches for messages matching a query.
+        https://docs.slack.dev/reference/methods/search.messages
+        """
+        kwargs.update(
+            {
+                "query": query,
+                "count": count,
+                "cursor": cursor,
+                "highlight": highlight,
+                "page": page,
+                "sort": sort,
+                "sort_dir": sort_dir,
+                "team_id": team_id,
+            }
+        )
+        return self.api_call("search.messages", http_verb="GET", params=kwargs)
+
+    def slackLists_access_delete(
+        self,
+        *,
+        list_id: str,
+        channel_ids: Optional[List[str]] = None,
+        user_ids: Optional[List[str]] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Revoke access to a List for specified entities.
+        https://docs.slack.dev/reference/methods/slackLists.access.delete
+        """
+        kwargs.update({"list_id": list_id, "channel_ids": channel_ids, "user_ids": user_ids})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("slackLists.access.delete", json=kwargs)
+
+    def slackLists_access_set(
+        self,
+        *,
+        list_id: str,
+        access_level: str,
+        channel_ids: Optional[List[str]] = None,
+        user_ids: Optional[List[str]] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Set the access level to a List for specified entities.
+        https://docs.slack.dev/reference/methods/slackLists.access.set
+        """
+        kwargs.update({"list_id": list_id, "access_level": access_level, "channel_ids": channel_ids, "user_ids": user_ids})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("slackLists.access.set", json=kwargs)
+
+    def slackLists_create(
+        self,
+        *,
+        name: str,
+        description_blocks: Optional[Union[str, Sequence[Union[Dict, RichTextBlock]]]] = None,
+        schema: Optional[List[Dict[str, Any]]] = None,
+        copy_from_list_id: Optional[str] = None,
+        include_copied_list_records: Optional[bool] = None,
+        todo_mode: Optional[bool] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Creates a List.
+        https://docs.slack.dev/reference/methods/slackLists.create
+        """
+        kwargs.update(
+            {
+                "name": name,
+                "description_blocks": description_blocks,
+                "schema": schema,
+                "copy_from_list_id": copy_from_list_id,
+                "include_copied_list_records": include_copied_list_records,
+                "todo_mode": todo_mode,
+            }
+        )
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("slackLists.create", json=kwargs)
+
+    def slackLists_download_get(
+        self,
+        *,
+        list_id: str,
+        job_id: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Retrieve List download URL from an export job to download List contents.
+        https://docs.slack.dev/reference/methods/slackLists.download.get
+        """
+        kwargs.update(
+            {
+                "list_id": list_id,
+                "job_id": job_id,
+            }
+        )
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("slackLists.download.get", json=kwargs)
+
+    def slackLists_download_start(
+        self,
+        *,
+        list_id: str,
+        include_archived: Optional[bool] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Initiate a job to export List contents.
+        https://docs.slack.dev/reference/methods/slackLists.download.start
+        """
+        kwargs.update(
+            {
+                "list_id": list_id,
+                "include_archived": include_archived,
+            }
+        )
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("slackLists.download.start", json=kwargs)
+
+    def slackLists_items_create(
+        self,
+        *,
+        list_id: str,
+        duplicated_item_id: Optional[str] = None,
+        parent_item_id: Optional[str] = None,
+        initial_fields: Optional[List[Dict[str, Any]]] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Add a new item to an existing List.
+        https://docs.slack.dev/reference/methods/slackLists.items.create
+        """
+        kwargs.update(
+            {
+                "list_id": list_id,
+                "duplicated_item_id": duplicated_item_id,
+                "parent_item_id": parent_item_id,
+                "initial_fields": initial_fields,
+            }
+        )
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("slackLists.items.create", json=kwargs)
+
+    def slackLists_items_delete(
+        self,
+        *,
+        list_id: str,
+        id: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Deletes an item from an existing List.
+        https://docs.slack.dev/reference/methods/slackLists.items.delete
+        """
+        kwargs.update(
+            {
+                "list_id": list_id,
+                "id": id,
+            }
+        )
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("slackLists.items.delete", json=kwargs)
+
+    def slackLists_items_deleteMultiple(
+        self,
+        *,
+        list_id: str,
+        ids: List[str],
+        **kwargs,
+    ) -> SlackResponse:
+        """Deletes multiple items from an existing List.
+        https://docs.slack.dev/reference/methods/slackLists.items.deleteMultiple
+        """
+        kwargs.update(
+            {
+                "list_id": list_id,
+                "ids": ids,
+            }
+        )
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("slackLists.items.deleteMultiple", json=kwargs)
+
+    def slackLists_items_info(
+        self,
+        *,
+        list_id: str,
+        id: str,
+        include_is_subscribed: Optional[bool] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Get a row from a List.
+        https://docs.slack.dev/reference/methods/slackLists.items.info
+        """
+        kwargs.update(
+            {
+                "list_id": list_id,
+                "id": id,
+                "include_is_subscribed": include_is_subscribed,
+            }
+        )
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("slackLists.items.info", json=kwargs)
+
+    def slackLists_items_list(
+        self,
+        *,
+        list_id: str,
+        limit: Optional[int] = None,
+        cursor: Optional[str] = None,
+        archived: Optional[bool] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Get records from a List.
+        https://docs.slack.dev/reference/methods/slackLists.items.list
+        """
+        kwargs.update(
+            {
+                "list_id": list_id,
+                "limit": limit,
+                "cursor": cursor,
+                "archived": archived,
+            }
+        )
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("slackLists.items.list", json=kwargs)
+
+    def slackLists_items_update(
+        self,
+        *,
+        list_id: str,
+        cells: List[Dict[str, Any]],
+        **kwargs,
+    ) -> SlackResponse:
+        """Updates cells in a List.
+        https://docs.slack.dev/reference/methods/slackLists.items.update
+        """
+        kwargs.update(
+            {
+                "list_id": list_id,
+                "cells": cells,
+            }
+        )
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("slackLists.items.update", json=kwargs)
+
+    def slackLists_update(
+        self,
+        *,
+        id: str,
+        name: Optional[str] = None,
+        description_blocks: Optional[Union[str, Sequence[Union[Dict, RichTextBlock]]]] = None,
+        todo_mode: Optional[bool] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Update a List.
+        https://docs.slack.dev/reference/methods/slackLists.update
+        """
+        kwargs.update(
+            {
+                "id": id,
+                "name": name,
+                "description_blocks": description_blocks,
+                "todo_mode": todo_mode,
+            }
+        )
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("slackLists.update", json=kwargs)
+
+    def stars_add(
+        self,
+        *,
+        channel: Optional[str] = None,
+        file: Optional[str] = None,
+        file_comment: Optional[str] = None,
+        timestamp: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Adds a star to an item.
+        https://docs.slack.dev/reference/methods/stars.add
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "file": file,
+                "file_comment": file_comment,
+                "timestamp": timestamp,
+            }
+        )
+        return self.api_call("stars.add", params=kwargs)
+
+    def stars_list(
+        self,
+        *,
+        count: Optional[int] = None,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        page: Optional[int] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Lists stars for a user.
+        https://docs.slack.dev/reference/methods/stars.list
+        """
+        kwargs.update(
+            {
+                "count": count,
+                "cursor": cursor,
+                "limit": limit,
+                "page": page,
+                "team_id": team_id,
+            }
+        )
+        return self.api_call("stars.list", http_verb="GET", params=kwargs)
+
+    def stars_remove(
+        self,
+        *,
+        channel: Optional[str] = None,
+        file: Optional[str] = None,
+        file_comment: Optional[str] = None,
+        timestamp: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Removes a star from an item.
+        https://docs.slack.dev/reference/methods/stars.remove
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "file": file,
+                "file_comment": file_comment,
+                "timestamp": timestamp,
+            }
+        )
+        return self.api_call("stars.remove", params=kwargs)
+
+    def team_accessLogs(
+        self,
+        *,
+        before: Optional[Union[int, str]] = None,
+        count: Optional[Union[int, str]] = None,
+        page: Optional[Union[int, str]] = None,
+        team_id: Optional[str] = None,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Gets the access logs for the current team.
+        https://docs.slack.dev/reference/methods/team.accessLogs
+        """
+        kwargs.update(
+            {
+                "before": before,
+                "count": count,
+                "page": page,
+                "team_id": team_id,
+                "cursor": cursor,
+                "limit": limit,
+            }
+        )
+        return self.api_call("team.accessLogs", http_verb="GET", params=kwargs)
+
+    def team_billableInfo(
+        self,
+        *,
+        team_id: Optional[str] = None,
+        user: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Gets billable users information for the current team.
+        https://docs.slack.dev/reference/methods/team.billableInfo
+        """
+        kwargs.update({"team_id": team_id, "user": user})
+        return self.api_call("team.billableInfo", http_verb="GET", params=kwargs)
+
+    def team_billing_info(
+        self,
+        **kwargs,
+    ) -> SlackResponse:
+        """Reads a workspace's billing plan information.
+        https://docs.slack.dev/reference/methods/team.billing.info
+        """
+        return self.api_call("team.billing.info", params=kwargs)
+
+    def team_externalTeams_disconnect(
+        self,
+        *,
+        target_team: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Disconnects an external organization.
+        https://docs.slack.dev/reference/methods/team.externalTeams.disconnect
+        """
+        kwargs.update(
+            {
+                "target_team": target_team,
+            }
+        )
+        return self.api_call("team.externalTeams.disconnect", params=kwargs)
+
+    def team_externalTeams_list(
+        self,
+        *,
+        connection_status_filter: Optional[str] = None,
+        slack_connect_pref_filter: Optional[Sequence[str]] = None,
+        sort_direction: Optional[str] = None,
+        sort_field: Optional[str] = None,
+        workspace_filter: Optional[Sequence[str]] = None,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Returns a list of all the external teams connected and details about the connection.
+        https://docs.slack.dev/reference/methods/team.externalTeams.list
+        """
+        kwargs.update(
+            {
+                "connection_status_filter": connection_status_filter,
+                "sort_direction": sort_direction,
+                "sort_field": sort_field,
+                "cursor": cursor,
+                "limit": limit,
+            }
+        )
+        if slack_connect_pref_filter is not None:
+            if isinstance(slack_connect_pref_filter, (list, tuple)):
+                kwargs.update({"slack_connect_pref_filter": ",".join(slack_connect_pref_filter)})
+            else:
+                kwargs.update({"slack_connect_pref_filter": slack_connect_pref_filter})
+        if workspace_filter is not None:
+            if isinstance(workspace_filter, (list, tuple)):
+                kwargs.update({"workspace_filter": ",".join(workspace_filter)})
+            else:
+                kwargs.update({"workspace_filter": workspace_filter})
+        return self.api_call("team.externalTeams.list", http_verb="GET", params=kwargs)
+
+    def team_info(
+        self,
+        *,
+        team: Optional[str] = None,
+        domain: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Gets information about the current team.
+        https://docs.slack.dev/reference/methods/team.info
+        """
+        kwargs.update({"team": team, "domain": domain})
+        return self.api_call("team.info", http_verb="GET", params=kwargs)
+
+    def team_integrationLogs(
+        self,
+        *,
+        app_id: Optional[str] = None,
+        change_type: Optional[str] = None,
+        count: Optional[Union[int, str]] = None,
+        page: Optional[Union[int, str]] = None,
+        service_id: Optional[str] = None,
+        team_id: Optional[str] = None,
+        user: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Gets the integration logs for the current team.
+        https://docs.slack.dev/reference/methods/team.integrationLogs
+        """
+        kwargs.update(
+            {
+                "app_id": app_id,
+                "change_type": change_type,
+                "count": count,
+                "page": page,
+                "service_id": service_id,
+                "team_id": team_id,
+                "user": user,
+            }
+        )
+        return self.api_call("team.integrationLogs", http_verb="GET", params=kwargs)
+
+    def team_profile_get(
+        self,
+        *,
+        visibility: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Retrieve a team's profile.
+        https://docs.slack.dev/reference/methods/team.profile.get
+        """
+        kwargs.update({"visibility": visibility})
+        return self.api_call("team.profile.get", http_verb="GET", params=kwargs)
+
+    def team_preferences_list(
+        self,
+        **kwargs,
+    ) -> SlackResponse:
+        """Retrieve a list of a workspace's team preferences.
+        https://docs.slack.dev/reference/methods/team.preferences.list
+        """
+        return self.api_call("team.preferences.list", params=kwargs)
+
+    def usergroups_create(
+        self,
+        *,
+        name: str,
+        channels: Optional[Union[str, Sequence[str]]] = None,
+        description: Optional[str] = None,
+        handle: Optional[str] = None,
+        include_count: Optional[bool] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Create a User Group
+        https://docs.slack.dev/reference/methods/usergroups.create
+        """
+        kwargs.update(
+            {
+                "name": name,
+                "description": description,
+                "handle": handle,
+                "include_count": include_count,
+                "team_id": team_id,
+            }
+        )
+        if isinstance(channels, (list, tuple)):
+            kwargs.update({"channels": ",".join(channels)})
+        else:
+            kwargs.update({"channels": channels})
+        return self.api_call("usergroups.create", params=kwargs)
+
+    def usergroups_disable(
+        self,
+        *,
+        usergroup: str,
+        include_count: Optional[bool] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Disable an existing User Group
+        https://docs.slack.dev/reference/methods/usergroups.disable
+        """
+        kwargs.update({"usergroup": usergroup, "include_count": include_count, "team_id": team_id})
+        return self.api_call("usergroups.disable", params=kwargs)
+
+    def usergroups_enable(
+        self,
+        *,
+        usergroup: str,
+        include_count: Optional[bool] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Enable a User Group
+        https://docs.slack.dev/reference/methods/usergroups.enable
+        """
+        kwargs.update({"usergroup": usergroup, "include_count": include_count, "team_id": team_id})
+        return self.api_call("usergroups.enable", params=kwargs)
+
+    def usergroups_list(
+        self,
+        *,
+        include_count: Optional[bool] = None,
+        include_disabled: Optional[bool] = None,
+        include_users: Optional[bool] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """List all User Groups for a team
+        https://docs.slack.dev/reference/methods/usergroups.list
+        """
+        kwargs.update(
+            {
+                "include_count": include_count,
+                "include_disabled": include_disabled,
+                "include_users": include_users,
+                "team_id": team_id,
+            }
+        )
+        return self.api_call("usergroups.list", http_verb="GET", params=kwargs)
+
+    def usergroups_update(
+        self,
+        *,
+        usergroup: str,
+        channels: Optional[Union[str, Sequence[str]]] = None,
+        description: Optional[str] = None,
+        handle: Optional[str] = None,
+        include_count: Optional[bool] = None,
+        name: Optional[str] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Update an existing User Group
+        https://docs.slack.dev/reference/methods/usergroups.update
+        """
+        kwargs.update(
+            {
+                "usergroup": usergroup,
+                "description": description,
+                "handle": handle,
+                "include_count": include_count,
+                "name": name,
+                "team_id": team_id,
+            }
+        )
+        if isinstance(channels, (list, tuple)):
+            kwargs.update({"channels": ",".join(channels)})
+        else:
+            kwargs.update({"channels": channels})
+        return self.api_call("usergroups.update", params=kwargs)
+
+    def usergroups_users_list(
+        self,
+        *,
+        usergroup: str,
+        include_disabled: Optional[bool] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """List all users in a User Group
+        https://docs.slack.dev/reference/methods/usergroups.users.list
+        """
+        kwargs.update(
+            {
+                "usergroup": usergroup,
+                "include_disabled": include_disabled,
+                "team_id": team_id,
+            }
+        )
+        return self.api_call("usergroups.users.list", http_verb="GET", params=kwargs)
+
+    def usergroups_users_update(
+        self,
+        *,
+        usergroup: str,
+        users: Union[str, Sequence[str]],
+        include_count: Optional[bool] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Update the list of users for a User Group
+        https://docs.slack.dev/reference/methods/usergroups.users.update
+        """
+        kwargs.update(
+            {
+                "usergroup": usergroup,
+                "include_count": include_count,
+                "team_id": team_id,
+            }
+        )
+        if isinstance(users, (list, tuple)):
+            kwargs.update({"users": ",".join(users)})
+        else:
+            kwargs.update({"users": users})
+        return self.api_call("usergroups.users.update", params=kwargs)
+
+    def users_conversations(
+        self,
+        *,
+        cursor: Optional[str] = None,
+        exclude_archived: Optional[bool] = None,
+        limit: Optional[int] = None,
+        team_id: Optional[str] = None,
+        types: Optional[Union[str, Sequence[str]]] = None,
+        user: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """List conversations the calling user may access.
+        https://docs.slack.dev/reference/methods/users.conversations
+        """
+        kwargs.update(
+            {
+                "cursor": cursor,
+                "exclude_archived": exclude_archived,
+                "limit": limit,
+                "team_id": team_id,
+                "user": user,
+            }
+        )
+        if isinstance(types, (list, tuple)):
+            kwargs.update({"types": ",".join(types)})
+        else:
+            kwargs.update({"types": types})
+        return self.api_call("users.conversations", http_verb="GET", params=kwargs)
+
+    def users_deletePhoto(
+        self,
+        **kwargs,
+    ) -> SlackResponse:
+        """Delete the user profile photo
+        https://docs.slack.dev/reference/methods/users.deletePhoto
+        """
+        return self.api_call("users.deletePhoto", http_verb="GET", params=kwargs)
+
+    def users_getPresence(
+        self,
+        *,
+        user: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Gets user presence information.
+        https://docs.slack.dev/reference/methods/users.getPresence
+        """
+        kwargs.update({"user": user})
+        return self.api_call("users.getPresence", http_verb="GET", params=kwargs)
+
+    def users_identity(
+        self,
+        **kwargs,
+    ) -> SlackResponse:
+        """Get a user's identity.
+        https://docs.slack.dev/reference/methods/users.identity
+        """
+        return self.api_call("users.identity", http_verb="GET", params=kwargs)
+
+    def users_info(
+        self,
+        *,
+        user: str,
+        include_locale: Optional[bool] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Gets information about a user.
+        https://docs.slack.dev/reference/methods/users.info
+        """
+        kwargs.update({"user": user, "include_locale": include_locale})
+        return self.api_call("users.info", http_verb="GET", params=kwargs)
+
+    def users_list(
+        self,
+        *,
+        cursor: Optional[str] = None,
+        include_locale: Optional[bool] = None,
+        limit: Optional[int] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Lists all users in a Slack team.
+        https://docs.slack.dev/reference/methods/users.list
+        """
+        kwargs.update(
+            {
+                "cursor": cursor,
+                "include_locale": include_locale,
+                "limit": limit,
+                "team_id": team_id,
+            }
+        )
+        return self.api_call("users.list", http_verb="GET", params=kwargs)
+
+    def users_lookupByEmail(
+        self,
+        *,
+        email: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Find a user with an email address.
+        https://docs.slack.dev/reference/methods/users.lookupByEmail
+        """
+        kwargs.update({"email": email})
+        return self.api_call("users.lookupByEmail", http_verb="GET", params=kwargs)
+
+    def users_setPhoto(
+        self,
+        *,
+        image: Union[str, IOBase],
+        crop_w: Optional[Union[int, str]] = None,
+        crop_x: Optional[Union[int, str]] = None,
+        crop_y: Optional[Union[int, str]] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Set the user profile photo
+        https://docs.slack.dev/reference/methods/users.setPhoto
+        """
+        kwargs.update({"crop_w": crop_w, "crop_x": crop_x, "crop_y": crop_y})
+        return self.api_call("users.setPhoto", files={"image": image}, data=kwargs)
+
+    def users_setPresence(
+        self,
+        *,
+        presence: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Manually sets user presence.
+        https://docs.slack.dev/reference/methods/users.setPresence
+        """
+        kwargs.update({"presence": presence})
+        return self.api_call("users.setPresence", params=kwargs)
+
+    def users_discoverableContacts_lookup(
+        self,
+        email: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Lookup an email address to see if someone is on Slack
+        https://docs.slack.dev/reference/methods/users.discoverableContacts.lookup
+        """
+        kwargs.update({"email": email})
+        return self.api_call("users.discoverableContacts.lookup", params=kwargs)
+
+    def users_profile_get(
+        self,
+        *,
+        user: Optional[str] = None,
+        include_labels: Optional[bool] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Retrieves a user's profile information.
+        https://docs.slack.dev/reference/methods/users.profile.get
+        """
+        kwargs.update({"user": user, "include_labels": include_labels})
+        return self.api_call("users.profile.get", http_verb="GET", params=kwargs)
+
+    def users_profile_set(
+        self,
+        *,
+        name: Optional[str] = None,
+        value: Optional[str] = None,
+        user: Optional[str] = None,
+        profile: Optional[Dict] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Set the profile information for a user.
+        https://docs.slack.dev/reference/methods/users.profile.set
+        """
+        kwargs.update(
+            {
+                "name": name,
+                "profile": profile,
+                "user": user,
+                "value": value,
+            }
+        )
+        kwargs = _remove_none_values(kwargs)
+        # NOTE: Intentionally using json for the "profile" parameter
+        return self.api_call("users.profile.set", json=kwargs)
+
+    def views_open(
+        self,
+        *,
+        trigger_id: Optional[str] = None,
+        interactivity_pointer: Optional[str] = None,
+        view: Union[dict, View],
+        **kwargs,
+    ) -> SlackResponse:
+        """Open a view for a user.
+        https://docs.slack.dev/reference/methods/views.open
+        See https://docs.slack.dev/surfaces/modals/ for details.
+        """
+        kwargs.update({"trigger_id": trigger_id, "interactivity_pointer": interactivity_pointer})
+        if isinstance(view, View):
+            kwargs.update({"view": view.to_dict()})
+        else:
+            kwargs.update({"view": view})
+        kwargs = _remove_none_values(kwargs)
+        # NOTE: Intentionally using json for the "view" parameter
+        return self.api_call("views.open", json=kwargs)
+
+    def views_push(
+        self,
+        *,
+        trigger_id: Optional[str] = None,
+        interactivity_pointer: Optional[str] = None,
+        view: Union[dict, View],
+        **kwargs,
+    ) -> SlackResponse:
+        """Push a view onto the stack of a root view.
+        Push a new view onto the existing view stack by passing a view
+        payload and a valid trigger_id generated from an interaction
+        within the existing modal.
+        Read the modals documentation (https://docs.slack.dev/surfaces/modals/)
+        to learn more about the lifecycle and intricacies of views.
+        https://docs.slack.dev/reference/methods/views.push
+        """
+        kwargs.update({"trigger_id": trigger_id, "interactivity_pointer": interactivity_pointer})
+        if isinstance(view, View):
+            kwargs.update({"view": view.to_dict()})
+        else:
+            kwargs.update({"view": view})
+        kwargs = _remove_none_values(kwargs)
+        # NOTE: Intentionally using json for the "view" parameter
+        return self.api_call("views.push", json=kwargs)
+
+    def views_update(
+        self,
+        *,
+        view: Union[dict, View],
+        external_id: Optional[str] = None,
+        view_id: Optional[str] = None,
+        hash: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Update an existing view.
+        Update a view by passing a new view definition along with the
+        view_id returned in views.open or the external_id.
+        See the modals documentation (https://docs.slack.dev/surfaces/modals/#updating_views)
+        to learn more about updating views and avoiding race conditions with the hash argument.
+        https://docs.slack.dev/reference/methods/views.update
+        """
+        if isinstance(view, View):
+            kwargs.update({"view": view.to_dict()})
+        else:
+            kwargs.update({"view": view})
+        if external_id:
+            kwargs.update({"external_id": external_id})
+        elif view_id:
+            kwargs.update({"view_id": view_id})
+        else:
+            raise e.SlackRequestError("Either view_id or external_id is required.")
+        kwargs.update({"hash": hash})
+        kwargs = _remove_none_values(kwargs)
+        # NOTE: Intentionally using json for the "view" parameter
+        return self.api_call("views.update", json=kwargs)
+
+    def views_publish(
+        self,
+        *,
+        user_id: str,
+        view: Union[dict, View],
+        hash: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Publish a static view for a User.
+        Create or update the view that comprises an
+        app's Home tab (https://docs.slack.dev/surfaces/app-home/)
+        https://docs.slack.dev/reference/methods/views.publish
+        """
+        kwargs.update({"user_id": user_id, "hash": hash})
+        if isinstance(view, View):
+            kwargs.update({"view": view.to_dict()})
+        else:
+            kwargs.update({"view": view})
+        kwargs = _remove_none_values(kwargs)
+        # NOTE: Intentionally using json for the "view" parameter
+        return self.api_call("views.publish", json=kwargs)
+
+    def workflows_featured_add(
+        self,
+        *,
+        channel_id: str,
+        trigger_ids: Union[str, Sequence[str]],
+        **kwargs,
+    ) -> SlackResponse:
+        """Add featured workflows to a channel.
+        https://docs.slack.dev/reference/methods/workflows.featured.add
+        """
+        kwargs.update({"channel_id": channel_id})
+        if isinstance(trigger_ids, (list, tuple)):
+            kwargs.update({"trigger_ids": ",".join(trigger_ids)})
+        else:
+            kwargs.update({"trigger_ids": trigger_ids})
+        return self.api_call("workflows.featured.add", params=kwargs)
+
+    def workflows_featured_list(
+        self,
+        *,
+        channel_ids: Union[str, Sequence[str]],
+        **kwargs,
+    ) -> SlackResponse:
+        """List the featured workflows for specified channels.
+        https://docs.slack.dev/reference/methods/workflows.featured.list
+        """
+        if isinstance(channel_ids, (list, tuple)):
+            kwargs.update({"channel_ids": ",".join(channel_ids)})
+        else:
+            kwargs.update({"channel_ids": channel_ids})
+        return self.api_call("workflows.featured.list", params=kwargs)
+
+    def workflows_featured_remove(
+        self,
+        *,
+        channel_id: str,
+        trigger_ids: Union[str, Sequence[str]],
+        **kwargs,
+    ) -> SlackResponse:
+        """Remove featured workflows from a channel.
+        https://docs.slack.dev/reference/methods/workflows.featured.remove
+        """
+        kwargs.update({"channel_id": channel_id})
+        if isinstance(trigger_ids, (list, tuple)):
+            kwargs.update({"trigger_ids": ",".join(trigger_ids)})
+        else:
+            kwargs.update({"trigger_ids": trigger_ids})
+        return self.api_call("workflows.featured.remove", params=kwargs)
+
+    def workflows_featured_set(
+        self,
+        *,
+        channel_id: str,
+        trigger_ids: Union[str, Sequence[str]],
+        **kwargs,
+    ) -> SlackResponse:
+        """Set featured workflows for a channel.
+        https://docs.slack.dev/reference/methods/workflows.featured.set
+        """
+        kwargs.update({"channel_id": channel_id})
+        if isinstance(trigger_ids, (list, tuple)):
+            kwargs.update({"trigger_ids": ",".join(trigger_ids)})
+        else:
+            kwargs.update({"trigger_ids": trigger_ids})
+        return self.api_call("workflows.featured.set", params=kwargs)
+
+    def workflows_stepCompleted(
+        self,
+        *,
+        workflow_step_execute_id: str,
+        outputs: Optional[dict] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Indicate a successful outcome of a workflow step's execution.
+        https://docs.slack.dev/reference/methods/workflows.stepCompleted
+        """
+        kwargs.update({"workflow_step_execute_id": workflow_step_execute_id})
+        if outputs is not None:
+            kwargs.update({"outputs": outputs})
+        kwargs = _remove_none_values(kwargs)
+        # NOTE: Intentionally using json for the "outputs" parameter
+        return self.api_call("workflows.stepCompleted", json=kwargs)
+
+    def workflows_stepFailed(
+        self,
+        *,
+        workflow_step_execute_id: str,
+        error: Dict[str, str],
+        **kwargs,
+    ) -> SlackResponse:
+        """Indicate an unsuccessful outcome of a workflow step's execution.
+        https://docs.slack.dev/reference/methods/workflows.stepFailed
+        """
+        kwargs.update(
+            {
+                "workflow_step_execute_id": workflow_step_execute_id,
+                "error": error,
+            }
+        )
+        kwargs = _remove_none_values(kwargs)
+        # NOTE: Intentionally using json for the "error" parameter
+        return self.api_call("workflows.stepFailed", json=kwargs)
+
+    def workflows_updateStep(
+        self,
+        *,
+        workflow_step_edit_id: str,
+        inputs: Optional[Dict[str, Any]] = None,
+        outputs: Optional[List[Dict[str, str]]] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Update the configuration for a workflow extension step.
+        https://docs.slack.dev/reference/methods/workflows.updateStep
+        """
+        kwargs.update({"workflow_step_edit_id": workflow_step_edit_id})
+        if inputs is not None:
+            kwargs.update({"inputs": inputs})
+        if outputs is not None:
+            kwargs.update({"outputs": outputs})
+        kwargs = _remove_none_values(kwargs)
+        # NOTE: Intentionally using json for the "inputs" / "outputs" parameters
+        return self.api_call("workflows.updateStep", json=kwargs)
+
+

A WebClient allows apps to communicate with the Slack Platform's Web API.

+

https://docs.slack.dev/reference/methods

+

The Slack Web API is an interface for querying information from +and enacting change in a Slack workspace.

+

This client handles constructing and sending HTTP requests to Slack +as well as parsing any responses received into a SlackResponse.

+

Attributes

+
+
token : str
+
A string specifying an xoxp-* or xoxb-* token.
+
base_url : str
+
A string representing the Slack API base URL. +Default is 'https://slack.com/api/'
+
timeout : int
+
The maximum number of seconds the client will wait +to connect and receive a response from Slack. +Default is 30 seconds.
+
ssl : SSLContext
+
An ssl.SSLContext instance, helpful for specifying +your own custom certificate chain.
+
proxy : str
+
String representing a fully-qualified URL to a proxy through +which to route all requests to the Slack API. Even if this parameter +is not specified, if any of the following environment variables are +present, they will be loaded into this parameter: HTTPS_PROXY, +https_proxy, HTTP_PROXY or http_proxy.
+
headers : dict
+
Additional request headers to attach to all requests.
+
+

Methods

+

api_call: Constructs a request and executes the API call to Slack.

+

Example of recommended usage:

+
    import os
+    from slack_sdk import WebClient
+
+    client = WebClient(token=os.environ['SLACK_API_TOKEN'])
+    response = client.chat_postMessage(
+        channel='#random',
+        text="Hello world!")
+    assert response["ok"]
+    assert response["message"]["text"] == "Hello world!"
+
+

Example manually creating an API request:

+
    import os
+    from slack_sdk import WebClient
+
+    client = WebClient(token=os.environ['SLACK_API_TOKEN'])
+    response = client.api_call(
+        api_method='chat.postMessage',
+        json={'channel': '#random','text': "Hello world!"}
+    )
+    assert response["ok"]
+    assert response["message"]["text"] == "Hello world!"
+
+

Note

+

Any attributes or methods prefixed with _underscores are +intended to be "private" internal use only. They may be changed or +removed at anytime.

+

Ancestors

+ +

Methods

+
+
+def admin_analytics_getFile(self,
*,
type: str,
date: str | None = None,
metadata_only: bool | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_analytics_getFile(
+    self,
+    *,
+    type: str,
+    date: Optional[str] = None,
+    metadata_only: Optional[bool] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Retrieve analytics data for a given date, presented as a compressed JSON file
+    https://docs.slack.dev/reference/methods/admin.analytics.getFile
+    """
+    kwargs.update({"type": type})
+    if date is not None:
+        kwargs.update({"date": date})
+    if metadata_only is not None:
+        kwargs.update({"metadata_only": metadata_only})
+    return self.api_call("admin.analytics.getFile", params=kwargs)
+
+

Retrieve analytics data for a given date, presented as a compressed JSON file +https://docs.slack.dev/reference/methods/admin.analytics.getFile

+
+
+def admin_apps_activities_list(self,
*,
app_id: str | None = None,
component_id: str | None = None,
component_type: str | None = None,
log_event_type: str | None = None,
max_date_created: int | None = None,
min_date_created: int | None = None,
min_log_level: str | None = None,
sort_direction: str | None = None,
source: str | None = None,
team_id: str | None = None,
trace_id: str | None = None,
cursor: str | None = None,
limit: int | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_apps_activities_list(
+    self,
+    *,
+    app_id: Optional[str] = None,
+    component_id: Optional[str] = None,
+    component_type: Optional[str] = None,
+    log_event_type: Optional[str] = None,
+    max_date_created: Optional[int] = None,
+    min_date_created: Optional[int] = None,
+    min_log_level: Optional[str] = None,
+    sort_direction: Optional[str] = None,
+    source: Optional[str] = None,
+    team_id: Optional[str] = None,
+    trace_id: Optional[str] = None,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Get logs for a specified team/org
+    https://docs.slack.dev/reference/methods/admin.apps.activities.list
+    """
+    kwargs.update(
+        {
+            "app_id": app_id,
+            "component_id": component_id,
+            "component_type": component_type,
+            "log_event_type": log_event_type,
+            "max_date_created": max_date_created,
+            "min_date_created": min_date_created,
+            "min_log_level": min_log_level,
+            "sort_direction": sort_direction,
+            "source": source,
+            "team_id": team_id,
+            "trace_id": trace_id,
+            "cursor": cursor,
+            "limit": limit,
+        }
+    )
+    return self.api_call("admin.apps.activities.list", params=kwargs)
+
+ +
+
+def admin_apps_approve(self,
*,
app_id: str | None = None,
request_id: str | None = None,
enterprise_id: str | None = None,
team_id: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_apps_approve(
+    self,
+    *,
+    app_id: Optional[str] = None,
+    request_id: Optional[str] = None,
+    enterprise_id: Optional[str] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Approve an app for installation on a workspace.
+    Either app_id or request_id is required.
+    These IDs can be obtained either directly via the app_requested event,
+    or by the admin.apps.requests.list method.
+    https://docs.slack.dev/reference/methods/admin.apps.approve
+    """
+    if app_id:
+        kwargs.update({"app_id": app_id})
+    elif request_id:
+        kwargs.update({"request_id": request_id})
+    else:
+        raise e.SlackRequestError("The app_id or request_id argument must be specified.")
+
+    kwargs.update(
+        {
+            "enterprise_id": enterprise_id,
+            "team_id": team_id,
+        }
+    )
+    return self.api_call("admin.apps.approve", params=kwargs)
+
+

Approve an app for installation on a workspace. +Either app_id or request_id is required. +These IDs can be obtained either directly via the app_requested event, +or by the admin.apps.requests.list method. +https://docs.slack.dev/reference/methods/admin.apps.approve

+
+
+def admin_apps_approved_list(self,
*,
cursor: str | None = None,
limit: int | None = None,
enterprise_id: str | None = None,
team_id: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_apps_approved_list(
+    self,
+    *,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    enterprise_id: Optional[str] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """List approved apps for an org or workspace.
+    https://docs.slack.dev/reference/methods/admin.apps.approved.list
+    """
+    kwargs.update(
+        {
+            "cursor": cursor,
+            "limit": limit,
+            "enterprise_id": enterprise_id,
+            "team_id": team_id,
+        }
+    )
+    return self.api_call("admin.apps.approved.list", http_verb="GET", params=kwargs)
+
+

List approved apps for an org or workspace. +https://docs.slack.dev/reference/methods/admin.apps.approved.list

+
+
+def admin_apps_clearResolution(self,
*,
app_id: str,
enterprise_id: str | None = None,
team_id: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_apps_clearResolution(
+    self,
+    *,
+    app_id: str,
+    enterprise_id: Optional[str] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Clear an app resolution
+    https://docs.slack.dev/reference/methods/admin.apps.clearResolution
+    """
+    kwargs.update(
+        {
+            "app_id": app_id,
+            "enterprise_id": enterprise_id,
+            "team_id": team_id,
+        }
+    )
+    return self.api_call("admin.apps.clearResolution", http_verb="POST", params=kwargs)
+
+ +
+
+def admin_apps_config_lookup(self, *, app_ids: str | Sequence[str], **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_apps_config_lookup(
+    self,
+    *,
+    app_ids: Union[str, Sequence[str]],
+    **kwargs,
+) -> SlackResponse:
+    """Look up the app config for connectors by their IDs
+    https://docs.slack.dev/reference/methods/admin.apps.config.lookup
+    """
+    if isinstance(app_ids, (list, tuple)):
+        kwargs.update({"app_ids": ",".join(app_ids)})
+    else:
+        kwargs.update({"app_ids": app_ids})
+    return self.api_call("admin.apps.config.lookup", params=kwargs)
+
+

Look up the app config for connectors by their IDs +https://docs.slack.dev/reference/methods/admin.apps.config.lookup

+
+
+def admin_apps_config_set(self,
*,
app_id: str,
domain_restrictions: Dict[str, Any] | None = None,
workflow_auth_strategy: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_apps_config_set(
+    self,
+    *,
+    app_id: str,
+    domain_restrictions: Optional[Dict[str, Any]] = None,
+    workflow_auth_strategy: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Set the app config for a connector
+    https://docs.slack.dev/reference/methods/admin.apps.config.set
+    """
+    kwargs.update(
+        {
+            "app_id": app_id,
+            "workflow_auth_strategy": workflow_auth_strategy,
+        }
+    )
+    if domain_restrictions is not None:
+        kwargs.update({"domain_restrictions": json.dumps(domain_restrictions)})
+    return self.api_call("admin.apps.config.set", params=kwargs)
+
+ +
+
+def admin_apps_requests_cancel(self,
*,
request_id: str,
enterprise_id: str | None = None,
team_id: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_apps_requests_cancel(
+    self,
+    *,
+    request_id: str,
+    enterprise_id: Optional[str] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """List app requests for a team/workspace.
+    https://docs.slack.dev/reference/methods/admin.apps.requests.cancel
+    """
+    kwargs.update(
+        {
+            "request_id": request_id,
+            "enterprise_id": enterprise_id,
+            "team_id": team_id,
+        }
+    )
+    return self.api_call("admin.apps.requests.cancel", http_verb="POST", params=kwargs)
+
+ +
+
+def admin_apps_requests_list(self,
*,
cursor: str | None = None,
limit: int | None = None,
team_id: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_apps_requests_list(
+    self,
+    *,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """List app requests for a team/workspace.
+    https://docs.slack.dev/reference/methods/admin.apps.requests.list
+    """
+    kwargs.update(
+        {
+            "cursor": cursor,
+            "limit": limit,
+            "team_id": team_id,
+        }
+    )
+    return self.api_call("admin.apps.requests.list", http_verb="GET", params=kwargs)
+
+ +
+
+def admin_apps_restrict(self,
*,
app_id: str | None = None,
request_id: str | None = None,
enterprise_id: str | None = None,
team_id: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_apps_restrict(
+    self,
+    *,
+    app_id: Optional[str] = None,
+    request_id: Optional[str] = None,
+    enterprise_id: Optional[str] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Restrict an app for installation on a workspace.
+    Exactly one of the team_id or enterprise_id arguments is required, not both.
+    Either app_id or request_id is required. These IDs can be obtained either directly
+    via the app_requested event, or by the admin.apps.requests.list method.
+    https://docs.slack.dev/reference/methods/admin.apps.restrict
+    """
+    if app_id:
+        kwargs.update({"app_id": app_id})
+    elif request_id:
+        kwargs.update({"request_id": request_id})
+    else:
+        raise e.SlackRequestError("The app_id or request_id argument must be specified.")
+
+    kwargs.update(
+        {
+            "enterprise_id": enterprise_id,
+            "team_id": team_id,
+        }
+    )
+    return self.api_call("admin.apps.restrict", params=kwargs)
+
+

Restrict an app for installation on a workspace. +Exactly one of the team_id or enterprise_id arguments is required, not both. +Either app_id or request_id is required. These IDs can be obtained either directly +via the app_requested event, or by the admin.apps.requests.list method. +https://docs.slack.dev/reference/methods/admin.apps.restrict

+
+
+def admin_apps_restricted_list(self,
*,
cursor: str | None = None,
limit: int | None = None,
enterprise_id: str | None = None,
team_id: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_apps_restricted_list(
+    self,
+    *,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    enterprise_id: Optional[str] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """List restricted apps for an org or workspace.
+    https://docs.slack.dev/reference/methods/admin.apps.restricted.list
+    """
+    kwargs.update(
+        {
+            "cursor": cursor,
+            "limit": limit,
+            "enterprise_id": enterprise_id,
+            "team_id": team_id,
+        }
+    )
+    return self.api_call("admin.apps.restricted.list", http_verb="GET", params=kwargs)
+
+

List restricted apps for an org or workspace. +https://docs.slack.dev/reference/methods/admin.apps.restricted.list

+
+
+def admin_apps_uninstall(self,
*,
app_id: str,
enterprise_id: str | None = None,
team_ids: str | Sequence[str] | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_apps_uninstall(
+    self,
+    *,
+    app_id: str,
+    enterprise_id: Optional[str] = None,
+    team_ids: Optional[Union[str, Sequence[str]]] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Uninstall an app from one or many workspaces, or an entire enterprise organization.
+    With an org-level token, enterprise_id or team_ids is required.
+    https://docs.slack.dev/reference/methods/admin.apps.uninstall
+    """
+    kwargs.update({"app_id": app_id})
+    if enterprise_id is not None:
+        kwargs.update({"enterprise_id": enterprise_id})
+    if team_ids is not None:
+        if isinstance(team_ids, (list, tuple)):
+            kwargs.update({"team_ids": ",".join(team_ids)})
+        else:
+            kwargs.update({"team_ids": team_ids})
+    return self.api_call("admin.apps.uninstall", http_verb="POST", params=kwargs)
+
+

Uninstall an app from one or many workspaces, or an entire enterprise organization. +With an org-level token, enterprise_id or team_ids is required. +https://docs.slack.dev/reference/methods/admin.apps.uninstall

+
+
+def admin_auth_policy_assignEntities(self,
*,
entity_ids: str | Sequence[str],
policy_name: str,
entity_type: str,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_auth_policy_assignEntities(
+    self,
+    *,
+    entity_ids: Union[str, Sequence[str]],
+    policy_name: str,
+    entity_type: str,
+    **kwargs,
+) -> SlackResponse:
+    """Assign entities to a particular authentication policy.
+    https://docs.slack.dev/reference/methods/admin.auth.policy.assignEntities
+    """
+    if isinstance(entity_ids, (list, tuple)):
+        kwargs.update({"entity_ids": ",".join(entity_ids)})
+    else:
+        kwargs.update({"entity_ids": entity_ids})
+    kwargs.update({"policy_name": policy_name})
+    kwargs.update({"entity_type": entity_type})
+    return self.api_call("admin.auth.policy.assignEntities", http_verb="POST", params=kwargs)
+
+

Assign entities to a particular authentication policy. +https://docs.slack.dev/reference/methods/admin.auth.policy.assignEntities

+
+
+def admin_auth_policy_getEntities(self,
*,
policy_name: str,
cursor: str | None = None,
entity_type: str | None = None,
limit: int | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_auth_policy_getEntities(
+    self,
+    *,
+    policy_name: str,
+    cursor: Optional[str] = None,
+    entity_type: Optional[str] = None,
+    limit: Optional[int] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Fetch all the entities assigned to a particular authentication policy by name.
+    https://docs.slack.dev/reference/methods/admin.auth.policy.getEntities
+    """
+    kwargs.update({"policy_name": policy_name})
+    if cursor is not None:
+        kwargs.update({"cursor": cursor})
+    if entity_type is not None:
+        kwargs.update({"entity_type": entity_type})
+    if limit is not None:
+        kwargs.update({"limit": limit})
+    return self.api_call("admin.auth.policy.getEntities", http_verb="POST", params=kwargs)
+
+

Fetch all the entities assigned to a particular authentication policy by name. +https://docs.slack.dev/reference/methods/admin.auth.policy.getEntities

+
+
+def admin_auth_policy_removeEntities(self,
*,
entity_ids: str | Sequence[str],
policy_name: str,
entity_type: str,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_auth_policy_removeEntities(
+    self,
+    *,
+    entity_ids: Union[str, Sequence[str]],
+    policy_name: str,
+    entity_type: str,
+    **kwargs,
+) -> SlackResponse:
+    """Remove specified entities from a specified authentication policy.
+    https://docs.slack.dev/reference/methods/admin.auth.policy.removeEntities
+    """
+    if isinstance(entity_ids, (list, tuple)):
+        kwargs.update({"entity_ids": ",".join(entity_ids)})
+    else:
+        kwargs.update({"entity_ids": entity_ids})
+    kwargs.update({"policy_name": policy_name})
+    kwargs.update({"entity_type": entity_type})
+    return self.api_call("admin.auth.policy.removeEntities", http_verb="POST", params=kwargs)
+
+

Remove specified entities from a specified authentication policy. +https://docs.slack.dev/reference/methods/admin.auth.policy.removeEntities

+
+
+def admin_barriers_create(self,
*,
barriered_from_usergroup_ids: str | Sequence[str],
primary_usergroup_id: str,
restricted_subjects: str | Sequence[str],
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_barriers_create(
+    self,
+    *,
+    barriered_from_usergroup_ids: Union[str, Sequence[str]],
+    primary_usergroup_id: str,
+    restricted_subjects: Union[str, Sequence[str]],
+    **kwargs,
+) -> SlackResponse:
+    """Create an Information Barrier
+    https://docs.slack.dev/reference/methods/admin.barriers.create
+    """
+    kwargs.update({"primary_usergroup_id": primary_usergroup_id})
+    if isinstance(barriered_from_usergroup_ids, (list, tuple)):
+        kwargs.update({"barriered_from_usergroup_ids": ",".join(barriered_from_usergroup_ids)})
+    else:
+        kwargs.update({"barriered_from_usergroup_ids": barriered_from_usergroup_ids})
+    if isinstance(restricted_subjects, (list, tuple)):
+        kwargs.update({"restricted_subjects": ",".join(restricted_subjects)})
+    else:
+        kwargs.update({"restricted_subjects": restricted_subjects})
+    return self.api_call("admin.barriers.create", http_verb="POST", params=kwargs)
+
+ +
+
+def admin_barriers_delete(self, *, barrier_id: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_barriers_delete(
+    self,
+    *,
+    barrier_id: str,
+    **kwargs,
+) -> SlackResponse:
+    """Delete an existing Information Barrier
+    https://docs.slack.dev/reference/methods/admin.barriers.delete
+    """
+    kwargs.update({"barrier_id": barrier_id})
+    return self.api_call("admin.barriers.delete", http_verb="POST", params=kwargs)
+
+ +
+
+def admin_barriers_list(self, *, cursor: str | None = None, limit: int | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_barriers_list(
+    self,
+    *,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Get all Information Barriers for your organization
+    https://docs.slack.dev/reference/methods/admin.barriers.list"""
+    kwargs.update(
+        {
+            "cursor": cursor,
+            "limit": limit,
+        }
+    )
+    return self.api_call("admin.barriers.list", http_verb="GET", params=kwargs)
+
+

Get all Information Barriers for your organization +https://docs.slack.dev/reference/methods/admin.barriers.list

+
+
+def admin_barriers_update(self,
*,
barrier_id: str,
barriered_from_usergroup_ids: str | Sequence[str],
primary_usergroup_id: str,
restricted_subjects: str | Sequence[str],
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_barriers_update(
+    self,
+    *,
+    barrier_id: str,
+    barriered_from_usergroup_ids: Union[str, Sequence[str]],
+    primary_usergroup_id: str,
+    restricted_subjects: Union[str, Sequence[str]],
+    **kwargs,
+) -> SlackResponse:
+    """Update an existing Information Barrier
+    https://docs.slack.dev/reference/methods/admin.barriers.update
+    """
+    kwargs.update({"barrier_id": barrier_id, "primary_usergroup_id": primary_usergroup_id})
+    if isinstance(barriered_from_usergroup_ids, (list, tuple)):
+        kwargs.update({"barriered_from_usergroup_ids": ",".join(barriered_from_usergroup_ids)})
+    else:
+        kwargs.update({"barriered_from_usergroup_ids": barriered_from_usergroup_ids})
+    if isinstance(restricted_subjects, (list, tuple)):
+        kwargs.update({"restricted_subjects": ",".join(restricted_subjects)})
+    else:
+        kwargs.update({"restricted_subjects": restricted_subjects})
+    return self.api_call("admin.barriers.update", http_verb="POST", params=kwargs)
+
+ +
+
+def admin_conversations_archive(self, *, channel_id: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_conversations_archive(
+    self,
+    *,
+    channel_id: str,
+    **kwargs,
+) -> SlackResponse:
+    """Archive a public or private channel.
+    https://docs.slack.dev/reference/methods/admin.conversations.archive
+    """
+    kwargs.update({"channel_id": channel_id})
+    return self.api_call("admin.conversations.archive", params=kwargs)
+
+ +
+
+def admin_conversations_bulkArchive(self, *, channel_ids: str | Sequence[str], **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_conversations_bulkArchive(
+    self,
+    *,
+    channel_ids: Union[Sequence[str], str],
+    **kwargs,
+) -> SlackResponse:
+    """Archive public or private channels in bulk.
+    https://docs.slack.dev/reference/methods/admin.conversations.bulkArchive
+    """
+    kwargs.update({"channel_ids": ",".join(channel_ids) if isinstance(channel_ids, (list, tuple)) else channel_ids})
+    return self.api_call("admin.conversations.bulkArchive", params=kwargs)
+
+ +
+
+def admin_conversations_bulkDelete(self, *, channel_ids: str | Sequence[str], **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_conversations_bulkDelete(
+    self,
+    *,
+    channel_ids: Union[Sequence[str], str],
+    **kwargs,
+) -> SlackResponse:
+    """Delete public or private channels in bulk.
+    https://slack.com/api/admin.conversations.bulkDelete
+    """
+    kwargs.update({"channel_ids": ",".join(channel_ids) if isinstance(channel_ids, (list, tuple)) else channel_ids})
+    return self.api_call("admin.conversations.bulkDelete", params=kwargs)
+
+

Delete public or private channels in bulk. +https://slack.com/api/admin.conversations.bulkDelete

+
+
+def admin_conversations_bulkMove(self, *, channel_ids: str | Sequence[str], target_team_id: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_conversations_bulkMove(
+    self,
+    *,
+    channel_ids: Union[Sequence[str], str],
+    target_team_id: str,
+    **kwargs,
+) -> SlackResponse:
+    """Move public or private channels in bulk.
+    https://docs.slack.dev/reference/methods/admin.conversations.bulkMove
+    """
+    kwargs.update(
+        {
+            "target_team_id": target_team_id,
+            "channel_ids": ",".join(channel_ids) if isinstance(channel_ids, (list, tuple)) else channel_ids,
+        }
+    )
+    return self.api_call("admin.conversations.bulkMove", params=kwargs)
+
+ +
+
+def admin_conversations_convertToPrivate(self, *, channel_id: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_conversations_convertToPrivate(
+    self,
+    *,
+    channel_id: str,
+    **kwargs,
+) -> SlackResponse:
+    """Convert a public channel to a private channel.
+    https://docs.slack.dev/reference/methods/admin.conversations.convertToPrivate
+    """
+    kwargs.update({"channel_id": channel_id})
+    return self.api_call("admin.conversations.convertToPrivate", params=kwargs)
+
+ +
+
+def admin_conversations_convertToPublic(self, *, channel_id: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_conversations_convertToPublic(
+    self,
+    *,
+    channel_id: str,
+    **kwargs,
+) -> SlackResponse:
+    """Convert a privte channel to a public channel.
+    https://docs.slack.dev/reference/methods/admin.conversations.convertToPublic
+    """
+    kwargs.update({"channel_id": channel_id})
+    return self.api_call("admin.conversations.convertToPublic", params=kwargs)
+
+ +
+
+def admin_conversations_create(self,
*,
is_private: bool,
name: str,
description: str | None = None,
org_wide: bool | None = None,
team_id: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_conversations_create(
+    self,
+    *,
+    is_private: bool,
+    name: str,
+    description: Optional[str] = None,
+    org_wide: Optional[bool] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Create a public or private channel-based conversation.
+    https://docs.slack.dev/reference/methods/admin.conversations.create
+    """
+    kwargs.update(
+        {
+            "is_private": is_private,
+            "name": name,
+            "description": description,
+            "org_wide": org_wide,
+            "team_id": team_id,
+        }
+    )
+    return self.api_call("admin.conversations.create", params=kwargs)
+
+

Create a public or private channel-based conversation. +https://docs.slack.dev/reference/methods/admin.conversations.create

+
+
+def admin_conversations_createForObjects(self,
*,
object_id: str,
salesforce_org_id: str,
invite_object_team: bool | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_conversations_createForObjects(
+    self,
+    *,
+    object_id: str,
+    salesforce_org_id: str,
+    invite_object_team: Optional[bool] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Create a Salesforce channel for the corresponding object provided.
+    https://docs.slack.dev/reference/methods/admin.conversations.createForObjects
+    """
+    kwargs.update(
+        {"object_id": object_id, "salesforce_org_id": salesforce_org_id, "invite_object_team": invite_object_team}
+    )
+    return self.api_call("admin.conversations.createForObjects", params=kwargs)
+
+

Create a Salesforce channel for the corresponding object provided. +https://docs.slack.dev/reference/methods/admin.conversations.createForObjects

+
+
+def admin_conversations_delete(self, *, channel_id: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_conversations_delete(
+    self,
+    *,
+    channel_id: str,
+    **kwargs,
+) -> SlackResponse:
+    """Delete a public or private channel.
+    https://docs.slack.dev/reference/methods/admin.conversations.delete
+    """
+    kwargs.update({"channel_id": channel_id})
+    return self.api_call("admin.conversations.delete", params=kwargs)
+
+ +
+
+def admin_conversations_disconnectShared(self,
*,
channel_id: str,
leaving_team_ids: str | Sequence[str] | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_conversations_disconnectShared(
+    self,
+    *,
+    channel_id: str,
+    leaving_team_ids: Optional[Union[str, Sequence[str]]] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Disconnect a connected channel from one or more workspaces.
+    https://docs.slack.dev/reference/methods/admin.conversations.disconnectShared
+    """
+    kwargs.update({"channel_id": channel_id})
+    if isinstance(leaving_team_ids, (list, tuple)):
+        kwargs.update({"leaving_team_ids": ",".join(leaving_team_ids)})
+    else:
+        kwargs.update({"leaving_team_ids": leaving_team_ids})
+    return self.api_call("admin.conversations.disconnectShared", params=kwargs)
+
+

Disconnect a connected channel from one or more workspaces. +https://docs.slack.dev/reference/methods/admin.conversations.disconnectShared

+
+
+def admin_conversations_ekm_listOriginalConnectedChannelInfo(self,
*,
channel_ids: str | Sequence[str] | None = None,
cursor: str | None = None,
limit: int | None = None,
team_ids: str | Sequence[str] | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_conversations_ekm_listOriginalConnectedChannelInfo(
+    self,
+    *,
+    channel_ids: Optional[Union[str, Sequence[str]]] = None,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    team_ids: Optional[Union[str, Sequence[str]]] = None,
+    **kwargs,
+) -> SlackResponse:
+    """List all disconnected channels—i.e.,
+    channels that were once connected to other workspaces and then disconnected—and
+    the corresponding original channel IDs for key revocation with EKM.
+    https://docs.slack.dev/reference/methods/admin.conversations.ekm.listOriginalConnectedChannelInfo
+    """
+    kwargs.update(
+        {
+            "cursor": cursor,
+            "limit": limit,
+        }
+    )
+    if isinstance(channel_ids, (list, tuple)):
+        kwargs.update({"channel_ids": ",".join(channel_ids)})
+    else:
+        kwargs.update({"channel_ids": channel_ids})
+    if isinstance(team_ids, (list, tuple)):
+        kwargs.update({"team_ids": ",".join(team_ids)})
+    else:
+        kwargs.update({"team_ids": team_ids})
+    return self.api_call("admin.conversations.ekm.listOriginalConnectedChannelInfo", params=kwargs)
+
+

List all disconnected channels—i.e., +channels that were once connected to other workspaces and then disconnected—and +the corresponding original channel IDs for key revocation with EKM. +https://docs.slack.dev/reference/methods/admin.conversations.ekm.listOriginalConnectedChannelInfo

+
+
+def admin_conversations_getConversationPrefs(self, *, channel_id: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_conversations_getConversationPrefs(
+    self,
+    *,
+    channel_id: str,
+    **kwargs,
+) -> SlackResponse:
+    """Get conversation preferences for a public or private channel.
+    https://docs.slack.dev/reference/methods/admin.conversations.getConversationPrefs
+    """
+    kwargs.update({"channel_id": channel_id})
+    return self.api_call("admin.conversations.getConversationPrefs", params=kwargs)
+
+

Get conversation preferences for a public or private channel. +https://docs.slack.dev/reference/methods/admin.conversations.getConversationPrefs

+
+
+def admin_conversations_getCustomRetention(self, *, channel_id: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_conversations_getCustomRetention(
+    self,
+    *,
+    channel_id: str,
+    **kwargs,
+) -> SlackResponse:
+    """Get a channel's retention policy
+    https://docs.slack.dev/reference/methods/admin.conversations.getCustomRetention
+    """
+    kwargs.update({"channel_id": channel_id})
+    return self.api_call("admin.conversations.getCustomRetention", params=kwargs)
+
+ +
+
+def admin_conversations_getTeams(self,
*,
channel_id: str,
cursor: str | None = None,
limit: int | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_conversations_getTeams(
+    self,
+    *,
+    channel_id: str,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Set the workspaces in an Enterprise grid org that connect to a channel.
+    https://docs.slack.dev/reference/methods/admin.conversations.getTeams
+    """
+    kwargs.update(
+        {
+            "channel_id": channel_id,
+            "cursor": cursor,
+            "limit": limit,
+        }
+    )
+    return self.api_call("admin.conversations.getTeams", params=kwargs)
+
+

Set the workspaces in an Enterprise grid org that connect to a channel. +https://docs.slack.dev/reference/methods/admin.conversations.getTeams

+
+
+def admin_conversations_invite(self, *, channel_id: str, user_ids: str | Sequence[str], **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_conversations_invite(
+    self,
+    *,
+    channel_id: str,
+    user_ids: Union[str, Sequence[str]],
+    **kwargs,
+) -> SlackResponse:
+    """Invite a user to a public or private channel.
+    https://docs.slack.dev/reference/methods/admin.conversations.invite
+    """
+    kwargs.update({"channel_id": channel_id})
+    if isinstance(user_ids, (list, tuple)):
+        kwargs.update({"user_ids": ",".join(user_ids)})
+    else:
+        kwargs.update({"user_ids": user_ids})
+    # NOTE: the endpoint is unable to handle Content-Type: application/json as of Sep 3, 2020.
+    return self.api_call("admin.conversations.invite", params=kwargs)
+
+

Invite a user to a public or private channel. +https://docs.slack.dev/reference/methods/admin.conversations.invite

+
+
+def admin_conversations_linkObjects(self, *, channel: str, record_id: str, salesforce_org_id: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_conversations_linkObjects(
+    self,
+    *,
+    channel: str,
+    record_id: str,
+    salesforce_org_id: str,
+    **kwargs,
+) -> SlackResponse:
+    """Link a Salesforce record to a channel.
+    https://docs.slack.dev/reference/methods/admin.conversations.linkObjects
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "record_id": record_id,
+            "salesforce_org_id": salesforce_org_id,
+        }
+    )
+    return self.api_call("admin.conversations.linkObjects", params=kwargs)
+
+ +
+
+def admin_conversations_lookup(self,
*,
last_message_activity_before: int,
team_ids: str | Sequence[str],
cursor: str | None = None,
limit: int | None = None,
max_member_count: int | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_conversations_lookup(
+    self,
+    *,
+    last_message_activity_before: int,
+    team_ids: Union[str, Sequence[str]],
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    max_member_count: Optional[int] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Returns channels on the given team using the filters.
+    https://docs.slack.dev/reference/methods/admin.conversations.lookup
+    """
+    kwargs.update(
+        {
+            "last_message_activity_before": last_message_activity_before,
+            "cursor": cursor,
+            "limit": limit,
+            "max_member_count": max_member_count,
+        }
+    )
+    if isinstance(team_ids, (list, tuple)):
+        kwargs.update({"team_ids": ",".join(team_ids)})
+    else:
+        kwargs.update({"team_ids": team_ids})
+    return self.api_call("admin.conversations.lookup", params=kwargs)
+
+

Returns channels on the given team using the filters. +https://docs.slack.dev/reference/methods/admin.conversations.lookup

+
+
+def admin_conversations_removeCustomRetention(self, *, channel_id: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_conversations_removeCustomRetention(
+    self,
+    *,
+    channel_id: str,
+    **kwargs,
+) -> SlackResponse:
+    """Remove a channel's retention policy
+    https://docs.slack.dev/reference/methods/admin.conversations.removeCustomRetention
+    """
+    kwargs.update({"channel_id": channel_id})
+    return self.api_call("admin.conversations.removeCustomRetention", params=kwargs)
+
+ +
+
+def admin_conversations_rename(self, *, channel_id: str, name: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_conversations_rename(
+    self,
+    *,
+    channel_id: str,
+    name: str,
+    **kwargs,
+) -> SlackResponse:
+    """Rename a public or private channel.
+    https://docs.slack.dev/reference/methods/admin.conversations.rename
+    """
+    kwargs.update({"channel_id": channel_id, "name": name})
+    return self.api_call("admin.conversations.rename", params=kwargs)
+
+ +
+
+def admin_conversations_restrictAccess_addGroup(self, *, channel_id: str, group_id: str, team_id: str | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_conversations_restrictAccess_addGroup(
+    self,
+    *,
+    channel_id: str,
+    group_id: str,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Add an allowlist of IDP groups for accessing a channel.
+    https://docs.slack.dev/reference/methods/admin.conversations.restrictAccess.addGroup
+    """
+    kwargs.update(
+        {
+            "channel_id": channel_id,
+            "group_id": group_id,
+            "team_id": team_id,
+        }
+    )
+    return self.api_call(
+        "admin.conversations.restrictAccess.addGroup",
+        http_verb="GET",
+        params=kwargs,
+    )
+
+

Add an allowlist of IDP groups for accessing a channel. +https://docs.slack.dev/reference/methods/admin.conversations.restrictAccess.addGroup

+
+
+def admin_conversations_restrictAccess_listGroups(self, *, channel_id: str, team_id: str | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_conversations_restrictAccess_listGroups(
+    self,
+    *,
+    channel_id: str,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """List all IDP Groups linked to a channel.
+    https://docs.slack.dev/reference/methods/admin.conversations.restrictAccess.listGroups
+    """
+    kwargs.update(
+        {
+            "channel_id": channel_id,
+            "team_id": team_id,
+        }
+    )
+    return self.api_call(
+        "admin.conversations.restrictAccess.listGroups",
+        http_verb="GET",
+        params=kwargs,
+    )
+
+ +
+
+def admin_conversations_restrictAccess_removeGroup(self, *, channel_id: str, group_id: str, team_id: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_conversations_restrictAccess_removeGroup(
+    self,
+    *,
+    channel_id: str,
+    group_id: str,
+    team_id: str,
+    **kwargs,
+) -> SlackResponse:
+    """Remove a linked IDP group linked from a private channel.
+    https://docs.slack.dev/reference/methods/admin.conversations.restrictAccess.removeGroup
+    """
+    kwargs.update(
+        {
+            "channel_id": channel_id,
+            "group_id": group_id,
+            "team_id": team_id,
+        }
+    )
+    return self.api_call(
+        "admin.conversations.restrictAccess.removeGroup",
+        http_verb="GET",
+        params=kwargs,
+    )
+
+

Remove a linked IDP group linked from a private channel. +https://docs.slack.dev/reference/methods/admin.conversations.restrictAccess.removeGroup

+
+ +
+
+ +Expand source code + +
def admin_conversations_search(
+    self,
+    *,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    query: Optional[str] = None,
+    search_channel_types: Optional[Union[str, Sequence[str]]] = None,
+    sort: Optional[str] = None,
+    sort_dir: Optional[str] = None,
+    team_ids: Optional[Union[str, Sequence[str]]] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Search for public or private channels in an Enterprise organization.
+    https://docs.slack.dev/reference/methods/admin.conversations.search
+    """
+    kwargs.update(
+        {
+            "cursor": cursor,
+            "limit": limit,
+            "query": query,
+            "sort": sort,
+            "sort_dir": sort_dir,
+        }
+    )
+
+    if isinstance(search_channel_types, (list, tuple)):
+        kwargs.update({"search_channel_types": ",".join(search_channel_types)})
+    else:
+        kwargs.update({"search_channel_types": search_channel_types})
+
+    if isinstance(team_ids, (list, tuple)):
+        kwargs.update({"team_ids": ",".join(team_ids)})
+    else:
+        kwargs.update({"team_ids": team_ids})
+
+    return self.api_call("admin.conversations.search", params=kwargs)
+
+

Search for public or private channels in an Enterprise organization. +https://docs.slack.dev/reference/methods/admin.conversations.search

+
+
+def admin_conversations_setConversationPrefs(self, *, channel_id: str, prefs: str | Dict[str, str], **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_conversations_setConversationPrefs(
+    self,
+    *,
+    channel_id: str,
+    prefs: Union[str, Dict[str, str]],
+    **kwargs,
+) -> SlackResponse:
+    """Set the posting permissions for a public or private channel.
+    https://docs.slack.dev/reference/methods/admin.conversations.setConversationPrefs
+    """
+    kwargs.update({"channel_id": channel_id})
+    if isinstance(prefs, dict):
+        kwargs.update({"prefs": json.dumps(prefs)})
+    else:
+        kwargs.update({"prefs": prefs})
+    return self.api_call("admin.conversations.setConversationPrefs", params=kwargs)
+
+

Set the posting permissions for a public or private channel. +https://docs.slack.dev/reference/methods/admin.conversations.setConversationPrefs

+
+
+def admin_conversations_setCustomRetention(self, *, channel_id: str, duration_days: int, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_conversations_setCustomRetention(
+    self,
+    *,
+    channel_id: str,
+    duration_days: int,
+    **kwargs,
+) -> SlackResponse:
+    """Set a channel's retention policy
+    https://docs.slack.dev/reference/methods/admin.conversations.setCustomRetention
+    """
+    kwargs.update({"channel_id": channel_id, "duration_days": duration_days})
+    return self.api_call("admin.conversations.setCustomRetention", params=kwargs)
+
+ +
+
+def admin_conversations_setTeams(self,
*,
channel_id: str,
org_channel: bool | None = None,
target_team_ids: str | Sequence[str] | None = None,
team_id: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_conversations_setTeams(
+    self,
+    *,
+    channel_id: str,
+    org_channel: Optional[bool] = None,
+    target_team_ids: Optional[Union[str, Sequence[str]]] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Set the workspaces in an Enterprise grid org that connect to a public or private channel.
+    https://docs.slack.dev/reference/methods/admin.conversations.setTeams
+    """
+    kwargs.update(
+        {
+            "channel_id": channel_id,
+            "org_channel": org_channel,
+            "team_id": team_id,
+        }
+    )
+    if isinstance(target_team_ids, (list, tuple)):
+        kwargs.update({"target_team_ids": ",".join(target_team_ids)})
+    else:
+        kwargs.update({"target_team_ids": target_team_ids})
+    return self.api_call("admin.conversations.setTeams", params=kwargs)
+
+

Set the workspaces in an Enterprise grid org that connect to a public or private channel. +https://docs.slack.dev/reference/methods/admin.conversations.setTeams

+
+
+def admin_conversations_unarchive(self, *, channel_id: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_conversations_unarchive(
+    self,
+    *,
+    channel_id: str,
+    **kwargs,
+) -> SlackResponse:
+    """Unarchive a public or private channel.
+    https://docs.slack.dev/reference/methods/admin.conversations.archive
+    """
+    kwargs.update({"channel_id": channel_id})
+    return self.api_call("admin.conversations.unarchive", params=kwargs)
+
+ +
+
+def admin_conversations_unlinkObjects(self, *, channel: str, new_name: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_conversations_unlinkObjects(
+    self,
+    *,
+    channel: str,
+    new_name: str,
+    **kwargs,
+) -> SlackResponse:
+    """Unlink a Salesforce record from a channel.
+    https://docs.slack.dev/reference/methods/admin.conversations.unlinkObjects
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "new_name": new_name,
+        }
+    )
+    return self.api_call("admin.conversations.unlinkObjects", params=kwargs)
+
+ +
+
+def admin_emoji_add(self, *, name: str, url: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_emoji_add(
+    self,
+    *,
+    name: str,
+    url: str,
+    **kwargs,
+) -> SlackResponse:
+    """Add an emoji.
+    https://docs.slack.dev/reference/methods/admin.emoji.add
+    """
+    kwargs.update({"name": name, "url": url})
+    return self.api_call("admin.emoji.add", http_verb="GET", params=kwargs)
+
+ +
+
+def admin_emoji_addAlias(self, *, alias_for: str, name: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_emoji_addAlias(
+    self,
+    *,
+    alias_for: str,
+    name: str,
+    **kwargs,
+) -> SlackResponse:
+    """Add an emoji alias.
+    https://docs.slack.dev/reference/methods/admin.emoji.addAlias
+    """
+    kwargs.update({"alias_for": alias_for, "name": name})
+    return self.api_call("admin.emoji.addAlias", http_verb="GET", params=kwargs)
+
+ +
+
+def admin_emoji_list(self, *, cursor: str | None = None, limit: int | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_emoji_list(
+    self,
+    *,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    **kwargs,
+) -> SlackResponse:
+    """List emoji for an Enterprise Grid organization.
+    https://docs.slack.dev/reference/methods/admin.emoji.list
+    """
+    kwargs.update({"cursor": cursor, "limit": limit})
+    return self.api_call("admin.emoji.list", http_verb="GET", params=kwargs)
+
+

List emoji for an Enterprise Grid organization. +https://docs.slack.dev/reference/methods/admin.emoji.list

+
+
+def admin_emoji_remove(self, *, name: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_emoji_remove(
+    self,
+    *,
+    name: str,
+    **kwargs,
+) -> SlackResponse:
+    """Remove an emoji across an Enterprise Grid organization.
+    https://docs.slack.dev/reference/methods/admin.emoji.remove
+    """
+    kwargs.update({"name": name})
+    return self.api_call("admin.emoji.remove", http_verb="GET", params=kwargs)
+
+

Remove an emoji across an Enterprise Grid organization. +https://docs.slack.dev/reference/methods/admin.emoji.remove

+
+
+def admin_emoji_rename(self, *, name: str, new_name: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_emoji_rename(
+    self,
+    *,
+    name: str,
+    new_name: str,
+    **kwargs,
+) -> SlackResponse:
+    """Rename an emoji.
+    https://docs.slack.dev/reference/methods/admin.emoji.rename
+    """
+    kwargs.update({"name": name, "new_name": new_name})
+    return self.api_call("admin.emoji.rename", http_verb="GET", params=kwargs)
+
+ +
+
+def admin_functions_list(self,
*,
app_ids: str | Sequence[str],
team_id: str | None = None,
cursor: str | None = None,
limit: int | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_functions_list(
+    self,
+    *,
+    app_ids: Union[str, Sequence[str]],
+    team_id: Optional[str] = None,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Look up functions by a set of apps
+    https://docs.slack.dev/reference/methods/admin.functions.list
+    """
+    if isinstance(app_ids, (list, tuple)):
+        kwargs.update({"app_ids": ",".join(app_ids)})
+    else:
+        kwargs.update({"app_ids": app_ids})
+    kwargs.update(
+        {
+            "team_id": team_id,
+            "cursor": cursor,
+            "limit": limit,
+        }
+    )
+    return self.api_call("admin.functions.list", params=kwargs)
+
+ +
+
+def admin_functions_permissions_lookup(self, *, function_ids: str | Sequence[str], **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_functions_permissions_lookup(
+    self,
+    *,
+    function_ids: Union[str, Sequence[str]],
+    **kwargs,
+) -> SlackResponse:
+    """Lookup the visibility of multiple Slack functions
+    and include the users if it is limited to particular named entities.
+    https://docs.slack.dev/reference/methods/admin.functions.permissions.lookup
+    """
+    if isinstance(function_ids, (list, tuple)):
+        kwargs.update({"function_ids": ",".join(function_ids)})
+    else:
+        kwargs.update({"function_ids": function_ids})
+    return self.api_call("admin.functions.permissions.lookup", params=kwargs)
+
+

Lookup the visibility of multiple Slack functions +and include the users if it is limited to particular named entities. +https://docs.slack.dev/reference/methods/admin.functions.permissions.lookup

+
+
+def admin_functions_permissions_set(self,
*,
function_id: str,
visibility: str,
user_ids: str | Sequence[str] | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_functions_permissions_set(
+    self,
+    *,
+    function_id: str,
+    visibility: str,
+    user_ids: Optional[Union[str, Sequence[str]]] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Set the visibility of a Slack function
+    and define the users or workspaces if it is set to named_entities
+    https://docs.slack.dev/reference/methods/admin.functions.permissions.set
+    """
+    kwargs.update(
+        {
+            "function_id": function_id,
+            "visibility": visibility,
+        }
+    )
+    if user_ids is not None:
+        if isinstance(user_ids, (list, tuple)):
+            kwargs.update({"user_ids": ",".join(user_ids)})
+        else:
+            kwargs.update({"user_ids": user_ids})
+    return self.api_call("admin.functions.permissions.set", params=kwargs)
+
+

Set the visibility of a Slack function +and define the users or workspaces if it is set to named_entities +https://docs.slack.dev/reference/methods/admin.functions.permissions.set

+
+
+def admin_inviteRequests_approve(self, *, invite_request_id: str, team_id: str | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_inviteRequests_approve(
+    self,
+    *,
+    invite_request_id: str,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Approve a workspace invite request.
+    https://docs.slack.dev/reference/methods/admin.inviteRequests.approve
+    """
+    kwargs.update({"invite_request_id": invite_request_id, "team_id": team_id})
+    return self.api_call("admin.inviteRequests.approve", params=kwargs)
+
+ +
+
+def admin_inviteRequests_approved_list(self,
*,
cursor: str | None = None,
limit: int | None = None,
team_id: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_inviteRequests_approved_list(
+    self,
+    *,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """List all approved workspace invite requests.
+    https://docs.slack.dev/reference/methods/admin.inviteRequests.approved.list
+    """
+    kwargs.update(
+        {
+            "cursor": cursor,
+            "limit": limit,
+            "team_id": team_id,
+        }
+    )
+    return self.api_call("admin.inviteRequests.approved.list", params=kwargs)
+
+ +
+
+def admin_inviteRequests_denied_list(self,
*,
cursor: str | None = None,
limit: int | None = None,
team_id: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_inviteRequests_denied_list(
+    self,
+    *,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """List all denied workspace invite requests.
+    https://docs.slack.dev/reference/methods/admin.inviteRequests.denied.list
+    """
+    kwargs.update(
+        {
+            "cursor": cursor,
+            "limit": limit,
+            "team_id": team_id,
+        }
+    )
+    return self.api_call("admin.inviteRequests.denied.list", params=kwargs)
+
+ +
+
+def admin_inviteRequests_deny(self, *, invite_request_id: str, team_id: str | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_inviteRequests_deny(
+    self,
+    *,
+    invite_request_id: str,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Deny a workspace invite request.
+    https://docs.slack.dev/reference/methods/admin.inviteRequests.deny
+    """
+    kwargs.update({"invite_request_id": invite_request_id, "team_id": team_id})
+    return self.api_call("admin.inviteRequests.deny", params=kwargs)
+
+ +
+
+def admin_inviteRequests_list(self, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_inviteRequests_list(
+    self,
+    **kwargs,
+) -> SlackResponse:
+    """List all pending workspace invite requests."""
+    return self.api_call("admin.inviteRequests.list", params=kwargs)
+
+

List all pending workspace invite requests.

+
+
+def admin_roles_addAssignments(self,
*,
role_id: str,
entity_ids: str | Sequence[str],
user_ids: str | Sequence[str],
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_roles_addAssignments(
+    self,
+    *,
+    role_id: str,
+    entity_ids: Union[str, Sequence[str]],
+    user_ids: Union[str, Sequence[str]],
+    **kwargs,
+) -> SlackResponse:
+    """Adds members to the specified role with the specified scopes
+    https://docs.slack.dev/reference/methods/admin.roles.addAssignments
+    """
+    kwargs.update({"role_id": role_id})
+    if isinstance(entity_ids, (list, tuple)):
+        kwargs.update({"entity_ids": ",".join(entity_ids)})
+    else:
+        kwargs.update({"entity_ids": entity_ids})
+    if isinstance(user_ids, (list, tuple)):
+        kwargs.update({"user_ids": ",".join(user_ids)})
+    else:
+        kwargs.update({"user_ids": user_ids})
+    return self.api_call("admin.roles.addAssignments", params=kwargs)
+
+

Adds members to the specified role with the specified scopes +https://docs.slack.dev/reference/methods/admin.roles.addAssignments

+
+
+def admin_roles_listAssignments(self,
*,
role_ids: str | Sequence[str] | None = None,
entity_ids: str | Sequence[str] | None = None,
cursor: str | None = None,
limit: str | int | None = None,
sort_dir: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_roles_listAssignments(
+    self,
+    *,
+    role_ids: Optional[Union[str, Sequence[str]]] = None,
+    entity_ids: Optional[Union[str, Sequence[str]]] = None,
+    cursor: Optional[str] = None,
+    limit: Optional[Union[str, int]] = None,
+    sort_dir: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Lists assignments for all roles across entities.
+        Options to scope results by any combination of roles or entities
+    https://docs.slack.dev/reference/methods/admin.roles.listAssignments
+    """
+    kwargs.update({"cursor": cursor, "limit": limit, "sort_dir": sort_dir})
+    if isinstance(entity_ids, (list, tuple)):
+        kwargs.update({"entity_ids": ",".join(entity_ids)})
+    else:
+        kwargs.update({"entity_ids": entity_ids})
+    if isinstance(role_ids, (list, tuple)):
+        kwargs.update({"role_ids": ",".join(role_ids)})
+    else:
+        kwargs.update({"role_ids": role_ids})
+    return self.api_call("admin.roles.listAssignments", params=kwargs)
+
+

Lists assignments for all roles across entities. +Options to scope results by any combination of roles or entities +https://docs.slack.dev/reference/methods/admin.roles.listAssignments

+
+
+def admin_roles_removeAssignments(self,
*,
role_id: str,
entity_ids: str | Sequence[str],
user_ids: str | Sequence[str],
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_roles_removeAssignments(
+    self,
+    *,
+    role_id: str,
+    entity_ids: Union[str, Sequence[str]],
+    user_ids: Union[str, Sequence[str]],
+    **kwargs,
+) -> SlackResponse:
+    """Removes a set of users from a role for the given scopes and entities
+    https://docs.slack.dev/reference/methods/admin.roles.removeAssignments
+    """
+    kwargs.update({"role_id": role_id})
+    if isinstance(entity_ids, (list, tuple)):
+        kwargs.update({"entity_ids": ",".join(entity_ids)})
+    else:
+        kwargs.update({"entity_ids": entity_ids})
+    if isinstance(user_ids, (list, tuple)):
+        kwargs.update({"user_ids": ",".join(user_ids)})
+    else:
+        kwargs.update({"user_ids": user_ids})
+    return self.api_call("admin.roles.removeAssignments", params=kwargs)
+
+

Removes a set of users from a role for the given scopes and entities +https://docs.slack.dev/reference/methods/admin.roles.removeAssignments

+
+
+def admin_teams_admins_list(self, *, team_id: str, cursor: str | None = None, limit: int | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_teams_admins_list(
+    self,
+    *,
+    team_id: str,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    **kwargs,
+) -> SlackResponse:
+    """List all of the admins on a given workspace.
+    https://docs.slack.dev/reference/methods/admin.inviteRequests.list
+    """
+    kwargs.update(
+        {
+            "cursor": cursor,
+            "limit": limit,
+            "team_id": team_id,
+        }
+    )
+    return self.api_call("admin.teams.admins.list", http_verb="GET", params=kwargs)
+
+

List all of the admins on a given workspace. +https://docs.slack.dev/reference/methods/admin.inviteRequests.list

+
+
+def admin_teams_create(self,
*,
team_domain: str,
team_name: str,
team_description: str | None = None,
team_discoverability: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_teams_create(
+    self,
+    *,
+    team_domain: str,
+    team_name: str,
+    team_description: Optional[str] = None,
+    team_discoverability: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Create an Enterprise team.
+    https://docs.slack.dev/reference/methods/admin.teams.create
+    """
+    kwargs.update(
+        {
+            "team_domain": team_domain,
+            "team_name": team_name,
+            "team_description": team_description,
+            "team_discoverability": team_discoverability,
+        }
+    )
+    return self.api_call("admin.teams.create", params=kwargs)
+
+ +
+
+def admin_teams_list(self, *, cursor: str | None = None, limit: int | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_teams_list(
+    self,
+    *,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    **kwargs,
+) -> SlackResponse:
+    """List all teams on an Enterprise organization.
+    https://docs.slack.dev/reference/methods/admin.teams.list
+    """
+    kwargs.update({"cursor": cursor, "limit": limit})
+    return self.api_call("admin.teams.list", params=kwargs)
+
+

List all teams on an Enterprise organization. +https://docs.slack.dev/reference/methods/admin.teams.list

+
+
+def admin_teams_owners_list(self, *, team_id: str, cursor: str | None = None, limit: int | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_teams_owners_list(
+    self,
+    *,
+    team_id: str,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    **kwargs,
+) -> SlackResponse:
+    """List all of the admins on a given workspace.
+    https://docs.slack.dev/reference/methods/admin.teams.owners.list
+    """
+    kwargs.update({"team_id": team_id, "cursor": cursor, "limit": limit})
+    return self.api_call("admin.teams.owners.list", http_verb="GET", params=kwargs)
+
+

List all of the admins on a given workspace. +https://docs.slack.dev/reference/methods/admin.teams.owners.list

+
+
+def admin_teams_settings_info(self, *, team_id: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_teams_settings_info(
+    self,
+    *,
+    team_id: str,
+    **kwargs,
+) -> SlackResponse:
+    """Fetch information about settings in a workspace
+    https://docs.slack.dev/reference/methods/admin.teams.settings.info
+    """
+    kwargs.update({"team_id": team_id})
+    return self.api_call("admin.teams.settings.info", params=kwargs)
+
+

Fetch information about settings in a workspace +https://docs.slack.dev/reference/methods/admin.teams.settings.info

+
+
+def admin_teams_settings_setDefaultChannels(self, *, team_id: str, channel_ids: str | Sequence[str], **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_teams_settings_setDefaultChannels(
+    self,
+    *,
+    team_id: str,
+    channel_ids: Union[str, Sequence[str]],
+    **kwargs,
+) -> SlackResponse:
+    """Set the default channels of a workspace.
+    https://docs.slack.dev/reference/methods/admin.teams.settings.setDefaultChannels
+    """
+    kwargs.update({"team_id": team_id})
+    if isinstance(channel_ids, (list, tuple)):
+        kwargs.update({"channel_ids": ",".join(channel_ids)})
+    else:
+        kwargs.update({"channel_ids": channel_ids})
+    return self.api_call("admin.teams.settings.setDefaultChannels", http_verb="GET", params=kwargs)
+
+ +
+
+def admin_teams_settings_setDescription(self, *, team_id: str, description: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_teams_settings_setDescription(
+    self,
+    *,
+    team_id: str,
+    description: str,
+    **kwargs,
+) -> SlackResponse:
+    """Set the description of a given workspace.
+    https://docs.slack.dev/reference/methods/admin.teams.settings.setDescription
+    """
+    kwargs.update({"team_id": team_id, "description": description})
+    return self.api_call("admin.teams.settings.setDescription", params=kwargs)
+
+ +
+
+def admin_teams_settings_setDiscoverability(self, *, team_id: str, discoverability: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_teams_settings_setDiscoverability(
+    self,
+    *,
+    team_id: str,
+    discoverability: str,
+    **kwargs,
+) -> SlackResponse:
+    """Sets the icon of a workspace.
+    https://docs.slack.dev/reference/methods/admin.teams.settings.setDiscoverability
+    """
+    kwargs.update({"team_id": team_id, "discoverability": discoverability})
+    return self.api_call("admin.teams.settings.setDiscoverability", params=kwargs)
+
+ +
+
+def admin_teams_settings_setIcon(self, *, team_id: str, image_url: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_teams_settings_setIcon(
+    self,
+    *,
+    team_id: str,
+    image_url: str,
+    **kwargs,
+) -> SlackResponse:
+    """Sets the icon of a workspace.
+    https://docs.slack.dev/reference/methods/admin.teams.settings.setIcon
+    """
+    kwargs.update({"team_id": team_id, "image_url": image_url})
+    return self.api_call("admin.teams.settings.setIcon", http_verb="GET", params=kwargs)
+
+ +
+
+def admin_teams_settings_setName(self, *, team_id: str, name: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_teams_settings_setName(
+    self,
+    *,
+    team_id: str,
+    name: str,
+    **kwargs,
+) -> SlackResponse:
+    """Sets the icon of a workspace.
+    https://docs.slack.dev/reference/methods/admin.teams.settings.setName
+    """
+    kwargs.update({"team_id": team_id, "name": name})
+    return self.api_call("admin.teams.settings.setName", params=kwargs)
+
+ +
+
+def admin_usergroups_addChannels(self,
*,
channel_ids: str | Sequence[str],
usergroup_id: str,
team_id: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_usergroups_addChannels(
+    self,
+    *,
+    channel_ids: Union[str, Sequence[str]],
+    usergroup_id: str,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Add one or more default channels to an IDP group.
+    https://docs.slack.dev/reference/methods/admin.usergroups.addChannels
+    """
+    kwargs.update({"team_id": team_id, "usergroup_id": usergroup_id})
+    if isinstance(channel_ids, (list, tuple)):
+        kwargs.update({"channel_ids": ",".join(channel_ids)})
+    else:
+        kwargs.update({"channel_ids": channel_ids})
+    return self.api_call("admin.usergroups.addChannels", params=kwargs)
+
+

Add one or more default channels to an IDP group. +https://docs.slack.dev/reference/methods/admin.usergroups.addChannels

+
+
+def admin_usergroups_addTeams(self,
*,
usergroup_id: str,
team_ids: str | Sequence[str],
auto_provision: bool | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_usergroups_addTeams(
+    self,
+    *,
+    usergroup_id: str,
+    team_ids: Union[str, Sequence[str]],
+    auto_provision: Optional[bool] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Associate one or more default workspaces with an organization-wide IDP group.
+    https://docs.slack.dev/reference/methods/admin.usergroups.addTeams
+    """
+    kwargs.update({"usergroup_id": usergroup_id, "auto_provision": auto_provision})
+    if isinstance(team_ids, (list, tuple)):
+        kwargs.update({"team_ids": ",".join(team_ids)})
+    else:
+        kwargs.update({"team_ids": team_ids})
+    return self.api_call("admin.usergroups.addTeams", params=kwargs)
+
+

Associate one or more default workspaces with an organization-wide IDP group. +https://docs.slack.dev/reference/methods/admin.usergroups.addTeams

+
+
+def admin_usergroups_listChannels(self,
*,
usergroup_id: str,
include_num_members: bool | None = None,
team_id: bool | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_usergroups_listChannels(
+    self,
+    *,
+    usergroup_id: str,
+    include_num_members: Optional[bool] = None,
+    team_id: Optional[bool] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Add one or more default channels to an IDP group.
+    https://docs.slack.dev/reference/methods/admin.usergroups.listChannels
+    """
+    kwargs.update(
+        {
+            "usergroup_id": usergroup_id,
+            "include_num_members": include_num_members,
+            "team_id": team_id,
+        }
+    )
+    return self.api_call("admin.usergroups.listChannels", params=kwargs)
+
+

Add one or more default channels to an IDP group. +https://docs.slack.dev/reference/methods/admin.usergroups.listChannels

+
+
+def admin_usergroups_removeChannels(self, *, usergroup_id: str, channel_ids: str | Sequence[str], **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_usergroups_removeChannels(
+    self,
+    *,
+    usergroup_id: str,
+    channel_ids: Union[str, Sequence[str]],
+    **kwargs,
+) -> SlackResponse:
+    """Add one or more default channels to an IDP group.
+    https://docs.slack.dev/reference/methods/admin.usergroups.removeChannels
+    """
+    kwargs.update({"usergroup_id": usergroup_id})
+    if isinstance(channel_ids, (list, tuple)):
+        kwargs.update({"channel_ids": ",".join(channel_ids)})
+    else:
+        kwargs.update({"channel_ids": channel_ids})
+    return self.api_call("admin.usergroups.removeChannels", params=kwargs)
+
+

Add one or more default channels to an IDP group. +https://docs.slack.dev/reference/methods/admin.usergroups.removeChannels

+
+
+def admin_users_assign(self,
*,
team_id: str,
user_id: str,
channel_ids: str | Sequence[str] | None = None,
is_restricted: bool | None = None,
is_ultra_restricted: bool | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_users_assign(
+    self,
+    *,
+    team_id: str,
+    user_id: str,
+    channel_ids: Optional[Union[str, Sequence[str]]] = None,
+    is_restricted: Optional[bool] = None,
+    is_ultra_restricted: Optional[bool] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Add an Enterprise user to a workspace.
+    https://docs.slack.dev/reference/methods/admin.users.assign
+    """
+    kwargs.update(
+        {
+            "team_id": team_id,
+            "user_id": user_id,
+            "is_restricted": is_restricted,
+            "is_ultra_restricted": is_ultra_restricted,
+        }
+    )
+    if isinstance(channel_ids, (list, tuple)):
+        kwargs.update({"channel_ids": ",".join(channel_ids)})
+    else:
+        kwargs.update({"channel_ids": channel_ids})
+    return self.api_call("admin.users.assign", params=kwargs)
+
+

Add an Enterprise user to a workspace. +https://docs.slack.dev/reference/methods/admin.users.assign

+
+
+def admin_users_invite(self,
*,
team_id: str,
email: str,
channel_ids: str | Sequence[str],
custom_message: str | None = None,
email_password_policy_enabled: bool | None = None,
guest_expiration_ts: str | float | None = None,
is_restricted: bool | None = None,
is_ultra_restricted: bool | None = None,
real_name: str | None = None,
resend: bool | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_users_invite(
+    self,
+    *,
+    team_id: str,
+    email: str,
+    channel_ids: Union[str, Sequence[str]],
+    custom_message: Optional[str] = None,
+    email_password_policy_enabled: Optional[bool] = None,
+    guest_expiration_ts: Optional[Union[str, float]] = None,
+    is_restricted: Optional[bool] = None,
+    is_ultra_restricted: Optional[bool] = None,
+    real_name: Optional[str] = None,
+    resend: Optional[bool] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Invite a user to a workspace.
+    https://docs.slack.dev/reference/methods/admin.users.invite
+    """
+    kwargs.update(
+        {
+            "team_id": team_id,
+            "email": email,
+            "custom_message": custom_message,
+            "email_password_policy_enabled": email_password_policy_enabled,
+            "guest_expiration_ts": str(guest_expiration_ts) if guest_expiration_ts is not None else None,
+            "is_restricted": is_restricted,
+            "is_ultra_restricted": is_ultra_restricted,
+            "real_name": real_name,
+            "resend": resend,
+        }
+    )
+    if isinstance(channel_ids, (list, tuple)):
+        kwargs.update({"channel_ids": ",".join(channel_ids)})
+    else:
+        kwargs.update({"channel_ids": channel_ids})
+    return self.api_call("admin.users.invite", params=kwargs)
+
+ +
+
+def admin_users_list(self,
*,
team_id: str | None = None,
include_deactivated_user_workspaces: bool | None = None,
is_active: bool | None = None,
cursor: str | None = None,
limit: int | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_users_list(
+    self,
+    *,
+    team_id: Optional[str] = None,
+    include_deactivated_user_workspaces: Optional[bool] = None,
+    is_active: Optional[bool] = None,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    **kwargs,
+) -> SlackResponse:
+    """List users on a workspace
+    https://docs.slack.dev/reference/methods/admin.users.list
+    """
+    kwargs.update(
+        {
+            "team_id": team_id,
+            "include_deactivated_user_workspaces": include_deactivated_user_workspaces,
+            "is_active": is_active,
+            "cursor": cursor,
+            "limit": limit,
+        }
+    )
+    return self.api_call("admin.users.list", params=kwargs)
+
+ +
+
+def admin_users_remove(self, *, team_id: str, user_id: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_users_remove(
+    self,
+    *,
+    team_id: str,
+    user_id: str,
+    **kwargs,
+) -> SlackResponse:
+    """Remove a user from a workspace.
+    https://docs.slack.dev/reference/methods/admin.users.remove
+    """
+    kwargs.update({"team_id": team_id, "user_id": user_id})
+    return self.api_call("admin.users.remove", params=kwargs)
+
+ +
+
+def admin_users_session_clearSettings(self, *, user_ids: str | Sequence[str], **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_users_session_clearSettings(
+    self,
+    *,
+    user_ids: Union[str, Sequence[str]],
+    **kwargs,
+) -> SlackResponse:
+    """Clear user-specific session settings—the session duration
+    and what happens when the client closes—for a list of users.
+    https://docs.slack.dev/reference/methods/admin.users.session.clearSettings
+    """
+    if isinstance(user_ids, (list, tuple)):
+        kwargs.update({"user_ids": ",".join(user_ids)})
+    else:
+        kwargs.update({"user_ids": user_ids})
+    return self.api_call("admin.users.session.clearSettings", params=kwargs)
+
+

Clear user-specific session settings—the session duration +and what happens when the client closes—for a list of users. +https://docs.slack.dev/reference/methods/admin.users.session.clearSettings

+
+
+def admin_users_session_getSettings(self, *, user_ids: str | Sequence[str], **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_users_session_getSettings(
+    self,
+    *,
+    user_ids: Union[str, Sequence[str]],
+    **kwargs,
+) -> SlackResponse:
+    """Get user-specific session settings—the session duration
+    and what happens when the client closes—given a list of users.
+    https://docs.slack.dev/reference/methods/admin.users.session.getSettings
+    """
+    if isinstance(user_ids, (list, tuple)):
+        kwargs.update({"user_ids": ",".join(user_ids)})
+    else:
+        kwargs.update({"user_ids": user_ids})
+    return self.api_call("admin.users.session.getSettings", params=kwargs)
+
+

Get user-specific session settings—the session duration +and what happens when the client closes—given a list of users. +https://docs.slack.dev/reference/methods/admin.users.session.getSettings

+
+
+def admin_users_session_invalidate(self, *, session_id: str, team_id: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_users_session_invalidate(
+    self,
+    *,
+    session_id: str,
+    team_id: str,
+    **kwargs,
+) -> SlackResponse:
+    """Invalidate a single session for a user by session_id.
+    https://docs.slack.dev/reference/methods/admin.users.session.invalidate
+    """
+    kwargs.update({"session_id": session_id, "team_id": team_id})
+    return self.api_call("admin.users.session.invalidate", params=kwargs)
+
+

Invalidate a single session for a user by session_id. +https://docs.slack.dev/reference/methods/admin.users.session.invalidate

+
+
+def admin_users_session_list(self,
*,
cursor: str | None = None,
limit: int | None = None,
team_id: str | None = None,
user_id: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_users_session_list(
+    self,
+    *,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    team_id: Optional[str] = None,
+    user_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Lists all active user sessions for an organization
+    https://docs.slack.dev/reference/methods/admin.users.session.list
+    """
+    kwargs.update(
+        {
+            "cursor": cursor,
+            "limit": limit,
+            "team_id": team_id,
+            "user_id": user_id,
+        }
+    )
+    return self.api_call("admin.users.session.list", params=kwargs)
+
+

Lists all active user sessions for an organization +https://docs.slack.dev/reference/methods/admin.users.session.list

+
+
+def admin_users_session_reset(self,
*,
user_id: str,
mobile_only: bool | None = None,
web_only: bool | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_users_session_reset(
+    self,
+    *,
+    user_id: str,
+    mobile_only: Optional[bool] = None,
+    web_only: Optional[bool] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Wipes all valid sessions on all devices for a given user.
+    https://docs.slack.dev/reference/methods/admin.users.session.reset
+    """
+    kwargs.update(
+        {
+            "user_id": user_id,
+            "mobile_only": mobile_only,
+            "web_only": web_only,
+        }
+    )
+    return self.api_call("admin.users.session.reset", params=kwargs)
+
+

Wipes all valid sessions on all devices for a given user. +https://docs.slack.dev/reference/methods/admin.users.session.reset

+
+
+def admin_users_session_resetBulk(self,
*,
user_ids: str | Sequence[str],
mobile_only: bool | None = None,
web_only: bool | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_users_session_resetBulk(
+    self,
+    *,
+    user_ids: Union[str, Sequence[str]],
+    mobile_only: Optional[bool] = None,
+    web_only: Optional[bool] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Enqueues an asynchronous job to wipe all valid sessions on all devices for a given list of users
+    https://docs.slack.dev/reference/methods/admin.users.session.resetBulk
+    """
+    if isinstance(user_ids, (list, tuple)):
+        kwargs.update({"user_ids": ",".join(user_ids)})
+    else:
+        kwargs.update({"user_ids": user_ids})
+    kwargs.update(
+        {
+            "mobile_only": mobile_only,
+            "web_only": web_only,
+        }
+    )
+    return self.api_call("admin.users.session.resetBulk", params=kwargs)
+
+

Enqueues an asynchronous job to wipe all valid sessions on all devices for a given list of users +https://docs.slack.dev/reference/methods/admin.users.session.resetBulk

+
+
+def admin_users_session_setSettings(self,
*,
user_ids: str | Sequence[str],
desktop_app_browser_quit: bool | None = None,
duration: int | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_users_session_setSettings(
+    self,
+    *,
+    user_ids: Union[str, Sequence[str]],
+    desktop_app_browser_quit: Optional[bool] = None,
+    duration: Optional[int] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Configure the user-level session settings—the session duration
+    and what happens when the client closes—for one or more users.
+    https://docs.slack.dev/reference/methods/admin.users.session.setSettings
+    """
+    if isinstance(user_ids, (list, tuple)):
+        kwargs.update({"user_ids": ",".join(user_ids)})
+    else:
+        kwargs.update({"user_ids": user_ids})
+    kwargs.update(
+        {
+            "desktop_app_browser_quit": desktop_app_browser_quit,
+            "duration": duration,
+        }
+    )
+    return self.api_call("admin.users.session.setSettings", params=kwargs)
+
+

Configure the user-level session settings—the session duration +and what happens when the client closes—for one or more users. +https://docs.slack.dev/reference/methods/admin.users.session.setSettings

+
+
+def admin_users_setAdmin(self, *, team_id: str, user_id: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_users_setAdmin(
+    self,
+    *,
+    team_id: str,
+    user_id: str,
+    **kwargs,
+) -> SlackResponse:
+    """Set an existing guest, regular user, or owner to be an admin user.
+    https://docs.slack.dev/reference/methods/admin.users.setAdmin
+    """
+    kwargs.update({"team_id": team_id, "user_id": user_id})
+    return self.api_call("admin.users.setAdmin", params=kwargs)
+
+

Set an existing guest, regular user, or owner to be an admin user. +https://docs.slack.dev/reference/methods/admin.users.setAdmin

+
+
+def admin_users_setExpiration(self, *, expiration_ts: int, user_id: str, team_id: str | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_users_setExpiration(
+    self,
+    *,
+    expiration_ts: int,
+    user_id: str,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Set an expiration for a guest user.
+    https://docs.slack.dev/reference/methods/admin.users.setExpiration
+    """
+    kwargs.update({"expiration_ts": expiration_ts, "team_id": team_id, "user_id": user_id})
+    return self.api_call("admin.users.setExpiration", params=kwargs)
+
+ +
+
+def admin_users_setOwner(self, *, team_id: str, user_id: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_users_setOwner(
+    self,
+    *,
+    team_id: str,
+    user_id: str,
+    **kwargs,
+) -> SlackResponse:
+    """Set an existing guest, regular user, or admin user to be a workspace owner.
+    https://docs.slack.dev/reference/methods/admin.users.setOwner
+    """
+    kwargs.update({"team_id": team_id, "user_id": user_id})
+    return self.api_call("admin.users.setOwner", params=kwargs)
+
+

Set an existing guest, regular user, or admin user to be a workspace owner. +https://docs.slack.dev/reference/methods/admin.users.setOwner

+
+
+def admin_users_setRegular(self, *, team_id: str, user_id: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_users_setRegular(
+    self,
+    *,
+    team_id: str,
+    user_id: str,
+    **kwargs,
+) -> SlackResponse:
+    """Set an existing guest user, admin user, or owner to be a regular user.
+    https://docs.slack.dev/reference/methods/admin.users.setRegular
+    """
+    kwargs.update({"team_id": team_id, "user_id": user_id})
+    return self.api_call("admin.users.setRegular", params=kwargs)
+
+

Set an existing guest user, admin user, or owner to be a regular user. +https://docs.slack.dev/reference/methods/admin.users.setRegular

+
+
+def admin_users_unsupportedVersions_export(self,
*,
date_end_of_support: str | int | None = None,
date_sessions_started: str | int | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_users_unsupportedVersions_export(
+    self,
+    *,
+    date_end_of_support: Optional[Union[str, int]] = None,
+    date_sessions_started: Optional[Union[str, int]] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Ask Slackbot to send you an export listing all workspace members using unsupported software,
+    presented as a zipped CSV file.
+    https://docs.slack.dev/reference/methods/admin.users.unsupportedVersions.export
+    """
+    kwargs.update(
+        {
+            "date_end_of_support": date_end_of_support,
+            "date_sessions_started": date_sessions_started,
+        }
+    )
+    return self.api_call("admin.users.unsupportedVersions.export", params=kwargs)
+
+

Ask Slackbot to send you an export listing all workspace members using unsupported software, +presented as a zipped CSV file. +https://docs.slack.dev/reference/methods/admin.users.unsupportedVersions.export

+
+
+def admin_workflows_collaborators_add(self,
*,
collaborator_ids: str | Sequence[str],
workflow_ids: str | Sequence[str],
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_workflows_collaborators_add(
+    self,
+    *,
+    collaborator_ids: Union[str, Sequence[str]],
+    workflow_ids: Union[str, Sequence[str]],
+    **kwargs,
+) -> SlackResponse:
+    """Add collaborators to workflows within the team or enterprise
+    https://docs.slack.dev/reference/methods/admin.workflows.collaborators.add
+    """
+    if isinstance(collaborator_ids, (list, tuple)):
+        kwargs.update({"collaborator_ids": ",".join(collaborator_ids)})
+    else:
+        kwargs.update({"collaborator_ids": collaborator_ids})
+    if isinstance(workflow_ids, (list, tuple)):
+        kwargs.update({"workflow_ids": ",".join(workflow_ids)})
+    else:
+        kwargs.update({"workflow_ids": workflow_ids})
+    return self.api_call("admin.workflows.collaborators.add", params=kwargs)
+
+

Add collaborators to workflows within the team or enterprise +https://docs.slack.dev/reference/methods/admin.workflows.collaborators.add

+
+
+def admin_workflows_collaborators_remove(self,
*,
collaborator_ids: str | Sequence[str],
workflow_ids: str | Sequence[str],
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_workflows_collaborators_remove(
+    self,
+    *,
+    collaborator_ids: Union[str, Sequence[str]],
+    workflow_ids: Union[str, Sequence[str]],
+    **kwargs,
+) -> SlackResponse:
+    """Remove collaborators from workflows within the team or enterprise
+    https://docs.slack.dev/reference/methods/admin.workflows.collaborators.remove
+    """
+    if isinstance(collaborator_ids, (list, tuple)):
+        kwargs.update({"collaborator_ids": ",".join(collaborator_ids)})
+    else:
+        kwargs.update({"collaborator_ids": collaborator_ids})
+    if isinstance(workflow_ids, (list, tuple)):
+        kwargs.update({"workflow_ids": ",".join(workflow_ids)})
+    else:
+        kwargs.update({"workflow_ids": workflow_ids})
+    return self.api_call("admin.workflows.collaborators.remove", params=kwargs)
+
+

Remove collaborators from workflows within the team or enterprise +https://docs.slack.dev/reference/methods/admin.workflows.collaborators.remove

+
+
+def admin_workflows_permissions_lookup(self,
*,
workflow_ids: str | Sequence[str],
max_workflow_triggers: int | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_workflows_permissions_lookup(
+    self,
+    *,
+    workflow_ids: Union[str, Sequence[str]],
+    max_workflow_triggers: Optional[int] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Look up the permissions for a set of workflows
+    https://docs.slack.dev/reference/methods/admin.workflows.permissions.lookup
+    """
+    if isinstance(workflow_ids, (list, tuple)):
+        kwargs.update({"workflow_ids": ",".join(workflow_ids)})
+    else:
+        kwargs.update({"workflow_ids": workflow_ids})
+    kwargs.update(
+        {
+            "max_workflow_triggers": max_workflow_triggers,
+        }
+    )
+    return self.api_call("admin.workflows.permissions.lookup", params=kwargs)
+
+ +
+ +
+
+ +Expand source code + +
def admin_workflows_search(
+    self,
+    *,
+    app_id: Optional[str] = None,
+    collaborator_ids: Optional[Union[str, Sequence[str]]] = None,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    no_collaborators: Optional[bool] = None,
+    num_trigger_ids: Optional[int] = None,
+    query: Optional[str] = None,
+    sort: Optional[str] = None,
+    sort_dir: Optional[str] = None,
+    source: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Search workflows within the team or enterprise
+    https://docs.slack.dev/reference/methods/admin.workflows.search
+    """
+    if collaborator_ids is not None:
+        if isinstance(collaborator_ids, (list, tuple)):
+            kwargs.update({"collaborator_ids": ",".join(collaborator_ids)})
+        else:
+            kwargs.update({"collaborator_ids": collaborator_ids})
+    kwargs.update(
+        {
+            "app_id": app_id,
+            "cursor": cursor,
+            "limit": limit,
+            "no_collaborators": no_collaborators,
+            "num_trigger_ids": num_trigger_ids,
+            "query": query,
+            "sort": sort,
+            "sort_dir": sort_dir,
+            "source": source,
+        }
+    )
+    return self.api_call("admin.workflows.search", params=kwargs)
+
+

Search workflows within the team or enterprise +https://docs.slack.dev/reference/methods/admin.workflows.search

+
+
+def admin_workflows_unpublish(self, *, workflow_ids: str | Sequence[str], **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_workflows_unpublish(
+    self,
+    *,
+    workflow_ids: Union[str, Sequence[str]],
+    **kwargs,
+) -> SlackResponse:
+    """Unpublish workflows within the team or enterprise
+    https://docs.slack.dev/reference/methods/admin.workflows.unpublish
+    """
+    if isinstance(workflow_ids, (list, tuple)):
+        kwargs.update({"workflow_ids": ",".join(workflow_ids)})
+    else:
+        kwargs.update({"workflow_ids": workflow_ids})
+    return self.api_call("admin.workflows.unpublish", params=kwargs)
+
+

Unpublish workflows within the team or enterprise +https://docs.slack.dev/reference/methods/admin.workflows.unpublish

+
+
+def api_test(self, *, error: str | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def api_test(
+    self,
+    *,
+    error: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Checks API calling code.
+    https://docs.slack.dev/reference/methods/api.test
+    """
+    kwargs.update({"error": error})
+    return self.api_call("api.test", params=kwargs)
+
+ +
+
+def apps_connections_open(self, *, app_token: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def apps_connections_open(
+    self,
+    *,
+    app_token: str,
+    **kwargs,
+) -> SlackResponse:
+    """Generate a temporary Socket Mode WebSocket URL that your app can connect to
+    in order to receive events and interactive payloads
+    https://docs.slack.dev/reference/methods/apps.connections.open
+    """
+    kwargs.update({"token": app_token})
+    return self.api_call("apps.connections.open", http_verb="POST", params=kwargs)
+
+

Generate a temporary Socket Mode WebSocket URL that your app can connect to +in order to receive events and interactive payloads +https://docs.slack.dev/reference/methods/apps.connections.open

+
+
+def apps_event_authorizations_list(self,
*,
event_context: str,
cursor: str | None = None,
limit: int | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def apps_event_authorizations_list(
+    self,
+    *,
+    event_context: str,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Get a list of authorizations for the given event context.
+    Each authorization represents an app installation that the event is visible to.
+    https://docs.slack.dev/reference/methods/apps.event.authorizations.list
+    """
+    kwargs.update({"event_context": event_context, "cursor": cursor, "limit": limit})
+    return self.api_call("apps.event.authorizations.list", params=kwargs)
+
+

Get a list of authorizations for the given event context. +Each authorization represents an app installation that the event is visible to. +https://docs.slack.dev/reference/methods/apps.event.authorizations.list

+
+
+def apps_manifest_create(self, *, manifest: str | Dict[str, Any], **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def apps_manifest_create(
+    self,
+    *,
+    manifest: Union[str, Dict[str, Any]],
+    **kwargs,
+) -> SlackResponse:
+    """Create an app from an app manifest
+    https://docs.slack.dev/reference/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 +
+
+
+ +Expand source code + +
def apps_manifest_delete(
+    self,
+    *,
+    app_id: str,
+    **kwargs,
+) -> SlackResponse:
+    """Permanently deletes an app created through app manifests
+    https://docs.slack.dev/reference/methods/apps.manifest.delete
+    """
+    kwargs.update({"app_id": app_id})
+    return self.api_call("apps.manifest.delete", params=kwargs)
+
+

Permanently deletes an app created through app manifests +https://docs.slack.dev/reference/methods/apps.manifest.delete

+
+
+def apps_manifest_export(self, *, app_id: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def apps_manifest_export(
+    self,
+    *,
+    app_id: str,
+    **kwargs,
+) -> SlackResponse:
+    """Export an app manifest from an existing app
+    https://docs.slack.dev/reference/methods/apps.manifest.export
+    """
+    kwargs.update({"app_id": app_id})
+    return self.api_call("apps.manifest.export", params=kwargs)
+
+

Export an app manifest from an existing app +https://docs.slack.dev/reference/methods/apps.manifest.export

+
+
+def apps_manifest_update(self, *, app_id: str, manifest: str | Dict[str, Any], **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def apps_manifest_update(
+    self,
+    *,
+    app_id: str,
+    manifest: Union[str, Dict[str, Any]],
+    **kwargs,
+) -> SlackResponse:
+    """Update an app from an app manifest
+    https://docs.slack.dev/reference/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: str | Dict[str, Any], app_id: str | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def apps_manifest_validate(
+    self,
+    *,
+    manifest: Union[str, Dict[str, Any]],
+    app_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Validate an app manifest
+    https://docs.slack.dev/reference/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 apps_uninstall(self, *, client_id: str, client_secret: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def apps_uninstall(
+    self,
+    *,
+    client_id: str,
+    client_secret: str,
+    **kwargs,
+) -> SlackResponse:
+    """Uninstalls your app from a workspace.
+    https://docs.slack.dev/reference/methods/apps.uninstall
+    """
+    kwargs.update({"client_id": client_id, "client_secret": client_secret})
+    return self.api_call("apps.uninstall", params=kwargs)
+
+

Uninstalls your app from a workspace. +https://docs.slack.dev/reference/methods/apps.uninstall

+
+
+def assistant_threads_setStatus(self,
*,
channel_id: str,
thread_ts: str,
status: str,
loading_messages: List[str] | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def assistant_threads_setStatus(
+    self,
+    *,
+    channel_id: str,
+    thread_ts: str,
+    status: str,
+    loading_messages: Optional[List[str]] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Set the status for an AI assistant thread.
+    https://docs.slack.dev/reference/methods/assistant.threads.setStatus
+    """
+    kwargs.update(
+        {"channel_id": channel_id, "thread_ts": thread_ts, "status": status, "loading_messages": loading_messages}
+    )
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("assistant.threads.setStatus", json=kwargs)
+
+ +
+
+def assistant_threads_setSuggestedPrompts(self,
*,
channel_id: str,
thread_ts: str,
title: str | None = None,
prompts: List[Dict[str, str]],
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def assistant_threads_setSuggestedPrompts(
+    self,
+    *,
+    channel_id: str,
+    thread_ts: str,
+    title: Optional[str] = None,
+    prompts: List[Dict[str, str]],
+    **kwargs,
+) -> SlackResponse:
+    """Set suggested prompts for the given assistant thread.
+    https://docs.slack.dev/reference/methods/assistant.threads.setSuggestedPrompts
+    """
+    kwargs.update({"channel_id": channel_id, "thread_ts": thread_ts, "prompts": prompts})
+    if title is not None:
+        kwargs.update({"title": title})
+    return self.api_call("assistant.threads.setSuggestedPrompts", json=kwargs)
+
+

Set suggested prompts for the given assistant thread. +https://docs.slack.dev/reference/methods/assistant.threads.setSuggestedPrompts

+
+
+def assistant_threads_setTitle(self, *, channel_id: str, thread_ts: str, title: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def assistant_threads_setTitle(
+    self,
+    *,
+    channel_id: str,
+    thread_ts: str,
+    title: str,
+    **kwargs,
+) -> SlackResponse:
+    """Set the title for the given assistant thread.
+    https://docs.slack.dev/reference/methods/assistant.threads.setTitle
+    """
+    kwargs.update({"channel_id": channel_id, "thread_ts": thread_ts, "title": title})
+    return self.api_call("assistant.threads.setTitle", params=kwargs)
+
+

Set the title for the given assistant thread. +https://docs.slack.dev/reference/methods/assistant.threads.setTitle

+
+
+def auth_revoke(self, *, test: bool | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def auth_revoke(
+    self,
+    *,
+    test: Optional[bool] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Revokes a token.
+    https://docs.slack.dev/reference/methods/auth.revoke
+    """
+    kwargs.update({"test": test})
+    return self.api_call("auth.revoke", http_verb="GET", params=kwargs)
+
+ +
+
+def auth_teams_list(self,
cursor: str | None = None,
limit: int | None = None,
include_icon: bool | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def auth_teams_list(
+    self,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    include_icon: Optional[bool] = None,
+    **kwargs,
+) -> SlackResponse:
+    """List the workspaces a token can access.
+    https://docs.slack.dev/reference/methods/auth.teams.list
+    """
+    kwargs.update({"cursor": cursor, "limit": limit, "include_icon": include_icon})
+    return self.api_call("auth.teams.list", params=kwargs)
+
+

List the workspaces a token can access. +https://docs.slack.dev/reference/methods/auth.teams.list

+
+
+def auth_test(self, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def auth_test(
+    self,
+    **kwargs,
+) -> SlackResponse:
+    """Checks authentication & identity.
+    https://docs.slack.dev/reference/methods/auth.test
+    """
+    return self.api_call("auth.test", params=kwargs)
+
+

Checks authentication & identity. +https://docs.slack.dev/reference/methods/auth.test

+
+
+def bookmarks_add(self,
*,
channel_id: str,
title: str,
type: str,
emoji: str | None = None,
entity_id: str | None = None,
link: str | None = None,
parent_id: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def bookmarks_add(
+    self,
+    *,
+    channel_id: str,
+    title: str,
+    type: str,
+    emoji: Optional[str] = None,
+    entity_id: Optional[str] = None,
+    link: Optional[str] = None,  # include when type is 'link'
+    parent_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Add bookmark to a channel.
+    https://docs.slack.dev/reference/methods/bookmarks.add
+    """
+    kwargs.update(
+        {
+            "channel_id": channel_id,
+            "title": title,
+            "type": type,
+            "emoji": emoji,
+            "entity_id": entity_id,
+            "link": link,
+            "parent_id": parent_id,
+        }
+    )
+    return self.api_call("bookmarks.add", http_verb="POST", params=kwargs)
+
+ +
+
+def bookmarks_edit(self,
*,
bookmark_id: str,
channel_id: str,
emoji: str | None = None,
link: str | None = None,
title: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def bookmarks_edit(
+    self,
+    *,
+    bookmark_id: str,
+    channel_id: str,
+    emoji: Optional[str] = None,
+    link: Optional[str] = None,
+    title: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Edit bookmark.
+    https://docs.slack.dev/reference/methods/bookmarks.edit
+    """
+    kwargs.update(
+        {
+            "bookmark_id": bookmark_id,
+            "channel_id": channel_id,
+            "emoji": emoji,
+            "link": link,
+            "title": title,
+        }
+    )
+    return self.api_call("bookmarks.edit", http_verb="POST", params=kwargs)
+
+ +
+
+def bookmarks_list(self, *, channel_id: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def bookmarks_list(
+    self,
+    *,
+    channel_id: str,
+    **kwargs,
+) -> SlackResponse:
+    """List bookmark for the channel.
+    https://docs.slack.dev/reference/methods/bookmarks.list
+    """
+    kwargs.update({"channel_id": channel_id})
+    return self.api_call("bookmarks.list", http_verb="POST", params=kwargs)
+
+ +
+
+def bookmarks_remove(self, *, bookmark_id: str, channel_id: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def bookmarks_remove(
+    self,
+    *,
+    bookmark_id: str,
+    channel_id: str,
+    **kwargs,
+) -> SlackResponse:
+    """Remove bookmark from the channel.
+    https://docs.slack.dev/reference/methods/bookmarks.remove
+    """
+    kwargs.update({"bookmark_id": bookmark_id, "channel_id": channel_id})
+    return self.api_call("bookmarks.remove", http_verb="POST", params=kwargs)
+
+ +
+
+def bots_info(self, *, bot: str | None = None, team_id: str | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def bots_info(
+    self,
+    *,
+    bot: Optional[str] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Gets information about a bot user.
+    https://docs.slack.dev/reference/methods/bots.info
+    """
+    kwargs.update({"bot": bot, "team_id": team_id})
+    return self.api_call("bots.info", http_verb="GET", params=kwargs)
+
+

Gets information about a bot user. +https://docs.slack.dev/reference/methods/bots.info

+
+
+def calls_add(self,
*,
external_unique_id: str,
join_url: str,
created_by: str | None = None,
date_start: int | None = None,
desktop_app_join_url: str | None = None,
external_display_id: str | None = None,
title: str | None = None,
users: str | Sequence[Dict[str, str]] | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def calls_add(
+    self,
+    *,
+    external_unique_id: str,
+    join_url: str,
+    created_by: Optional[str] = None,
+    date_start: Optional[int] = None,
+    desktop_app_join_url: Optional[str] = None,
+    external_display_id: Optional[str] = None,
+    title: Optional[str] = None,
+    users: Optional[Union[str, Sequence[Dict[str, str]]]] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Registers a new Call.
+    https://docs.slack.dev/reference/methods/calls.add
+    """
+    kwargs.update(
+        {
+            "external_unique_id": external_unique_id,
+            "join_url": join_url,
+            "created_by": created_by,
+            "date_start": date_start,
+            "desktop_app_join_url": desktop_app_join_url,
+            "external_display_id": external_display_id,
+            "title": title,
+        }
+    )
+    _update_call_participants(
+        kwargs,
+        users if users is not None else kwargs.get("users"),  # type: ignore[arg-type]
+    )
+    return self.api_call("calls.add", http_verb="POST", params=kwargs)
+
+ +
+
+def calls_end(self, *, id: str, duration: int | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def calls_end(
+    self,
+    *,
+    id: str,
+    duration: Optional[int] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Ends a Call.
+    https://docs.slack.dev/reference/methods/calls.end
+    """
+    kwargs.update({"id": id, "duration": duration})
+    return self.api_call("calls.end", http_verb="POST", params=kwargs)
+
+ +
+
+def calls_info(self, *, id: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def calls_info(
+    self,
+    *,
+    id: str,
+    **kwargs,
+) -> SlackResponse:
+    """Returns information about a Call.
+    https://docs.slack.dev/reference/methods/calls.info
+    """
+    kwargs.update({"id": id})
+    return self.api_call("calls.info", http_verb="POST", params=kwargs)
+
+

Returns information about a Call. +https://docs.slack.dev/reference/methods/calls.info

+
+
+def calls_participants_add(self, *, id: str, users: str | Sequence[Dict[str, str]], **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def calls_participants_add(
+    self,
+    *,
+    id: str,
+    users: Union[str, Sequence[Dict[str, str]]],
+    **kwargs,
+) -> SlackResponse:
+    """Registers new participants added to a Call.
+    https://docs.slack.dev/reference/methods/calls.participants.add
+    """
+    kwargs.update({"id": id})
+    _update_call_participants(kwargs, users)
+    return self.api_call("calls.participants.add", http_verb="POST", params=kwargs)
+
+

Registers new participants added to a Call. +https://docs.slack.dev/reference/methods/calls.participants.add

+
+
+def calls_participants_remove(self, *, id: str, users: str | Sequence[Dict[str, str]], **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def calls_participants_remove(
+    self,
+    *,
+    id: str,
+    users: Union[str, Sequence[Dict[str, str]]],
+    **kwargs,
+) -> SlackResponse:
+    """Registers participants removed from a Call.
+    https://docs.slack.dev/reference/methods/calls.participants.remove
+    """
+    kwargs.update({"id": id})
+    _update_call_participants(kwargs, users)
+    return self.api_call("calls.participants.remove", http_verb="POST", params=kwargs)
+
+

Registers participants removed from a Call. +https://docs.slack.dev/reference/methods/calls.participants.remove

+
+
+def calls_update(self,
*,
id: str,
desktop_app_join_url: str | None = None,
join_url: str | None = None,
title: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def calls_update(
+    self,
+    *,
+    id: str,
+    desktop_app_join_url: Optional[str] = None,
+    join_url: Optional[str] = None,
+    title: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Updates information about a Call.
+    https://docs.slack.dev/reference/methods/calls.update
+    """
+    kwargs.update(
+        {
+            "id": id,
+            "desktop_app_join_url": desktop_app_join_url,
+            "join_url": join_url,
+            "title": title,
+        }
+    )
+    return self.api_call("calls.update", http_verb="POST", params=kwargs)
+
+ +
+
+def canvases_access_delete(self,
*,
canvas_id: str,
channel_ids: str | Sequence[str] | None = None,
user_ids: str | Sequence[str] | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def canvases_access_delete(
+    self,
+    *,
+    canvas_id: str,
+    channel_ids: Optional[Union[Sequence[str], str]] = None,
+    user_ids: Optional[Union[Sequence[str], str]] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Create a Channel Canvas for a channel
+    https://docs.slack.dev/reference/methods/canvases.access.delete
+    """
+    kwargs.update({"canvas_id": canvas_id})
+    if channel_ids is not None:
+        if isinstance(channel_ids, (list, tuple)):
+            kwargs.update({"channel_ids": ",".join(channel_ids)})
+        else:
+            kwargs.update({"channel_ids": channel_ids})
+    if user_ids is not None:
+        if isinstance(user_ids, (list, tuple)):
+            kwargs.update({"user_ids": ",".join(user_ids)})
+        else:
+            kwargs.update({"user_ids": user_ids})
+    return self.api_call("canvases.access.delete", params=kwargs)
+
+ +
+
+def canvases_access_set(self,
*,
canvas_id: str,
access_level: str,
channel_ids: str | Sequence[str] | None = None,
user_ids: str | Sequence[str] | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def canvases_access_set(
+    self,
+    *,
+    canvas_id: str,
+    access_level: str,
+    channel_ids: Optional[Union[Sequence[str], str]] = None,
+    user_ids: Optional[Union[Sequence[str], str]] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Sets the access level to a canvas for specified entities
+    https://docs.slack.dev/reference/methods/canvases.access.set
+    """
+    kwargs.update({"canvas_id": canvas_id, "access_level": access_level})
+    if channel_ids is not None:
+        if isinstance(channel_ids, (list, tuple)):
+            kwargs.update({"channel_ids": ",".join(channel_ids)})
+        else:
+            kwargs.update({"channel_ids": channel_ids})
+    if user_ids is not None:
+        if isinstance(user_ids, (list, tuple)):
+            kwargs.update({"user_ids": ",".join(user_ids)})
+        else:
+            kwargs.update({"user_ids": user_ids})
+
+    return self.api_call("canvases.access.set", params=kwargs)
+
+

Sets the access level to a canvas for specified entities +https://docs.slack.dev/reference/methods/canvases.access.set

+
+
+def canvases_create(self, *, title: str | None = None, document_content: Dict[str, str], **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def canvases_create(
+    self,
+    *,
+    title: Optional[str] = None,
+    document_content: Dict[str, str],
+    **kwargs,
+) -> SlackResponse:
+    """Create Canvas for a user
+    https://docs.slack.dev/reference/methods/canvases.create
+    """
+    kwargs.update({"title": title, "document_content": document_content})
+    return self.api_call("canvases.create", json=kwargs)
+
+ +
+
+def canvases_delete(self, *, canvas_id: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def canvases_delete(
+    self,
+    *,
+    canvas_id: str,
+    **kwargs,
+) -> SlackResponse:
+    """Deletes a canvas
+    https://docs.slack.dev/reference/methods/canvases.delete
+    """
+    kwargs.update({"canvas_id": canvas_id})
+    return self.api_call("canvases.delete", params=kwargs)
+
+ +
+
+def canvases_edit(self, *, canvas_id: str, changes: Sequence[Dict[str, Any]], **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def canvases_edit(
+    self,
+    *,
+    canvas_id: str,
+    changes: Sequence[Dict[str, Any]],
+    **kwargs,
+) -> SlackResponse:
+    """Update an existing canvas
+    https://docs.slack.dev/reference/methods/canvases.edit
+    """
+    kwargs.update({"canvas_id": canvas_id, "changes": changes})
+    return self.api_call("canvases.edit", json=kwargs)
+
+ +
+
+def canvases_sections_lookup(self, *, canvas_id: str, criteria: Dict[str, Any], **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def canvases_sections_lookup(
+    self,
+    *,
+    canvas_id: str,
+    criteria: Dict[str, Any],
+    **kwargs,
+) -> SlackResponse:
+    """Find sections matching the provided criteria
+    https://docs.slack.dev/reference/methods/canvases.sections.lookup
+    """
+    kwargs.update({"canvas_id": canvas_id, "criteria": json.dumps(criteria)})
+    return self.api_call("canvases.sections.lookup", params=kwargs)
+
+

Find sections matching the provided criteria +https://docs.slack.dev/reference/methods/canvases.sections.lookup

+
+
+def channels_archive(self, *, channel: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def channels_archive(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> SlackResponse:
+    """Archives a channel."""
+    kwargs.update({"channel": channel})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("channels.archive", json=kwargs)
+
+

Archives a channel.

+
+
+def channels_create(self, *, name: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def channels_create(
+    self,
+    *,
+    name: str,
+    **kwargs,
+) -> SlackResponse:
+    """Creates a channel."""
+    kwargs.update({"name": name})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("channels.create", json=kwargs)
+
+

Creates a channel.

+
+
+def channels_history(self, *, channel: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def channels_history(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> SlackResponse:
+    """Fetches history of messages and events from a channel."""
+    kwargs.update({"channel": channel})
+    return self.api_call("channels.history", http_verb="GET", params=kwargs)
+
+

Fetches history of messages and events from a channel.

+
+
+def channels_info(self, *, channel: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def channels_info(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> SlackResponse:
+    """Gets information about a channel."""
+    kwargs.update({"channel": channel})
+    return self.api_call("channels.info", http_verb="GET", params=kwargs)
+
+

Gets information about a channel.

+
+
+def channels_invite(self, *, channel: str, user: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def channels_invite(
+    self,
+    *,
+    channel: str,
+    user: str,
+    **kwargs,
+) -> SlackResponse:
+    """Invites a user to a channel."""
+    kwargs.update({"channel": channel, "user": user})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("channels.invite", json=kwargs)
+
+

Invites a user to a channel.

+
+
+def channels_join(self, *, name: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def channels_join(
+    self,
+    *,
+    name: str,
+    **kwargs,
+) -> SlackResponse:
+    """Joins a channel, creating it if needed."""
+    kwargs.update({"name": name})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("channels.join", json=kwargs)
+
+

Joins a channel, creating it if needed.

+
+
+def channels_kick(self, *, channel: str, user: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def channels_kick(
+    self,
+    *,
+    channel: str,
+    user: str,
+    **kwargs,
+) -> SlackResponse:
+    """Removes a user from a channel."""
+    kwargs.update({"channel": channel, "user": user})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("channels.kick", json=kwargs)
+
+

Removes a user from a channel.

+
+
+def channels_leave(self, *, channel: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def channels_leave(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> SlackResponse:
+    """Leaves a channel."""
+    kwargs.update({"channel": channel})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("channels.leave", json=kwargs)
+
+

Leaves a channel.

+
+
+def channels_list(self, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def channels_list(
+    self,
+    **kwargs,
+) -> SlackResponse:
+    """Lists all channels in a Slack team."""
+    return self.api_call("channels.list", http_verb="GET", params=kwargs)
+
+

Lists all channels in a Slack team.

+
+
+def channels_mark(self, *, channel: str, ts: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def channels_mark(
+    self,
+    *,
+    channel: str,
+    ts: str,
+    **kwargs,
+) -> SlackResponse:
+    """Sets the read cursor in a channel."""
+    kwargs.update({"channel": channel, "ts": ts})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("channels.mark", json=kwargs)
+
+

Sets the read cursor in a channel.

+
+
+def channels_rename(self, *, channel: str, name: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def channels_rename(
+    self,
+    *,
+    channel: str,
+    name: str,
+    **kwargs,
+) -> SlackResponse:
+    """Renames a channel."""
+    kwargs.update({"channel": channel, "name": name})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("channels.rename", json=kwargs)
+
+

Renames a channel.

+
+
+def channels_replies(self, *, channel: str, thread_ts: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def channels_replies(
+    self,
+    *,
+    channel: str,
+    thread_ts: str,
+    **kwargs,
+) -> SlackResponse:
+    """Retrieve a thread of messages posted to a channel"""
+    kwargs.update({"channel": channel, "thread_ts": thread_ts})
+    return self.api_call("channels.replies", http_verb="GET", params=kwargs)
+
+

Retrieve a thread of messages posted to a channel

+
+
+def channels_setPurpose(self, *, channel: str, purpose: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def channels_setPurpose(
+    self,
+    *,
+    channel: str,
+    purpose: str,
+    **kwargs,
+) -> SlackResponse:
+    """Sets the purpose for a channel."""
+    kwargs.update({"channel": channel, "purpose": purpose})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("channels.setPurpose", json=kwargs)
+
+

Sets the purpose for a channel.

+
+
+def channels_setTopic(self, *, channel: str, topic: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def channels_setTopic(
+    self,
+    *,
+    channel: str,
+    topic: str,
+    **kwargs,
+) -> SlackResponse:
+    """Sets the topic for a channel."""
+    kwargs.update({"channel": channel, "topic": topic})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("channels.setTopic", json=kwargs)
+
+

Sets the topic for a channel.

+
+
+def channels_unarchive(self, *, channel: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def channels_unarchive(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> SlackResponse:
+    """Unarchives a channel."""
+    kwargs.update({"channel": channel})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("channels.unarchive", json=kwargs)
+
+

Unarchives a channel.

+
+
+def chat_appendStream(self, *, channel: str, ts: str, markdown_text: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def chat_appendStream(
+    self,
+    *,
+    channel: str,
+    ts: str,
+    markdown_text: str,
+    **kwargs,
+) -> SlackResponse:
+    """Appends text to an existing streaming conversation.
+    https://docs.slack.dev/reference/methods/chat.appendStream
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "ts": ts,
+            "markdown_text": markdown_text,
+        }
+    )
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("chat.appendStream", json=kwargs)
+
+

Appends text to an existing streaming conversation. +https://docs.slack.dev/reference/methods/chat.appendStream

+
+
+def chat_delete(self, *, channel: str, ts: str, as_user: bool | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def chat_delete(
+    self,
+    *,
+    channel: str,
+    ts: str,
+    as_user: Optional[bool] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Deletes a message.
+    https://docs.slack.dev/reference/methods/chat.delete
+    """
+    kwargs.update({"channel": channel, "ts": ts, "as_user": as_user})
+    return self.api_call("chat.delete", params=kwargs)
+
+ +
+
+def chat_deleteScheduledMessage(self,
*,
channel: str,
scheduled_message_id: str,
as_user: bool | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def chat_deleteScheduledMessage(
+    self,
+    *,
+    channel: str,
+    scheduled_message_id: str,
+    as_user: Optional[bool] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Deletes a scheduled message.
+    https://docs.slack.dev/reference/methods/chat.deleteScheduledMessage
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "scheduled_message_id": scheduled_message_id,
+            "as_user": as_user,
+        }
+    )
+    return self.api_call("chat.deleteScheduledMessage", params=kwargs)
+
+ +
+ +
+
+ +Expand source code + +
def chat_getPermalink(
+    self,
+    *,
+    channel: str,
+    message_ts: str,
+    **kwargs,
+) -> SlackResponse:
+    """Retrieve a permalink URL for a specific extant message
+    https://docs.slack.dev/reference/methods/chat.getPermalink
+    """
+    kwargs.update({"channel": channel, "message_ts": message_ts})
+    return self.api_call("chat.getPermalink", http_verb="GET", params=kwargs)
+
+

Retrieve a permalink URL for a specific extant message +https://docs.slack.dev/reference/methods/chat.getPermalink

+
+
+def chat_meMessage(self, *, channel: str, text: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def chat_meMessage(
+    self,
+    *,
+    channel: str,
+    text: str,
+    **kwargs,
+) -> SlackResponse:
+    """Share a me message into a channel.
+    https://docs.slack.dev/reference/methods/chat.meMessage
+    """
+    kwargs.update({"channel": channel, "text": text})
+    return self.api_call("chat.meMessage", params=kwargs)
+
+ +
+
+def chat_postEphemeral(self,
*,
channel: str,
user: str,
text: str | None = None,
as_user: bool | None = None,
attachments: str | Sequence[Dict | Attachment] | None = None,
blocks: str | Sequence[Dict | Block] | None = None,
thread_ts: str | None = None,
icon_emoji: str | None = None,
icon_url: str | None = None,
link_names: bool | None = None,
username: str | None = None,
parse: str | None = None,
markdown_text: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def chat_postEphemeral(
+    self,
+    *,
+    channel: str,
+    user: str,
+    text: Optional[str] = None,
+    as_user: Optional[bool] = None,
+    attachments: Optional[Union[str, Sequence[Union[Dict, Attachment]]]] = None,
+    blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None,
+    thread_ts: Optional[str] = None,
+    icon_emoji: Optional[str] = None,
+    icon_url: Optional[str] = None,
+    link_names: Optional[bool] = None,
+    username: Optional[str] = None,
+    parse: Optional[str] = None,
+    markdown_text: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Sends an ephemeral message to a user in a channel.
+    https://docs.slack.dev/reference/methods/chat.postEphemeral
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "user": user,
+            "text": text,
+            "as_user": as_user,
+            "attachments": attachments,
+            "blocks": blocks,
+            "thread_ts": thread_ts,
+            "icon_emoji": icon_emoji,
+            "icon_url": icon_url,
+            "link_names": link_names,
+            "username": username,
+            "parse": parse,
+            "markdown_text": markdown_text,
+        }
+    )
+    _parse_web_class_objects(kwargs)
+    kwargs = _remove_none_values(kwargs)
+    _warn_if_message_text_content_is_missing("chat.postEphemeral", kwargs)
+    # NOTE: intentionally using json over params for the API methods using blocks/attachments
+    return self.api_call("chat.postEphemeral", json=kwargs)
+
+

Sends an ephemeral message to a user in a channel. +https://docs.slack.dev/reference/methods/chat.postEphemeral

+
+
+def chat_postMessage(self,
*,
channel: str,
text: str | None = None,
as_user: bool | None = None,
attachments: str | Sequence[Dict | Attachment] | None = None,
blocks: str | Sequence[Dict | Block] | None = None,
thread_ts: str | None = None,
reply_broadcast: bool | None = None,
unfurl_links: bool | None = None,
unfurl_media: bool | None = None,
container_id: str | None = None,
icon_emoji: str | None = None,
icon_url: str | None = None,
mrkdwn: bool | None = None,
link_names: bool | None = None,
username: str | None = None,
parse: str | None = None,
metadata: Dict | Metadata | EventAndEntityMetadata | None = None,
markdown_text: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def chat_postMessage(
+    self,
+    *,
+    channel: str,
+    text: Optional[str] = None,
+    as_user: Optional[bool] = None,
+    attachments: Optional[Union[str, Sequence[Union[Dict, Attachment]]]] = None,
+    blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None,
+    thread_ts: Optional[str] = None,
+    reply_broadcast: Optional[bool] = None,
+    unfurl_links: Optional[bool] = None,
+    unfurl_media: Optional[bool] = None,
+    container_id: Optional[str] = None,
+    icon_emoji: Optional[str] = None,
+    icon_url: Optional[str] = None,
+    mrkdwn: Optional[bool] = None,
+    link_names: Optional[bool] = None,
+    username: Optional[str] = None,
+    parse: Optional[str] = None,  # none, full
+    metadata: Optional[Union[Dict, Metadata, EventAndEntityMetadata]] = None,
+    markdown_text: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Sends a message to a channel.
+    https://docs.slack.dev/reference/methods/chat.postMessage
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "text": text,
+            "as_user": as_user,
+            "attachments": attachments,
+            "blocks": blocks,
+            "thread_ts": thread_ts,
+            "reply_broadcast": reply_broadcast,
+            "unfurl_links": unfurl_links,
+            "unfurl_media": unfurl_media,
+            "container_id": container_id,
+            "icon_emoji": icon_emoji,
+            "icon_url": icon_url,
+            "mrkdwn": mrkdwn,
+            "link_names": link_names,
+            "username": username,
+            "parse": parse,
+            "metadata": metadata,
+            "markdown_text": markdown_text,
+        }
+    )
+    _parse_web_class_objects(kwargs)
+    kwargs = _remove_none_values(kwargs)
+    _warn_if_message_text_content_is_missing("chat.postMessage", kwargs)
+    # NOTE: intentionally using json over params for the API methods using blocks/attachments
+    return self.api_call("chat.postMessage", json=kwargs)
+
+ +
+
+def chat_scheduleMessage(self,
*,
channel: str,
post_at: str | int,
text: str | None = None,
as_user: bool | None = None,
attachments: str | Sequence[Dict | Attachment] | None = None,
blocks: str | Sequence[Dict | Block] | None = None,
thread_ts: str | None = None,
parse: str | None = None,
reply_broadcast: bool | None = None,
unfurl_links: bool | None = None,
unfurl_media: bool | None = None,
link_names: bool | None = None,
metadata: Dict | Metadata | None = None,
markdown_text: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def chat_scheduleMessage(
+    self,
+    *,
+    channel: str,
+    post_at: Union[str, int],
+    text: Optional[str] = None,
+    as_user: Optional[bool] = None,
+    attachments: Optional[Union[str, Sequence[Union[Dict, Attachment]]]] = None,
+    blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None,
+    thread_ts: Optional[str] = None,
+    parse: Optional[str] = None,
+    reply_broadcast: Optional[bool] = None,
+    unfurl_links: Optional[bool] = None,
+    unfurl_media: Optional[bool] = None,
+    link_names: Optional[bool] = None,
+    metadata: Optional[Union[Dict, Metadata]] = None,
+    markdown_text: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Schedules a message.
+    https://docs.slack.dev/reference/methods/chat.scheduleMessage
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "post_at": post_at,
+            "text": text,
+            "as_user": as_user,
+            "attachments": attachments,
+            "blocks": blocks,
+            "thread_ts": thread_ts,
+            "reply_broadcast": reply_broadcast,
+            "parse": parse,
+            "unfurl_links": unfurl_links,
+            "unfurl_media": unfurl_media,
+            "link_names": link_names,
+            "metadata": metadata,
+            "markdown_text": markdown_text,
+        }
+    )
+    _parse_web_class_objects(kwargs)
+    kwargs = _remove_none_values(kwargs)
+    _warn_if_message_text_content_is_missing("chat.scheduleMessage", kwargs)
+    # NOTE: intentionally using json over params for the API methods using blocks/attachments
+    return self.api_call("chat.scheduleMessage", json=kwargs)
+
+ +
+
+def chat_scheduledMessages_list(self,
*,
channel: str | None = None,
cursor: str | None = None,
latest: str | None = None,
limit: int | None = None,
oldest: str | None = None,
team_id: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def chat_scheduledMessages_list(
+    self,
+    *,
+    channel: Optional[str] = None,
+    cursor: Optional[str] = None,
+    latest: Optional[str] = None,
+    limit: Optional[int] = None,
+    oldest: Optional[str] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Lists all scheduled messages.
+    https://docs.slack.dev/reference/methods/chat.scheduledMessages.list
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "cursor": cursor,
+            "latest": latest,
+            "limit": limit,
+            "oldest": oldest,
+            "team_id": team_id,
+        }
+    )
+    return self.api_call("chat.scheduledMessages.list", params=kwargs)
+
+ +
+
+def chat_startStream(self,
*,
channel: str,
thread_ts: str,
markdown_text: str | None = None,
recipient_team_id: str | None = None,
recipient_user_id: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def chat_startStream(
+    self,
+    *,
+    channel: str,
+    thread_ts: str,
+    markdown_text: Optional[str] = None,
+    recipient_team_id: Optional[str] = None,
+    recipient_user_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Starts a new streaming conversation.
+    https://docs.slack.dev/reference/methods/chat.startStream
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "thread_ts": thread_ts,
+            "markdown_text": markdown_text,
+            "recipient_team_id": recipient_team_id,
+            "recipient_user_id": recipient_user_id,
+        }
+    )
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("chat.startStream", json=kwargs)
+
+

Starts a new streaming conversation. +https://docs.slack.dev/reference/methods/chat.startStream

+
+
+def chat_stopStream(self,
*,
channel: str,
ts: str,
markdown_text: str | None = None,
blocks: str | Sequence[Dict | Block] | None = None,
metadata: Dict | Metadata | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def chat_stopStream(
+    self,
+    *,
+    channel: str,
+    ts: str,
+    markdown_text: Optional[str] = None,
+    blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None,
+    metadata: Optional[Union[Dict, Metadata]] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Stops a streaming conversation.
+    https://docs.slack.dev/reference/methods/chat.stopStream
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "ts": ts,
+            "markdown_text": markdown_text,
+            "blocks": blocks,
+            "metadata": metadata,
+        }
+    )
+    _parse_web_class_objects(kwargs)
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("chat.stopStream", json=kwargs)
+
+ +
+
+def chat_stream(self,
*,
buffer_size: int = 256,
channel: str,
thread_ts: str,
recipient_team_id: str | None = None,
recipient_user_id: str | None = None,
**kwargs) ‑> ChatStream
+
+
+
+ +Expand source code + +
def chat_stream(
+    self,
+    *,
+    buffer_size: int = 256,
+    channel: str,
+    thread_ts: str,
+    recipient_team_id: Optional[str] = None,
+    recipient_user_id: Optional[str] = None,
+    **kwargs,
+) -> ChatStream:
+    """Stream markdown text into a conversation.
+
+    This method starts a new chat stream in a conversation that can be appended to. After appending an entire message,
+    the stream can be stopped with concluding arguments such as "blocks" for gathering feedback.
+
+    The following methods are used:
+
+    - chat.startStream: Starts a new streaming conversation.
+      [Reference](https://docs.slack.dev/reference/methods/chat.startStream).
+    - chat.appendStream: Appends text to an existing streaming conversation.
+      [Reference](https://docs.slack.dev/reference/methods/chat.appendStream).
+    - chat.stopStream: Stops a streaming conversation.
+      [Reference](https://docs.slack.dev/reference/methods/chat.stopStream).
+
+    Args:
+        buffer_size: The length of markdown_text to buffer in-memory before calling a stream method. Increasing this
+          value decreases the number of method calls made for the same amount of text, which is useful to avoid rate
+          limits. Default: 256.
+        channel: An encoded ID that represents a channel, private group, or DM.
+        thread_ts: Provide another message's ts value to reply to. Streamed messages should always be replies to a user
+          request.
+        recipient_team_id: The encoded ID of the team the user receiving the streaming text belongs to. Required when
+          streaming to channels.
+        recipient_user_id: The encoded ID of the user to receive the streaming text. Required when streaming to channels.
+        **kwargs: Additional arguments passed to the underlying API calls.
+
+    Returns:
+        ChatStream instance for managing the stream
+
+    Example:
+        ```python
+        streamer = client.chat_stream(
+            channel="C0123456789",
+            thread_ts="1700000001.123456",
+            recipient_team_id="T0123456789",
+            recipient_user_id="U0123456789",
+        )
+        streamer.append(markdown_text="**hello wo")
+        streamer.append(markdown_text="rld!**")
+        streamer.stop()
+        ```
+    """
+    return ChatStream(
+        self,
+        logger=self._logger,
+        channel=channel,
+        thread_ts=thread_ts,
+        recipient_team_id=recipient_team_id,
+        recipient_user_id=recipient_user_id,
+        buffer_size=buffer_size,
+        **kwargs,
+    )
+
+

Stream markdown text into a conversation.

+

This method starts a new chat stream in a conversation that can be appended to. After appending an entire message, +the stream can be stopped with concluding arguments such as "blocks" for gathering feedback.

+

The following methods are used:

+
    +
  • chat.startStream: Starts a new streaming conversation. +Reference.
  • +
  • chat.appendStream: Appends text to an existing streaming conversation. +Reference.
  • +
  • chat.stopStream: Stops a streaming conversation. +Reference.
  • +
+

Args

+
+
buffer_size
+
The length of markdown_text to buffer in-memory before calling a stream method. Increasing this +value decreases the number of method calls made for the same amount of text, which is useful to avoid rate +limits. Default: 256.
+
channel
+
An encoded ID that represents a channel, private group, or DM.
+
thread_ts
+
Provide another message's ts value to reply to. Streamed messages should always be replies to a user +request.
+
recipient_team_id
+
The encoded ID of the team the user receiving the streaming text belongs to. Required when +streaming to channels.
+
recipient_user_id
+
The encoded ID of the user to receive the streaming text. Required when streaming to channels.
+
**kwargs
+
Additional arguments passed to the underlying API calls.
+
+

Returns

+

ChatStream instance for managing the stream

+

Example

+
streamer = client.chat_stream(
+    channel="C0123456789",
+    thread_ts="1700000001.123456",
+    recipient_team_id="T0123456789",
+    recipient_user_id="U0123456789",
+)
+streamer.append(markdown_text="**hello wo")
+streamer.append(markdown_text="rld!**")
+streamer.stop()
+
+
+
+def chat_unfurl(self,
*,
channel: str | None = None,
ts: str | None = None,
source: str | None = None,
unfurl_id: str | None = None,
unfurls: Dict[str, Dict] | None = None,
metadata: Dict | EventAndEntityMetadata | None = None,
user_auth_blocks: str | Sequence[Dict | Block] | None = None,
user_auth_message: str | None = None,
user_auth_required: bool | None = None,
user_auth_url: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def chat_unfurl(
+    self,
+    *,
+    channel: Optional[str] = None,
+    ts: Optional[str] = None,
+    source: Optional[str] = None,
+    unfurl_id: Optional[str] = None,
+    unfurls: Optional[Dict[str, Dict]] = None,  # or user_auth_*
+    metadata: Optional[Union[Dict, EventAndEntityMetadata]] = None,
+    user_auth_blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None,
+    user_auth_message: Optional[str] = None,
+    user_auth_required: Optional[bool] = None,
+    user_auth_url: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Provide custom unfurl behavior for user-posted URLs.
+    https://docs.slack.dev/reference/methods/chat.unfurl
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "ts": ts,
+            "source": source,
+            "unfurl_id": unfurl_id,
+            "unfurls": unfurls,
+            "metadata": metadata,
+            "user_auth_blocks": user_auth_blocks,
+            "user_auth_message": user_auth_message,
+            "user_auth_required": user_auth_required,
+            "user_auth_url": user_auth_url,
+        }
+    )
+    _parse_web_class_objects(kwargs)  # for user_auth_blocks
+    kwargs = _remove_none_values(kwargs)
+    # NOTE: intentionally using json over params for API methods using blocks/attachments
+    return self.api_call("chat.unfurl", json=kwargs)
+
+

Provide custom unfurl behavior for user-posted URLs. +https://docs.slack.dev/reference/methods/chat.unfurl

+
+
+def chat_update(self,
*,
channel: str,
ts: str,
text: str | None = None,
attachments: str | Sequence[Dict | Attachment] | None = None,
blocks: str | Sequence[Dict | Block] | None = None,
as_user: bool | None = None,
file_ids: str | Sequence[str] | None = None,
link_names: bool | None = None,
parse: str | None = None,
reply_broadcast: bool | None = None,
metadata: Dict | Metadata | None = None,
markdown_text: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def chat_update(
+    self,
+    *,
+    channel: str,
+    ts: str,
+    text: Optional[str] = None,
+    attachments: Optional[Union[str, Sequence[Union[Dict, Attachment]]]] = None,
+    blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None,
+    as_user: Optional[bool] = None,
+    file_ids: Optional[Union[str, Sequence[str]]] = None,
+    link_names: Optional[bool] = None,
+    parse: Optional[str] = None,  # none, full
+    reply_broadcast: Optional[bool] = None,
+    metadata: Optional[Union[Dict, Metadata]] = None,
+    markdown_text: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Updates a message in a channel.
+    https://docs.slack.dev/reference/methods/chat.update
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "ts": ts,
+            "text": text,
+            "attachments": attachments,
+            "blocks": blocks,
+            "as_user": as_user,
+            "link_names": link_names,
+            "parse": parse,
+            "reply_broadcast": reply_broadcast,
+            "metadata": metadata,
+            "markdown_text": markdown_text,
+        }
+    )
+    if isinstance(file_ids, (list, tuple)):
+        kwargs.update({"file_ids": ",".join(file_ids)})
+    else:
+        kwargs.update({"file_ids": file_ids})
+    _parse_web_class_objects(kwargs)
+    kwargs = _remove_none_values(kwargs)
+    _warn_if_message_text_content_is_missing("chat.update", kwargs)
+    # NOTE: intentionally using json over params for API methods using blocks/attachments
+    return self.api_call("chat.update", json=kwargs)
+
+ +
+
+def conversations_acceptSharedInvite(self,
*,
channel_name: str,
channel_id: str | None = None,
invite_id: str | None = None,
free_trial_accepted: bool | None = None,
is_private: bool | None = None,
team_id: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def conversations_acceptSharedInvite(
+    self,
+    *,
+    channel_name: str,
+    channel_id: Optional[str] = None,
+    invite_id: Optional[str] = None,
+    free_trial_accepted: Optional[bool] = None,
+    is_private: Optional[bool] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Accepts an invitation to a Slack Connect channel.
+    https://docs.slack.dev/reference/methods/conversations.acceptSharedInvite
+    """
+    if channel_id is None and invite_id is None:
+        raise e.SlackRequestError("Either channel_id or invite_id must be provided.")
+    kwargs.update(
+        {
+            "channel_name": channel_name,
+            "channel_id": channel_id,
+            "invite_id": invite_id,
+            "free_trial_accepted": free_trial_accepted,
+            "is_private": is_private,
+            "team_id": team_id,
+        }
+    )
+    return self.api_call("conversations.acceptSharedInvite", http_verb="POST", params=kwargs)
+
+

Accepts an invitation to a Slack Connect channel. +https://docs.slack.dev/reference/methods/conversations.acceptSharedInvite

+
+
+def conversations_approveSharedInvite(self, *, invite_id: str, target_team: str | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def conversations_approveSharedInvite(
+    self,
+    *,
+    invite_id: str,
+    target_team: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Approves an invitation to a Slack Connect channel.
+    https://docs.slack.dev/reference/methods/conversations.approveSharedInvite
+    """
+    kwargs.update({"invite_id": invite_id, "target_team": target_team})
+    return self.api_call("conversations.approveSharedInvite", http_verb="POST", params=kwargs)
+
+

Approves an invitation to a Slack Connect channel. +https://docs.slack.dev/reference/methods/conversations.approveSharedInvite

+
+
+def conversations_archive(self, *, channel: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def conversations_archive(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> SlackResponse:
+    """Archives a conversation.
+    https://docs.slack.dev/reference/methods/conversations.archive
+    """
+    kwargs.update({"channel": channel})
+    return self.api_call("conversations.archive", params=kwargs)
+
+ +
+
+def conversations_canvases_create(self, *, channel_id: str, document_content: Dict[str, str], **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def conversations_canvases_create(
+    self,
+    *,
+    channel_id: str,
+    document_content: Dict[str, str],
+    **kwargs,
+) -> SlackResponse:
+    """Create a Channel Canvas for a channel
+    https://docs.slack.dev/reference/methods/conversations.canvases.create
+    """
+    kwargs.update({"channel_id": channel_id, "document_content": document_content})
+    return self.api_call("conversations.canvases.create", json=kwargs)
+
+ +
+
+def conversations_close(self, *, channel: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def conversations_close(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> SlackResponse:
+    """Closes a direct message or multi-person direct message.
+    https://docs.slack.dev/reference/methods/conversations.close
+    """
+    kwargs.update({"channel": channel})
+    return self.api_call("conversations.close", params=kwargs)
+
+

Closes a direct message or multi-person direct message. +https://docs.slack.dev/reference/methods/conversations.close

+
+
+def conversations_create(self,
*,
name: str,
is_private: bool | None = None,
team_id: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def conversations_create(
+    self,
+    *,
+    name: str,
+    is_private: Optional[bool] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Initiates a public or private channel-based conversation
+    https://docs.slack.dev/reference/methods/conversations.create
+    """
+    kwargs.update({"name": name, "is_private": is_private, "team_id": team_id})
+    return self.api_call("conversations.create", params=kwargs)
+
+

Initiates a public or private channel-based conversation +https://docs.slack.dev/reference/methods/conversations.create

+
+
+def conversations_declineSharedInvite(self, *, invite_id: str, target_team: str | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def conversations_declineSharedInvite(
+    self,
+    *,
+    invite_id: str,
+    target_team: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Declines a Slack Connect channel invite.
+    https://docs.slack.dev/reference/methods/conversations.declineSharedInvite
+    """
+    kwargs.update({"invite_id": invite_id, "target_team": target_team})
+    return self.api_call("conversations.declineSharedInvite", http_verb="GET", params=kwargs)
+
+ +
+
+def conversations_externalInvitePermissions_set(self, *, action: str, channel: str, target_team: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def conversations_externalInvitePermissions_set(
+    self, *, action: str, channel: str, target_team: str, **kwargs
+) -> SlackResponse:
+    """Sets a team in a shared External Limited channel to a shared Slack Connect channel or vice versa.
+    https://docs.slack.dev/reference/methods/conversations.externalInvitePermissions.set
+    """
+    kwargs.update(
+        {
+            "action": action,
+            "channel": channel,
+            "target_team": target_team,
+        }
+    )
+    return self.api_call("conversations.externalInvitePermissions.set", params=kwargs)
+
+

Sets a team in a shared External Limited channel to a shared Slack Connect channel or vice versa. +https://docs.slack.dev/reference/methods/conversations.externalInvitePermissions.set

+
+
+def conversations_history(self,
*,
channel: str,
cursor: str | None = None,
inclusive: bool | None = None,
include_all_metadata: bool | None = None,
latest: str | None = None,
limit: int | None = None,
oldest: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def conversations_history(
+    self,
+    *,
+    channel: str,
+    cursor: Optional[str] = None,
+    inclusive: Optional[bool] = None,
+    include_all_metadata: Optional[bool] = None,
+    latest: Optional[str] = None,
+    limit: Optional[int] = None,
+    oldest: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Fetches a conversation's history of messages and events.
+    https://docs.slack.dev/reference/methods/conversations.history
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "cursor": cursor,
+            "inclusive": inclusive,
+            "include_all_metadata": include_all_metadata,
+            "limit": limit,
+            "latest": latest,
+            "oldest": oldest,
+        }
+    )
+    return self.api_call("conversations.history", http_verb="GET", params=kwargs)
+
+

Fetches a conversation's history of messages and events. +https://docs.slack.dev/reference/methods/conversations.history

+
+
+def conversations_info(self,
*,
channel: str,
include_locale: bool | None = None,
include_num_members: bool | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def conversations_info(
+    self,
+    *,
+    channel: str,
+    include_locale: Optional[bool] = None,
+    include_num_members: Optional[bool] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Retrieve information about a conversation.
+    https://docs.slack.dev/reference/methods/conversations.info
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "include_locale": include_locale,
+            "include_num_members": include_num_members,
+        }
+    )
+    return self.api_call("conversations.info", http_verb="GET", params=kwargs)
+
+

Retrieve information about a conversation. +https://docs.slack.dev/reference/methods/conversations.info

+
+
+def conversations_invite(self,
*,
channel: str,
users: str | Sequence[str],
force: bool | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def conversations_invite(
+    self,
+    *,
+    channel: str,
+    users: Union[str, Sequence[str]],
+    force: Optional[bool] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Invites users to a channel.
+    https://docs.slack.dev/reference/methods/conversations.invite
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "force": force,
+        }
+    )
+    if isinstance(users, (list, tuple)):
+        kwargs.update({"users": ",".join(users)})
+    else:
+        kwargs.update({"users": users})
+    return self.api_call("conversations.invite", params=kwargs)
+
+ +
+
+def conversations_inviteShared(self,
*,
channel: str,
emails: str | Sequence[str] | None = None,
user_ids: str | Sequence[str] | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def conversations_inviteShared(
+    self,
+    *,
+    channel: str,
+    emails: Optional[Union[str, Sequence[str]]] = None,
+    user_ids: Optional[Union[str, Sequence[str]]] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Sends an invitation to a Slack Connect channel.
+    https://docs.slack.dev/reference/methods/conversations.inviteShared
+    """
+    if emails is None and user_ids is None:
+        raise e.SlackRequestError("Either emails or user ids must be provided.")
+    kwargs.update({"channel": channel})
+    if isinstance(emails, (list, tuple)):
+        kwargs.update({"emails": ",".join(emails)})
+    else:
+        kwargs.update({"emails": emails})
+    if isinstance(user_ids, (list, tuple)):
+        kwargs.update({"user_ids": ",".join(user_ids)})
+    else:
+        kwargs.update({"user_ids": user_ids})
+    return self.api_call("conversations.inviteShared", http_verb="GET", params=kwargs)
+
+

Sends an invitation to a Slack Connect channel. +https://docs.slack.dev/reference/methods/conversations.inviteShared

+
+
+def conversations_join(self, *, channel: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def conversations_join(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> SlackResponse:
+    """Joins an existing conversation.
+    https://docs.slack.dev/reference/methods/conversations.join
+    """
+    kwargs.update({"channel": channel})
+    return self.api_call("conversations.join", params=kwargs)
+
+ +
+
+def conversations_kick(self, *, channel: str, user: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def conversations_kick(
+    self,
+    *,
+    channel: str,
+    user: str,
+    **kwargs,
+) -> SlackResponse:
+    """Removes a user from a conversation.
+    https://docs.slack.dev/reference/methods/conversations.kick
+    """
+    kwargs.update({"channel": channel, "user": user})
+    return self.api_call("conversations.kick", params=kwargs)
+
+ +
+
+def conversations_leave(self, *, channel: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def conversations_leave(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> SlackResponse:
+    """Leaves a conversation.
+    https://docs.slack.dev/reference/methods/conversations.leave
+    """
+    kwargs.update({"channel": channel})
+    return self.api_call("conversations.leave", params=kwargs)
+
+ +
+
+def conversations_list(self,
*,
cursor: str | None = None,
exclude_archived: bool | None = None,
limit: int | None = None,
team_id: str | None = None,
types: str | Sequence[str] | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def conversations_list(
+    self,
+    *,
+    cursor: Optional[str] = None,
+    exclude_archived: Optional[bool] = None,
+    limit: Optional[int] = None,
+    team_id: Optional[str] = None,
+    types: Optional[Union[str, Sequence[str]]] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Lists all channels in a Slack team.
+    https://docs.slack.dev/reference/methods/conversations.list
+    """
+    kwargs.update(
+        {
+            "cursor": cursor,
+            "exclude_archived": exclude_archived,
+            "limit": limit,
+            "team_id": team_id,
+        }
+    )
+    if isinstance(types, (list, tuple)):
+        kwargs.update({"types": ",".join(types)})
+    else:
+        kwargs.update({"types": types})
+    return self.api_call("conversations.list", http_verb="GET", params=kwargs)
+
+ +
+
+def conversations_listConnectInvites(self,
*,
count: int | None = None,
cursor: str | None = None,
team_id: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def conversations_listConnectInvites(
+    self,
+    *,
+    count: Optional[int] = None,
+    cursor: Optional[str] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """List shared channel invites that have been generated
+    or received but have not yet been approved by all parties.
+    https://docs.slack.dev/reference/methods/conversations.listConnectInvites
+    """
+    kwargs.update({"count": count, "cursor": cursor, "team_id": team_id})
+    return self.api_call("conversations.listConnectInvites", params=kwargs)
+
+

List shared channel invites that have been generated +or received but have not yet been approved by all parties. +https://docs.slack.dev/reference/methods/conversations.listConnectInvites

+
+
+def conversations_mark(self, *, channel: str, ts: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def conversations_mark(
+    self,
+    *,
+    channel: str,
+    ts: str,
+    **kwargs,
+) -> SlackResponse:
+    """Sets the read cursor in a channel.
+    https://docs.slack.dev/reference/methods/conversations.mark
+    """
+    kwargs.update({"channel": channel, "ts": ts})
+    return self.api_call("conversations.mark", params=kwargs)
+
+ +
+
+def conversations_members(self, *, channel: str, cursor: str | None = None, limit: int | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def conversations_members(
+    self,
+    *,
+    channel: str,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Retrieve members of a conversation.
+    https://docs.slack.dev/reference/methods/conversations.members
+    """
+    kwargs.update({"channel": channel, "cursor": cursor, "limit": limit})
+    return self.api_call("conversations.members", http_verb="GET", params=kwargs)
+
+ +
+
+def conversations_open(self,
*,
channel: str | None = None,
return_im: bool | None = None,
users: str | Sequence[str] | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def conversations_open(
+    self,
+    *,
+    channel: Optional[str] = None,
+    return_im: Optional[bool] = None,
+    users: Optional[Union[str, Sequence[str]]] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Opens or resumes a direct message or multi-person direct message.
+    https://docs.slack.dev/reference/methods/conversations.open
+    """
+    if channel is None and users is None:
+        raise e.SlackRequestError("Either channel or users must be provided.")
+    kwargs.update({"channel": channel, "return_im": return_im})
+    if isinstance(users, (list, tuple)):
+        kwargs.update({"users": ",".join(users)})
+    else:
+        kwargs.update({"users": users})
+    return self.api_call("conversations.open", params=kwargs)
+
+

Opens or resumes a direct message or multi-person direct message. +https://docs.slack.dev/reference/methods/conversations.open

+
+
+def conversations_rename(self, *, channel: str, name: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def conversations_rename(
+    self,
+    *,
+    channel: str,
+    name: str,
+    **kwargs,
+) -> SlackResponse:
+    """Renames a conversation.
+    https://docs.slack.dev/reference/methods/conversations.rename
+    """
+    kwargs.update({"channel": channel, "name": name})
+    return self.api_call("conversations.rename", params=kwargs)
+
+ +
+
+def conversations_replies(self,
*,
channel: str,
ts: str,
cursor: str | None = None,
inclusive: bool | None = None,
include_all_metadata: bool | None = None,
latest: str | None = None,
limit: int | None = None,
oldest: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def conversations_replies(
+    self,
+    *,
+    channel: str,
+    ts: str,
+    cursor: Optional[str] = None,
+    inclusive: Optional[bool] = None,
+    include_all_metadata: Optional[bool] = None,
+    latest: Optional[str] = None,
+    limit: Optional[int] = None,
+    oldest: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Retrieve a thread of messages posted to a conversation
+    https://docs.slack.dev/reference/methods/conversations.replies
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "ts": ts,
+            "cursor": cursor,
+            "inclusive": inclusive,
+            "include_all_metadata": include_all_metadata,
+            "limit": limit,
+            "latest": latest,
+            "oldest": oldest,
+        }
+    )
+    return self.api_call("conversations.replies", http_verb="GET", params=kwargs)
+
+

Retrieve a thread of messages posted to a conversation +https://docs.slack.dev/reference/methods/conversations.replies

+
+
+def conversations_requestSharedInvite_approve(self,
*,
invite_id: str,
channel_id: str | None = None,
is_external_limited: str | None = None,
message: Dict[str, Any] | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def conversations_requestSharedInvite_approve(
+    self,
+    *,
+    invite_id: str,
+    channel_id: Optional[str] = None,
+    is_external_limited: Optional[str] = None,
+    message: Optional[Dict[str, Any]] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Approve a request to add an external user to a channel. This also sends them a Slack Connect invite.
+    https://docs.slack.dev/reference/methods/conversations.requestSharedInvite.approve
+    """
+    kwargs.update(
+        {
+            "invite_id": invite_id,
+            "channel_id": channel_id,
+            "is_external_limited": is_external_limited,
+        }
+    )
+    if message is not None:
+        kwargs.update({"message": json.dumps(message)})
+    return self.api_call("conversations.requestSharedInvite.approve", params=kwargs)
+
+

Approve a request to add an external user to a channel. This also sends them a Slack Connect invite. +https://docs.slack.dev/reference/methods/conversations.requestSharedInvite.approve

+
+
+def conversations_requestSharedInvite_deny(self, *, invite_id: str, message: str | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def conversations_requestSharedInvite_deny(
+    self,
+    *,
+    invite_id: str,
+    message: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Deny a request to invite an external user to a channel.
+    https://docs.slack.dev/reference/methods/conversations.requestSharedInvite.deny
+    """
+    kwargs.update({"invite_id": invite_id, "message": message})
+    return self.api_call("conversations.requestSharedInvite.deny", params=kwargs)
+
+

Deny a request to invite an external user to a channel. +https://docs.slack.dev/reference/methods/conversations.requestSharedInvite.deny

+
+
+def conversations_requestSharedInvite_list(self,
*,
cursor: str | None = None,
include_approved: bool | None = None,
include_denied: bool | None = None,
include_expired: bool | None = None,
invite_ids: str | Sequence[str] | None = None,
limit: int | None = None,
user_id: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def conversations_requestSharedInvite_list(
+    self,
+    *,
+    cursor: Optional[str] = None,
+    include_approved: Optional[bool] = None,
+    include_denied: Optional[bool] = None,
+    include_expired: Optional[bool] = None,
+    invite_ids: Optional[Union[str, Sequence[str]]] = None,
+    limit: Optional[int] = None,
+    user_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Lists requests to add external users to channels with ability to filter.
+    https://docs.slack.dev/reference/methods/conversations.requestSharedInvite.list
+    """
+    kwargs.update(
+        {
+            "cursor": cursor,
+            "include_approved": include_approved,
+            "include_denied": include_denied,
+            "include_expired": include_expired,
+            "limit": limit,
+            "user_id": user_id,
+        }
+    )
+    if invite_ids is not None:
+        if isinstance(invite_ids, (list, tuple)):
+            kwargs.update({"invite_ids": ",".join(invite_ids)})
+        else:
+            kwargs.update({"invite_ids": invite_ids})
+    return self.api_call("conversations.requestSharedInvite.list", params=kwargs)
+
+

Lists requests to add external users to channels with ability to filter. +https://docs.slack.dev/reference/methods/conversations.requestSharedInvite.list

+
+
+def conversations_setPurpose(self, *, channel: str, purpose: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def conversations_setPurpose(
+    self,
+    *,
+    channel: str,
+    purpose: str,
+    **kwargs,
+) -> SlackResponse:
+    """Sets the purpose for a conversation.
+    https://docs.slack.dev/reference/methods/conversations.setPurpose
+    """
+    kwargs.update({"channel": channel, "purpose": purpose})
+    return self.api_call("conversations.setPurpose", params=kwargs)
+
+ +
+
+def conversations_setTopic(self, *, channel: str, topic: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def conversations_setTopic(
+    self,
+    *,
+    channel: str,
+    topic: str,
+    **kwargs,
+) -> SlackResponse:
+    """Sets the topic for a conversation.
+    https://docs.slack.dev/reference/methods/conversations.setTopic
+    """
+    kwargs.update({"channel": channel, "topic": topic})
+    return self.api_call("conversations.setTopic", params=kwargs)
+
+ +
+
+def conversations_unarchive(self, *, channel: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def conversations_unarchive(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> SlackResponse:
+    """Reverses conversation archival.
+    https://docs.slack.dev/reference/methods/conversations.unarchive
+    """
+    kwargs.update({"channel": channel})
+    return self.api_call("conversations.unarchive", params=kwargs)
+
+ +
+
+def dialog_open(self, *, dialog: Dict[str, Any], trigger_id: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def dialog_open(
+    self,
+    *,
+    dialog: Dict[str, Any],
+    trigger_id: str,
+    **kwargs,
+) -> SlackResponse:
+    """Open a dialog with a user.
+    https://docs.slack.dev/reference/methods/dialog.open
+    """
+    kwargs.update({"dialog": dialog, "trigger_id": trigger_id})
+    kwargs = _remove_none_values(kwargs)
+    # NOTE: As the dialog can be a dict, this API call works only with json format.
+    return self.api_call("dialog.open", json=kwargs)
+
+ +
+
+def dnd_endDnd(self, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def dnd_endDnd(
+    self,
+    **kwargs,
+) -> SlackResponse:
+    """Ends the current user's Do Not Disturb session immediately.
+    https://docs.slack.dev/reference/methods/dnd.endDnd
+    """
+    return self.api_call("dnd.endDnd", params=kwargs)
+
+

Ends the current user's Do Not Disturb session immediately. +https://docs.slack.dev/reference/methods/dnd.endDnd

+
+
+def dnd_endSnooze(self, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def dnd_endSnooze(
+    self,
+    **kwargs,
+) -> SlackResponse:
+    """Ends the current user's snooze mode immediately.
+    https://docs.slack.dev/reference/methods/dnd.endSnooze
+    """
+    return self.api_call("dnd.endSnooze", params=kwargs)
+
+

Ends the current user's snooze mode immediately. +https://docs.slack.dev/reference/methods/dnd.endSnooze

+
+
+def dnd_info(self, *, team_id: str | None = None, user: str | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def dnd_info(
+    self,
+    *,
+    team_id: Optional[str] = None,
+    user: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Retrieves a user's current Do Not Disturb status.
+    https://docs.slack.dev/reference/methods/dnd.info
+    """
+    kwargs.update({"team_id": team_id, "user": user})
+    return self.api_call("dnd.info", http_verb="GET", params=kwargs)
+
+

Retrieves a user's current Do Not Disturb status. +https://docs.slack.dev/reference/methods/dnd.info

+
+
+def dnd_setSnooze(self, *, num_minutes: str | int, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def dnd_setSnooze(
+    self,
+    *,
+    num_minutes: Union[int, str],
+    **kwargs,
+) -> SlackResponse:
+    """Turns on Do Not Disturb mode for the current user, or changes its duration.
+    https://docs.slack.dev/reference/methods/dnd.setSnooze
+    """
+    kwargs.update({"num_minutes": num_minutes})
+    return self.api_call("dnd.setSnooze", http_verb="GET", params=kwargs)
+
+

Turns on Do Not Disturb mode for the current user, or changes its duration. +https://docs.slack.dev/reference/methods/dnd.setSnooze

+
+
+def dnd_teamInfo(self, users: str | Sequence[str], team_id: str | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def dnd_teamInfo(
+    self,
+    users: Union[str, Sequence[str]],
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Retrieves the Do Not Disturb status for users on a team.
+    https://docs.slack.dev/reference/methods/dnd.teamInfo
+    """
+    if isinstance(users, (list, tuple)):
+        kwargs.update({"users": ",".join(users)})
+    else:
+        kwargs.update({"users": users})
+    kwargs.update({"team_id": team_id})
+    return self.api_call("dnd.teamInfo", http_verb="GET", params=kwargs)
+
+

Retrieves the Do Not Disturb status for users on a team. +https://docs.slack.dev/reference/methods/dnd.teamInfo

+
+
+def emoji_list(self, include_categories: bool | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def emoji_list(
+    self,
+    include_categories: Optional[bool] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Lists custom emoji for a team.
+    https://docs.slack.dev/reference/methods/emoji.list
+    """
+    kwargs.update({"include_categories": include_categories})
+    return self.api_call("emoji.list", http_verb="GET", params=kwargs)
+
+ +
+
+def entity_presentDetails(self,
trigger_id: str,
metadata: Dict | EntityMetadata | None = None,
user_auth_required: bool | None = None,
user_auth_url: str | None = None,
error: Dict[str, Any] | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def entity_presentDetails(
+    self,
+    trigger_id: str,
+    metadata: Optional[Union[Dict, EntityMetadata]] = None,
+    user_auth_required: Optional[bool] = None,
+    user_auth_url: Optional[str] = None,
+    error: Optional[Dict[str, Any]] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Provides entity details for the flexpane.
+    https://docs.slack.dev/reference/methods/entity.presentDetails/
+    """
+    kwargs.update({"trigger_id": trigger_id})
+    if metadata is not None:
+        kwargs.update({"metadata": metadata})
+    if user_auth_required is not None:
+        kwargs.update({"user_auth_required": user_auth_required})
+    if user_auth_url is not None:
+        kwargs.update({"user_auth_url": user_auth_url})
+    if error is not None:
+        kwargs.update({"error": error})
+    _parse_web_class_objects(kwargs)
+    return self.api_call("entity.presentDetails", json=kwargs)
+
+

Provides entity details for the flexpane. +https://docs.slack.dev/reference/methods/entity.presentDetails/

+
+
+def files_comments_delete(self, *, file: str, id: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def files_comments_delete(
+    self,
+    *,
+    file: str,
+    id: str,
+    **kwargs,
+) -> SlackResponse:
+    """Deletes an existing comment on a file.
+    https://docs.slack.dev/reference/methods/files.comments.delete
+    """
+    kwargs.update({"file": file, "id": id})
+    return self.api_call("files.comments.delete", params=kwargs)
+
+ +
+
+def files_completeUploadExternal(self,
*,
files: List[Dict[str, str]],
channel_id: str | None = None,
channels: List[str] | None = None,
initial_comment: str | None = None,
thread_ts: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def files_completeUploadExternal(
+    self,
+    *,
+    files: List[Dict[str, str]],
+    channel_id: Optional[str] = None,
+    channels: Optional[List[str]] = None,
+    initial_comment: Optional[str] = None,
+    thread_ts: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Finishes an upload started with files.getUploadURLExternal.
+    https://docs.slack.dev/reference/methods/files.completeUploadExternal
+    """
+    _files = [{k: v for k, v in f.items() if v is not None} for f in files]
+    kwargs.update(
+        {
+            "files": json.dumps(_files),
+            "channel_id": channel_id,
+            "initial_comment": initial_comment,
+            "thread_ts": thread_ts,
+        }
+    )
+    if channels:
+        kwargs["channels"] = ",".join(channels)
+    return self.api_call("files.completeUploadExternal", params=kwargs)
+
+

Finishes an upload started with files.getUploadURLExternal. +https://docs.slack.dev/reference/methods/files.completeUploadExternal

+
+
+def files_delete(self, *, file: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def files_delete(
+    self,
+    *,
+    file: str,
+    **kwargs,
+) -> SlackResponse:
+    """Deletes a file.
+    https://docs.slack.dev/reference/methods/files.delete
+    """
+    kwargs.update({"file": file})
+    return self.api_call("files.delete", params=kwargs)
+
+ +
+
+def files_getUploadURLExternal(self,
*,
filename: str,
length: int,
alt_txt: str | None = None,
snippet_type: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def files_getUploadURLExternal(
+    self,
+    *,
+    filename: str,
+    length: int,
+    alt_txt: Optional[str] = None,
+    snippet_type: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Gets a URL for an edge external upload.
+    https://docs.slack.dev/reference/methods/files.getUploadURLExternal
+    """
+    kwargs.update(
+        {
+            "filename": filename,
+            "length": length,
+            "alt_txt": alt_txt,
+            "snippet_type": snippet_type,
+        }
+    )
+    return self.api_call("files.getUploadURLExternal", params=kwargs)
+
+ +
+
+def files_info(self,
*,
file: str,
count: int | None = None,
cursor: str | None = None,
limit: int | None = None,
page: int | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def files_info(
+    self,
+    *,
+    file: str,
+    count: Optional[int] = None,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    page: Optional[int] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Gets information about a team file.
+    https://docs.slack.dev/reference/methods/files.info
+    """
+    kwargs.update(
+        {
+            "file": file,
+            "count": count,
+            "cursor": cursor,
+            "limit": limit,
+            "page": page,
+        }
+    )
+    return self.api_call("files.info", http_verb="GET", params=kwargs)
+
+

Gets information about a team file. +https://docs.slack.dev/reference/methods/files.info

+
+
+def files_list(self,
*,
channel: str | None = None,
count: int | None = None,
page: int | None = None,
show_files_hidden_by_limit: bool | None = None,
team_id: str | None = None,
ts_from: str | None = None,
ts_to: str | None = None,
types: str | Sequence[str] | None = None,
user: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def files_list(
+    self,
+    *,
+    channel: Optional[str] = None,
+    count: Optional[int] = None,
+    page: Optional[int] = None,
+    show_files_hidden_by_limit: Optional[bool] = None,
+    team_id: Optional[str] = None,
+    ts_from: Optional[str] = None,
+    ts_to: Optional[str] = None,
+    types: Optional[Union[str, Sequence[str]]] = None,
+    user: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Lists & filters team files.
+    https://docs.slack.dev/reference/methods/files.list
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "count": count,
+            "page": page,
+            "show_files_hidden_by_limit": show_files_hidden_by_limit,
+            "team_id": team_id,
+            "ts_from": ts_from,
+            "ts_to": ts_to,
+            "user": user,
+        }
+    )
+    if isinstance(types, (list, tuple)):
+        kwargs.update({"types": ",".join(types)})
+    else:
+        kwargs.update({"types": types})
+    return self.api_call("files.list", http_verb="GET", params=kwargs)
+
+ +
+
+def files_remote_add(self,
*,
external_id: str,
external_url: str,
title: str,
filetype: str | None = None,
indexable_file_contents: str | bytes | io.IOBase | None = None,
preview_image: str | bytes | io.IOBase | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def files_remote_add(
+    self,
+    *,
+    external_id: str,
+    external_url: str,
+    title: str,
+    filetype: Optional[str] = None,
+    indexable_file_contents: Optional[Union[str, bytes, IOBase]] = None,
+    preview_image: Optional[Union[str, bytes, IOBase]] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Adds a file from a remote service.
+    https://docs.slack.dev/reference/methods/files.remote.add
+    """
+    kwargs.update(
+        {
+            "external_id": external_id,
+            "external_url": external_url,
+            "title": title,
+            "filetype": filetype,
+        }
+    )
+    files = None
+    # preview_image (file): Preview of the document via multipart/form-data.
+    if preview_image is not None or indexable_file_contents is not None:
+        files = {
+            "preview_image": preview_image,
+            "indexable_file_contents": indexable_file_contents,
+        }
+
+    return self.api_call(
+        # Intentionally using "POST" method over "GET" here
+        "files.remote.add",
+        http_verb="POST",
+        data=kwargs,
+        files=files,
+    )
+
+ +
+
+def files_remote_info(self, *, external_id: str | None = None, file: str | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def files_remote_info(
+    self,
+    *,
+    external_id: Optional[str] = None,
+    file: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Retrieve information about a remote file added to Slack.
+    https://docs.slack.dev/reference/methods/files.remote.info
+    """
+    kwargs.update({"external_id": external_id, "file": file})
+    return self.api_call("files.remote.info", http_verb="GET", params=kwargs)
+
+

Retrieve information about a remote file added to Slack. +https://docs.slack.dev/reference/methods/files.remote.info

+
+
+def files_remote_list(self,
*,
channel: str | None = None,
cursor: str | None = None,
limit: int | None = None,
ts_from: str | None = None,
ts_to: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def files_remote_list(
+    self,
+    *,
+    channel: Optional[str] = None,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    ts_from: Optional[str] = None,
+    ts_to: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Retrieve information about a remote file added to Slack.
+    https://docs.slack.dev/reference/methods/files.remote.list
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "cursor": cursor,
+            "limit": limit,
+            "ts_from": ts_from,
+            "ts_to": ts_to,
+        }
+    )
+    return self.api_call("files.remote.list", http_verb="GET", params=kwargs)
+
+

Retrieve information about a remote file added to Slack. +https://docs.slack.dev/reference/methods/files.remote.list

+
+
+def files_remote_remove(self, *, external_id: str | None = None, file: str | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def files_remote_remove(
+    self,
+    *,
+    external_id: Optional[str] = None,
+    file: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Remove a remote file.
+    https://docs.slack.dev/reference/methods/files.remote.remove
+    """
+    kwargs.update({"external_id": external_id, "file": file})
+    return self.api_call("files.remote.remove", http_verb="POST", params=kwargs)
+
+ +
+
+def files_remote_share(self,
*,
channels: str | Sequence[str],
external_id: str | None = None,
file: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def files_remote_share(
+    self,
+    *,
+    channels: Union[str, Sequence[str]],
+    external_id: Optional[str] = None,
+    file: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Share a remote file into a channel.
+    https://docs.slack.dev/reference/methods/files.remote.share
+    """
+    if external_id is None and file is None:
+        raise e.SlackRequestError("Either external_id or file must be provided.")
+    if isinstance(channels, (list, tuple)):
+        kwargs.update({"channels": ",".join(channels)})
+    else:
+        kwargs.update({"channels": channels})
+    kwargs.update({"external_id": external_id, "file": file})
+    return self.api_call("files.remote.share", http_verb="GET", params=kwargs)
+
+ +
+
+def files_remote_update(self,
*,
external_id: str | None = None,
external_url: str | None = None,
file: str | None = None,
title: str | None = None,
filetype: str | None = None,
indexable_file_contents: str | None = None,
preview_image: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def files_remote_update(
+    self,
+    *,
+    external_id: Optional[str] = None,
+    external_url: Optional[str] = None,
+    file: Optional[str] = None,
+    title: Optional[str] = None,
+    filetype: Optional[str] = None,
+    indexable_file_contents: Optional[str] = None,
+    preview_image: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Updates an existing remote file.
+    https://docs.slack.dev/reference/methods/files.remote.update
+    """
+    kwargs.update(
+        {
+            "external_id": external_id,
+            "external_url": external_url,
+            "file": file,
+            "title": title,
+            "filetype": filetype,
+        }
+    )
+    files = None
+    # preview_image (file): Preview of the document via multipart/form-data.
+    if preview_image is not None or indexable_file_contents is not None:
+        files = {
+            "preview_image": preview_image,
+            "indexable_file_contents": indexable_file_contents,
+        }
+
+    return self.api_call(
+        # Intentionally using "POST" method over "GET" here
+        "files.remote.update",
+        http_verb="POST",
+        data=kwargs,
+        files=files,
+    )
+
+ +
+
+def files_revokePublicURL(self, *, file: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def files_revokePublicURL(
+    self,
+    *,
+    file: str,
+    **kwargs,
+) -> SlackResponse:
+    """Revokes public/external sharing access for a file
+    https://docs.slack.dev/reference/methods/files.revokePublicURL
+    """
+    kwargs.update({"file": file})
+    return self.api_call("files.revokePublicURL", params=kwargs)
+
+

Revokes public/external sharing access for a file +https://docs.slack.dev/reference/methods/files.revokePublicURL

+
+
+def files_sharedPublicURL(self, *, file: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def files_sharedPublicURL(
+    self,
+    *,
+    file: str,
+    **kwargs,
+) -> SlackResponse:
+    """Enables a file for public/external sharing.
+    https://docs.slack.dev/reference/methods/files.sharedPublicURL
+    """
+    kwargs.update({"file": file})
+    return self.api_call("files.sharedPublicURL", params=kwargs)
+
+

Enables a file for public/external sharing. +https://docs.slack.dev/reference/methods/files.sharedPublicURL

+
+
+def files_upload(self,
*,
file: str | bytes | io.IOBase | None = None,
content: str | bytes | None = None,
filename: str | None = None,
filetype: str | None = None,
initial_comment: str | None = None,
thread_ts: str | None = None,
title: str | None = None,
channels: str | Sequence[str] | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def files_upload(
+    self,
+    *,
+    file: Optional[Union[str, bytes, IOBase]] = None,
+    content: Optional[Union[str, bytes]] = None,
+    filename: Optional[str] = None,
+    filetype: Optional[str] = None,
+    initial_comment: Optional[str] = None,
+    thread_ts: Optional[str] = None,
+    title: Optional[str] = None,
+    channels: Optional[Union[str, Sequence[str]]] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Uploads or creates a file.
+    https://docs.slack.dev/reference/methods/files.upload
+    """
+    _print_files_upload_v2_suggestion()
+
+    if file is None and content is None:
+        raise e.SlackRequestError("The file or content argument must be specified.")
+    if file is not None and content is not None:
+        raise e.SlackRequestError("You cannot specify both the file and the content argument.")
+
+    if isinstance(channels, (list, tuple)):
+        kwargs.update({"channels": ",".join(channels)})
+    else:
+        kwargs.update({"channels": channels})
+    kwargs.update(
+        {
+            "filename": filename,
+            "filetype": filetype,
+            "initial_comment": initial_comment,
+            "thread_ts": thread_ts,
+            "title": title,
+        }
+    )
+    if file:
+        if kwargs.get("filename") is None and isinstance(file, str):
+            # use the local filename if filename is missing
+            if kwargs.get("filename") is None:
+                kwargs["filename"] = file.split(os.path.sep)[-1]
+        return self.api_call("files.upload", files={"file": file}, data=kwargs)
+    else:
+        kwargs["content"] = content
+        return self.api_call("files.upload", data=kwargs)
+
+ +
+
+def files_upload_v2(self,
*,
filename: str | None = None,
file: str | bytes | io.IOBase | os.PathLike | None = None,
content: str | bytes | None = None,
title: str | None = None,
alt_txt: str | None = None,
snippet_type: str | None = None,
file_uploads: List[Dict[str, Any]] | None = None,
channel: str | None = None,
channels: List[str] | None = None,
initial_comment: str | None = None,
thread_ts: str | None = None,
request_file_info: bool = True,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def files_upload_v2(
+    self,
+    *,
+    # for sending a single file
+    filename: Optional[str] = None,  # you can skip this only when sending along with content parameter
+    file: Optional[Union[str, bytes, IOBase, os.PathLike]] = None,
+    content: Optional[Union[str, bytes]] = None,
+    title: Optional[str] = None,
+    alt_txt: Optional[str] = None,
+    snippet_type: Optional[str] = None,
+    # To upload multiple files at a time
+    file_uploads: Optional[List[Dict[str, Any]]] = None,
+    channel: Optional[str] = None,
+    channels: Optional[List[str]] = None,
+    initial_comment: Optional[str] = None,
+    thread_ts: Optional[str] = None,
+    request_file_info: bool = True,  # since v3.23, this flag is no longer necessary
+    **kwargs,
+) -> SlackResponse:
+    """This wrapper method provides an easy way to upload files using the following endpoints:
+
+    - step1: https://docs.slack.dev/reference/methods/files.getUploadURLExternal
+
+    - step2: "https://files.slack.com/upload/v1/..." URLs returned from files.getUploadURLExternal API
+
+    - step3: https://docs.slack.dev/reference/methods/files.completeUploadExternal
+        and https://docs.slack.dev/reference/methods/files.info
+
+    """
+    if file is None and content is None and file_uploads is None:
+        raise e.SlackRequestError("Any of file, content, and file_uploads must be specified.")
+    if file is not None and content is not None:
+        raise e.SlackRequestError("You cannot specify both the file and the content argument.")
+
+    # deprecated arguments:
+    filetype = kwargs.get("filetype")
+
+    if filetype is not None:
+        warnings.warn("The filetype parameter is no longer supported. Please remove it from the arguments.")
+
+    # step1: files.getUploadURLExternal per file
+    files: List[Dict[str, Any]] = []
+    if file_uploads is not None:
+        for f in file_uploads:
+            files.append(_to_v2_file_upload_item(f))
+    else:
+        f = _to_v2_file_upload_item(
+            {
+                "filename": filename,
+                "file": file,
+                "content": content,
+                "title": title,
+                "alt_txt": alt_txt,
+                "snippet_type": snippet_type,
+            }
+        )
+        files.append(f)
+
+    for f in files:
+        url_response = self.files_getUploadURLExternal(
+            filename=f.get("filename"),  # type: ignore[arg-type]
+            length=f.get("length"),  # type: ignore[arg-type]
+            alt_txt=f.get("alt_txt"),
+            snippet_type=f.get("snippet_type"),
+            token=kwargs.get("token"),
+        )
+        _validate_for_legacy_client(url_response)
+        f["file_id"] = url_response.get("file_id")  # type: ignore[union-attr, unused-ignore]
+        f["upload_url"] = url_response.get("upload_url")  # type: ignore[union-attr, unused-ignore]
+
+    # step2: "https://files.slack.com/upload/v1/..." per file
+    for f in files:
+        upload_result = self._upload_file(
+            url=f["upload_url"],
+            data=f["data"],
+            logger=self._logger,
+            timeout=self.timeout,
+            proxy=self.proxy,
+            ssl=self.ssl,
+        )
+        if upload_result.status != 200:
+            status = upload_result.status
+            body = upload_result.body
+            message = (
+                "Failed to upload a file "
+                f"(status: {status}, body: {body}, filename: {f.get('filename')}, title: {f.get('title')})"
+            )
+            raise e.SlackRequestError(message)
+
+    # step3: files.completeUploadExternal with all the sets of (file_id + title)
+    completion = self.files_completeUploadExternal(
+        files=[{"id": f["file_id"], "title": f["title"]} for f in files],
+        channel_id=channel,
+        channels=channels,
+        initial_comment=initial_comment,
+        thread_ts=thread_ts,
+        **kwargs,
+    )
+    if len(completion.get("files")) == 1:  # type: ignore[arg-type, union-attr, unused-ignore]
+        completion.data["file"] = completion.get("files")[0]  # type: ignore[index, union-attr, unused-ignore]
+    return completion
+
+

This wrapper method provides an easy way to upload files using the following endpoints:

+
+
+
+def functions_completeError(self, *, function_execution_id: str, error: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def functions_completeError(
+    self,
+    *,
+    function_execution_id: str,
+    error: str,
+    **kwargs,
+) -> SlackResponse:
+    """Signal the failure to execute a function
+    https://docs.slack.dev/reference/methods/functions.completeError
+    """
+    kwargs.update({"function_execution_id": function_execution_id, "error": error})
+    return self.api_call("functions.completeError", params=kwargs)
+
+ +
+
+def functions_completeSuccess(self, *, function_execution_id: str, outputs: Dict[str, Any], **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def functions_completeSuccess(
+    self,
+    *,
+    function_execution_id: str,
+    outputs: Dict[str, Any],
+    **kwargs,
+) -> SlackResponse:
+    """Signal the successful completion of a function
+    https://docs.slack.dev/reference/methods/functions.completeSuccess
+    """
+    kwargs.update({"function_execution_id": function_execution_id, "outputs": json.dumps(outputs)})
+    return self.api_call("functions.completeSuccess", params=kwargs)
+
+

Signal the successful completion of a function +https://docs.slack.dev/reference/methods/functions.completeSuccess

+
+
+def groups_archive(self, *, channel: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def groups_archive(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> SlackResponse:
+    """Archives a private channel."""
+    kwargs.update({"channel": channel})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("groups.archive", json=kwargs)
+
+

Archives a private channel.

+
+
+def groups_create(self, *, name: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def groups_create(
+    self,
+    *,
+    name: str,
+    **kwargs,
+) -> SlackResponse:
+    """Creates a private channel."""
+    kwargs.update({"name": name})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("groups.create", json=kwargs)
+
+

Creates a private channel.

+
+
+def groups_createChild(self, *, channel: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def groups_createChild(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> SlackResponse:
+    """Clones and archives a private channel."""
+    kwargs.update({"channel": channel})
+    return self.api_call("groups.createChild", http_verb="GET", params=kwargs)
+
+

Clones and archives a private channel.

+
+
+def groups_history(self, *, channel: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def groups_history(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> SlackResponse:
+    """Fetches history of messages and events from a private channel."""
+    kwargs.update({"channel": channel})
+    return self.api_call("groups.history", http_verb="GET", params=kwargs)
+
+

Fetches history of messages and events from a private channel.

+
+
+def groups_info(self, *, channel: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def groups_info(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> SlackResponse:
+    """Gets information about a private channel."""
+    kwargs.update({"channel": channel})
+    return self.api_call("groups.info", http_verb="GET", params=kwargs)
+
+

Gets information about a private channel.

+
+
+def groups_invite(self, *, channel: str, user: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def groups_invite(
+    self,
+    *,
+    channel: str,
+    user: str,
+    **kwargs,
+) -> SlackResponse:
+    """Invites a user to a private channel."""
+    kwargs.update({"channel": channel, "user": user})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("groups.invite", json=kwargs)
+
+

Invites a user to a private channel.

+
+
+def groups_kick(self, *, channel: str, user: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def groups_kick(
+    self,
+    *,
+    channel: str,
+    user: str,
+    **kwargs,
+) -> SlackResponse:
+    """Removes a user from a private channel."""
+    kwargs.update({"channel": channel, "user": user})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("groups.kick", json=kwargs)
+
+

Removes a user from a private channel.

+
+
+def groups_leave(self, *, channel: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def groups_leave(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> SlackResponse:
+    """Leaves a private channel."""
+    kwargs.update({"channel": channel})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("groups.leave", json=kwargs)
+
+

Leaves a private channel.

+
+
+def groups_list(self, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def groups_list(
+    self,
+    **kwargs,
+) -> SlackResponse:
+    """Lists private channels that the calling user has access to."""
+    return self.api_call("groups.list", http_verb="GET", params=kwargs)
+
+

Lists private channels that the calling user has access to.

+
+
+def groups_mark(self, *, channel: str, ts: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def groups_mark(
+    self,
+    *,
+    channel: str,
+    ts: str,
+    **kwargs,
+) -> SlackResponse:
+    """Sets the read cursor in a private channel."""
+    kwargs.update({"channel": channel, "ts": ts})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("groups.mark", json=kwargs)
+
+

Sets the read cursor in a private channel.

+
+
+def groups_open(self, *, channel: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def groups_open(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> SlackResponse:
+    """Opens a private channel."""
+    kwargs.update({"channel": channel})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("groups.open", json=kwargs)
+
+

Opens a private channel.

+
+
+def groups_rename(self, *, channel: str, name: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def groups_rename(
+    self,
+    *,
+    channel: str,
+    name: str,
+    **kwargs,
+) -> SlackResponse:
+    """Renames a private channel."""
+    kwargs.update({"channel": channel, "name": name})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("groups.rename", json=kwargs)
+
+

Renames a private channel.

+
+
+def groups_replies(self, *, channel: str, thread_ts: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def groups_replies(
+    self,
+    *,
+    channel: str,
+    thread_ts: str,
+    **kwargs,
+) -> SlackResponse:
+    """Retrieve a thread of messages posted to a private channel"""
+    kwargs.update({"channel": channel, "thread_ts": thread_ts})
+    return self.api_call("groups.replies", http_verb="GET", params=kwargs)
+
+

Retrieve a thread of messages posted to a private channel

+
+
+def groups_setPurpose(self, *, channel: str, purpose: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def groups_setPurpose(
+    self,
+    *,
+    channel: str,
+    purpose: str,
+    **kwargs,
+) -> SlackResponse:
+    """Sets the purpose for a private channel."""
+    kwargs.update({"channel": channel, "purpose": purpose})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("groups.setPurpose", json=kwargs)
+
+

Sets the purpose for a private channel.

+
+
+def groups_setTopic(self, *, channel: str, topic: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def groups_setTopic(
+    self,
+    *,
+    channel: str,
+    topic: str,
+    **kwargs,
+) -> SlackResponse:
+    """Sets the topic for a private channel."""
+    kwargs.update({"channel": channel, "topic": topic})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("groups.setTopic", json=kwargs)
+
+

Sets the topic for a private channel.

+
+
+def groups_unarchive(self, *, channel: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def groups_unarchive(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> SlackResponse:
+    """Unarchives a private channel."""
+    kwargs.update({"channel": channel})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("groups.unarchive", json=kwargs)
+
+

Unarchives a private channel.

+
+
+def im_close(self, *, channel: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def im_close(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> SlackResponse:
+    """Close a direct message channel."""
+    kwargs.update({"channel": channel})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("im.close", json=kwargs)
+
+

Close a direct message channel.

+
+
+def im_history(self, *, channel: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def im_history(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> SlackResponse:
+    """Fetches history of messages and events from direct message channel."""
+    kwargs.update({"channel": channel})
+    return self.api_call("im.history", http_verb="GET", params=kwargs)
+
+

Fetches history of messages and events from direct message channel.

+
+
+def im_list(self, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def im_list(
+    self,
+    **kwargs,
+) -> SlackResponse:
+    """Lists direct message channels for the calling user."""
+    return self.api_call("im.list", http_verb="GET", params=kwargs)
+
+

Lists direct message channels for the calling user.

+
+
+def im_mark(self, *, channel: str, ts: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def im_mark(
+    self,
+    *,
+    channel: str,
+    ts: str,
+    **kwargs,
+) -> SlackResponse:
+    """Sets the read cursor in a direct message channel."""
+    kwargs.update({"channel": channel, "ts": ts})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("im.mark", json=kwargs)
+
+

Sets the read cursor in a direct message channel.

+
+
+def im_open(self, *, user: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def im_open(
+    self,
+    *,
+    user: str,
+    **kwargs,
+) -> SlackResponse:
+    """Opens a direct message channel."""
+    kwargs.update({"user": user})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("im.open", json=kwargs)
+
+

Opens a direct message channel.

+
+
+def im_replies(self, *, channel: str, thread_ts: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def im_replies(
+    self,
+    *,
+    channel: str,
+    thread_ts: str,
+    **kwargs,
+) -> SlackResponse:
+    """Retrieve a thread of messages posted to a direct message conversation"""
+    kwargs.update({"channel": channel, "thread_ts": thread_ts})
+    return self.api_call("im.replies", http_verb="GET", params=kwargs)
+
+

Retrieve a thread of messages posted to a direct message conversation

+
+
+def migration_exchange(self,
*,
users: str | Sequence[str],
team_id: str | None = None,
to_old: bool | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def migration_exchange(
+    self,
+    *,
+    users: Union[str, Sequence[str]],
+    team_id: Optional[str] = None,
+    to_old: Optional[bool] = None,
+    **kwargs,
+) -> SlackResponse:
+    """For Enterprise Grid workspaces, map local user IDs to global user IDs
+    https://docs.slack.dev/reference/methods/migration.exchange
+    """
+    if isinstance(users, (list, tuple)):
+        kwargs.update({"users": ",".join(users)})
+    else:
+        kwargs.update({"users": users})
+    kwargs.update({"team_id": team_id, "to_old": to_old})
+    return self.api_call("migration.exchange", http_verb="GET", params=kwargs)
+
+

For Enterprise Grid workspaces, map local user IDs to global user IDs +https://docs.slack.dev/reference/methods/migration.exchange

+
+
+def mpim_close(self, *, channel: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def mpim_close(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> SlackResponse:
+    """Closes a multiparty direct message channel."""
+    kwargs.update({"channel": channel})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("mpim.close", json=kwargs)
+
+

Closes a multiparty direct message channel.

+
+
+def mpim_history(self, *, channel: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def mpim_history(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> SlackResponse:
+    """Fetches history of messages and events from a multiparty direct message."""
+    kwargs.update({"channel": channel})
+    return self.api_call("mpim.history", http_verb="GET", params=kwargs)
+
+

Fetches history of messages and events from a multiparty direct message.

+
+
+def mpim_list(self, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def mpim_list(
+    self,
+    **kwargs,
+) -> SlackResponse:
+    """Lists multiparty direct message channels for the calling user."""
+    return self.api_call("mpim.list", http_verb="GET", params=kwargs)
+
+

Lists multiparty direct message channels for the calling user.

+
+
+def mpim_mark(self, *, channel: str, ts: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def mpim_mark(
+    self,
+    *,
+    channel: str,
+    ts: str,
+    **kwargs,
+) -> SlackResponse:
+    """Sets the read cursor in a multiparty direct message channel."""
+    kwargs.update({"channel": channel, "ts": ts})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("mpim.mark", json=kwargs)
+
+

Sets the read cursor in a multiparty direct message channel.

+
+
+def mpim_open(self, *, users: str | Sequence[str], **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def mpim_open(
+    self,
+    *,
+    users: Union[str, Sequence[str]],
+    **kwargs,
+) -> SlackResponse:
+    """This method opens a multiparty direct message."""
+    if isinstance(users, (list, tuple)):
+        kwargs.update({"users": ",".join(users)})
+    else:
+        kwargs.update({"users": users})
+    return self.api_call("mpim.open", params=kwargs)
+
+

This method opens a multiparty direct message.

+
+
+def mpim_replies(self, *, channel: str, thread_ts: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def mpim_replies(
+    self,
+    *,
+    channel: str,
+    thread_ts: str,
+    **kwargs,
+) -> SlackResponse:
+    """Retrieve a thread of messages posted to a direct message conversation from a
+    multiparty direct message.
+    """
+    kwargs.update({"channel": channel, "thread_ts": thread_ts})
+    return self.api_call("mpim.replies", http_verb="GET", params=kwargs)
+
+

Retrieve a thread of messages posted to a direct message conversation from a +multiparty direct message.

+
+
+def oauth_access(self,
*,
client_id: str,
client_secret: str,
code: str,
redirect_uri: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def oauth_access(
+    self,
+    *,
+    client_id: str,
+    client_secret: str,
+    code: str,
+    redirect_uri: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Exchanges a temporary OAuth verifier code for an access token.
+    https://docs.slack.dev/reference/methods/oauth.access
+    """
+    if redirect_uri is not None:
+        kwargs.update({"redirect_uri": redirect_uri})
+    kwargs.update({"code": code})
+    return self.api_call(
+        "oauth.access",
+        data=kwargs,
+        auth={"client_id": client_id, "client_secret": client_secret},
+    )
+
+

Exchanges a temporary OAuth verifier code for an access token. +https://docs.slack.dev/reference/methods/oauth.access

+
+
+def oauth_v2_access(self,
*,
client_id: str,
client_secret: str,
code: str | None = None,
redirect_uri: str | None = None,
grant_type: str | None = None,
refresh_token: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def oauth_v2_access(
+    self,
+    *,
+    client_id: str,
+    client_secret: str,
+    # This field is required when processing the OAuth redirect URL requests
+    # while it's absent for token rotation
+    code: Optional[str] = None,
+    redirect_uri: Optional[str] = None,
+    # This field is required for token rotation
+    grant_type: Optional[str] = None,
+    # This field is required for token rotation
+    refresh_token: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Exchanges a temporary OAuth verifier code for an access token.
+    https://docs.slack.dev/reference/methods/oauth.v2.access
+    """
+    if redirect_uri is not None:
+        kwargs.update({"redirect_uri": redirect_uri})
+    if code is not None:
+        kwargs.update({"code": code})
+    if grant_type is not None:
+        kwargs.update({"grant_type": grant_type})
+    if refresh_token is not None:
+        kwargs.update({"refresh_token": refresh_token})
+    return self.api_call(
+        "oauth.v2.access",
+        data=kwargs,
+        auth={"client_id": client_id, "client_secret": client_secret},
+    )
+
+

Exchanges a temporary OAuth verifier code for an access token. +https://docs.slack.dev/reference/methods/oauth.v2.access

+
+
+def oauth_v2_exchange(self, *, token: str, client_id: str, client_secret: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def oauth_v2_exchange(
+    self,
+    *,
+    token: str,
+    client_id: str,
+    client_secret: str,
+    **kwargs,
+) -> SlackResponse:
+    """Exchanges a legacy access token for a new expiring access token and refresh token
+    https://docs.slack.dev/reference/methods/oauth.v2.exchange
+    """
+    kwargs.update({"client_id": client_id, "client_secret": client_secret, "token": token})
+    return self.api_call("oauth.v2.exchange", params=kwargs)
+
+

Exchanges a legacy access token for a new expiring access token and refresh token +https://docs.slack.dev/reference/methods/oauth.v2.exchange

+
+
+def openid_connect_token(self,
client_id: str,
client_secret: str,
code: str | None = None,
redirect_uri: str | None = None,
grant_type: str | None = None,
refresh_token: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def openid_connect_token(
+    self,
+    client_id: str,
+    client_secret: str,
+    code: Optional[str] = None,
+    redirect_uri: Optional[str] = None,
+    grant_type: Optional[str] = None,
+    refresh_token: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Exchanges a temporary OAuth verifier code for an access token for Sign in with Slack.
+    https://docs.slack.dev/reference/methods/openid.connect.token
+    """
+    if redirect_uri is not None:
+        kwargs.update({"redirect_uri": redirect_uri})
+    if code is not None:
+        kwargs.update({"code": code})
+    if grant_type is not None:
+        kwargs.update({"grant_type": grant_type})
+    if refresh_token is not None:
+        kwargs.update({"refresh_token": refresh_token})
+    return self.api_call(
+        "openid.connect.token",
+        data=kwargs,
+        auth={"client_id": client_id, "client_secret": client_secret},
+    )
+
+

Exchanges a temporary OAuth verifier code for an access token for Sign in with Slack. +https://docs.slack.dev/reference/methods/openid.connect.token

+
+
+def openid_connect_userInfo(self, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def openid_connect_userInfo(
+    self,
+    **kwargs,
+) -> SlackResponse:
+    """Get the identity of a user who has authorized Sign in with Slack.
+    https://docs.slack.dev/reference/methods/openid.connect.userInfo
+    """
+    return self.api_call("openid.connect.userInfo", params=kwargs)
+
+

Get the identity of a user who has authorized Sign in with Slack. +https://docs.slack.dev/reference/methods/openid.connect.userInfo

+
+
+def pins_add(self, *, channel: str, timestamp: str | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def pins_add(
+    self,
+    *,
+    channel: str,
+    timestamp: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Pins an item to a channel.
+    https://docs.slack.dev/reference/methods/pins.add
+    """
+    kwargs.update({"channel": channel, "timestamp": timestamp})
+    return self.api_call("pins.add", params=kwargs)
+
+ +
+
+def pins_list(self, *, channel: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def pins_list(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> SlackResponse:
+    """Lists items pinned to a channel.
+    https://docs.slack.dev/reference/methods/pins.list
+    """
+    kwargs.update({"channel": channel})
+    return self.api_call("pins.list", http_verb="GET", params=kwargs)
+
+

Lists items pinned to a channel. +https://docs.slack.dev/reference/methods/pins.list

+
+
+def pins_remove(self, *, channel: str, timestamp: str | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def pins_remove(
+    self,
+    *,
+    channel: str,
+    timestamp: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Un-pins an item from a channel.
+    https://docs.slack.dev/reference/methods/pins.remove
+    """
+    kwargs.update({"channel": channel, "timestamp": timestamp})
+    return self.api_call("pins.remove", params=kwargs)
+
+ +
+
+def reactions_add(self, *, channel: str, name: str, timestamp: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def reactions_add(
+    self,
+    *,
+    channel: str,
+    name: str,
+    timestamp: str,
+    **kwargs,
+) -> SlackResponse:
+    """Adds a reaction to an item.
+    https://docs.slack.dev/reference/methods/reactions.add
+    """
+    kwargs.update({"channel": channel, "name": name, "timestamp": timestamp})
+    return self.api_call("reactions.add", params=kwargs)
+
+ +
+
+def reactions_get(self,
*,
channel: str | None = None,
file: str | None = None,
file_comment: str | None = None,
full: bool | None = None,
timestamp: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def reactions_get(
+    self,
+    *,
+    channel: Optional[str] = None,
+    file: Optional[str] = None,
+    file_comment: Optional[str] = None,
+    full: Optional[bool] = None,
+    timestamp: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Gets reactions for an item.
+    https://docs.slack.dev/reference/methods/reactions.get
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "file": file,
+            "file_comment": file_comment,
+            "full": full,
+            "timestamp": timestamp,
+        }
+    )
+    return self.api_call("reactions.get", http_verb="GET", params=kwargs)
+
+ +
+
+def reactions_list(self,
*,
count: int | None = None,
cursor: str | None = None,
full: bool | None = None,
limit: int | None = None,
page: int | None = None,
team_id: str | None = None,
user: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def reactions_list(
+    self,
+    *,
+    count: Optional[int] = None,
+    cursor: Optional[str] = None,
+    full: Optional[bool] = None,
+    limit: Optional[int] = None,
+    page: Optional[int] = None,
+    team_id: Optional[str] = None,
+    user: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Lists reactions made by a user.
+    https://docs.slack.dev/reference/methods/reactions.list
+    """
+    kwargs.update(
+        {
+            "count": count,
+            "cursor": cursor,
+            "full": full,
+            "limit": limit,
+            "page": page,
+            "team_id": team_id,
+            "user": user,
+        }
+    )
+    return self.api_call("reactions.list", http_verb="GET", params=kwargs)
+
+ +
+
+def reactions_remove(self,
*,
name: str,
channel: str | None = None,
file: str | None = None,
file_comment: str | None = None,
timestamp: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def reactions_remove(
+    self,
+    *,
+    name: str,
+    channel: Optional[str] = None,
+    file: Optional[str] = None,
+    file_comment: Optional[str] = None,
+    timestamp: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Removes a reaction from an item.
+    https://docs.slack.dev/reference/methods/reactions.remove
+    """
+    kwargs.update(
+        {
+            "name": name,
+            "channel": channel,
+            "file": file,
+            "file_comment": file_comment,
+            "timestamp": timestamp,
+        }
+    )
+    return self.api_call("reactions.remove", params=kwargs)
+
+ +
+
+def reminders_add(self,
*,
text: str,
time: str,
team_id: str | None = None,
user: str | None = None,
recurrence: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def reminders_add(
+    self,
+    *,
+    text: str,
+    time: str,
+    team_id: Optional[str] = None,
+    user: Optional[str] = None,
+    recurrence: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Creates a reminder.
+    https://docs.slack.dev/reference/methods/reminders.add
+    """
+    kwargs.update(
+        {
+            "text": text,
+            "time": time,
+            "team_id": team_id,
+            "user": user,
+            "recurrence": recurrence,
+        }
+    )
+    return self.api_call("reminders.add", params=kwargs)
+
+ +
+
+def reminders_complete(self, *, reminder: str, team_id: str | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def reminders_complete(
+    self,
+    *,
+    reminder: str,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Marks a reminder as complete.
+    https://docs.slack.dev/reference/methods/reminders.complete
+    """
+    kwargs.update({"reminder": reminder, "team_id": team_id})
+    return self.api_call("reminders.complete", params=kwargs)
+
+ +
+
+def reminders_delete(self, *, reminder: str, team_id: str | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def reminders_delete(
+    self,
+    *,
+    reminder: str,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Deletes a reminder.
+    https://docs.slack.dev/reference/methods/reminders.delete
+    """
+    kwargs.update({"reminder": reminder, "team_id": team_id})
+    return self.api_call("reminders.delete", params=kwargs)
+
+ +
+
+def reminders_info(self, *, reminder: str, team_id: str | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def reminders_info(
+    self,
+    *,
+    reminder: str,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Gets information about a reminder.
+    https://docs.slack.dev/reference/methods/reminders.info
+    """
+    kwargs.update({"reminder": reminder, "team_id": team_id})
+    return self.api_call("reminders.info", http_verb="GET", params=kwargs)
+
+ +
+
+def reminders_list(self, *, team_id: str | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def reminders_list(
+    self,
+    *,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Lists all reminders created by or for a given user.
+    https://docs.slack.dev/reference/methods/reminders.list
+    """
+    kwargs.update({"team_id": team_id})
+    return self.api_call("reminders.list", http_verb="GET", params=kwargs)
+
+

Lists all reminders created by or for a given user. +https://docs.slack.dev/reference/methods/reminders.list

+
+
+def rtm_connect(self,
*,
batch_presence_aware: bool | None = None,
presence_sub: bool | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def rtm_connect(
+    self,
+    *,
+    batch_presence_aware: Optional[bool] = None,
+    presence_sub: Optional[bool] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Starts a Real Time Messaging session.
+    https://docs.slack.dev/reference/methods/rtm.connect
+    """
+    kwargs.update({"batch_presence_aware": batch_presence_aware, "presence_sub": presence_sub})
+    return self.api_call("rtm.connect", http_verb="GET", params=kwargs)
+
+

Starts a Real Time Messaging session. +https://docs.slack.dev/reference/methods/rtm.connect

+
+
+def rtm_start(self,
*,
batch_presence_aware: bool | None = None,
include_locale: bool | None = None,
mpim_aware: bool | None = None,
no_latest: bool | None = None,
no_unreads: bool | None = None,
presence_sub: bool | None = None,
simple_latest: bool | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def rtm_start(
+    self,
+    *,
+    batch_presence_aware: Optional[bool] = None,
+    include_locale: Optional[bool] = None,
+    mpim_aware: Optional[bool] = None,
+    no_latest: Optional[bool] = None,
+    no_unreads: Optional[bool] = None,
+    presence_sub: Optional[bool] = None,
+    simple_latest: Optional[bool] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Starts a Real Time Messaging session.
+    https://docs.slack.dev/reference/methods/rtm.start
+    """
+    kwargs.update(
+        {
+            "batch_presence_aware": batch_presence_aware,
+            "include_locale": include_locale,
+            "mpim_aware": mpim_aware,
+            "no_latest": no_latest,
+            "no_unreads": no_unreads,
+            "presence_sub": presence_sub,
+            "simple_latest": simple_latest,
+        }
+    )
+    return self.api_call("rtm.start", http_verb="GET", params=kwargs)
+
+

Starts a Real Time Messaging session. +https://docs.slack.dev/reference/methods/rtm.start

+
+
+def search_all(self,
*,
query: str,
count: int | None = None,
highlight: bool | None = None,
page: int | None = None,
sort: str | None = None,
sort_dir: str | None = None,
team_id: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def search_all(
+    self,
+    *,
+    query: str,
+    count: Optional[int] = None,
+    highlight: Optional[bool] = None,
+    page: Optional[int] = None,
+    sort: Optional[str] = None,
+    sort_dir: Optional[str] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Searches for messages and files matching a query.
+    https://docs.slack.dev/reference/methods/search.all
+    """
+    kwargs.update(
+        {
+            "query": query,
+            "count": count,
+            "highlight": highlight,
+            "page": page,
+            "sort": sort,
+            "sort_dir": sort_dir,
+            "team_id": team_id,
+        }
+    )
+    return self.api_call("search.all", http_verb="GET", params=kwargs)
+
+

Searches for messages and files matching a query. +https://docs.slack.dev/reference/methods/search.all

+
+
+def search_files(self,
*,
query: str,
count: int | None = None,
highlight: bool | None = None,
page: int | None = None,
sort: str | None = None,
sort_dir: str | None = None,
team_id: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def search_files(
+    self,
+    *,
+    query: str,
+    count: Optional[int] = None,
+    highlight: Optional[bool] = None,
+    page: Optional[int] = None,
+    sort: Optional[str] = None,
+    sort_dir: Optional[str] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Searches for files matching a query.
+    https://docs.slack.dev/reference/methods/search.files
+    """
+    kwargs.update(
+        {
+            "query": query,
+            "count": count,
+            "highlight": highlight,
+            "page": page,
+            "sort": sort,
+            "sort_dir": sort_dir,
+            "team_id": team_id,
+        }
+    )
+    return self.api_call("search.files", http_verb="GET", params=kwargs)
+
+

Searches for files matching a query. +https://docs.slack.dev/reference/methods/search.files

+
+
+def search_messages(self,
*,
query: str,
count: int | None = None,
cursor: str | None = None,
highlight: bool | None = None,
page: int | None = None,
sort: str | None = None,
sort_dir: str | None = None,
team_id: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def search_messages(
+    self,
+    *,
+    query: str,
+    count: Optional[int] = None,
+    cursor: Optional[str] = None,
+    highlight: Optional[bool] = None,
+    page: Optional[int] = None,
+    sort: Optional[str] = None,
+    sort_dir: Optional[str] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Searches for messages matching a query.
+    https://docs.slack.dev/reference/methods/search.messages
+    """
+    kwargs.update(
+        {
+            "query": query,
+            "count": count,
+            "cursor": cursor,
+            "highlight": highlight,
+            "page": page,
+            "sort": sort,
+            "sort_dir": sort_dir,
+            "team_id": team_id,
+        }
+    )
+    return self.api_call("search.messages", http_verb="GET", params=kwargs)
+
+

Searches for messages matching a query. +https://docs.slack.dev/reference/methods/search.messages

+
+
+def slackLists_access_delete(self,
*,
list_id: str,
channel_ids: List[str] | None = None,
user_ids: List[str] | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def slackLists_access_delete(
+    self,
+    *,
+    list_id: str,
+    channel_ids: Optional[List[str]] = None,
+    user_ids: Optional[List[str]] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Revoke access to a List for specified entities.
+    https://docs.slack.dev/reference/methods/slackLists.access.delete
+    """
+    kwargs.update({"list_id": list_id, "channel_ids": channel_ids, "user_ids": user_ids})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("slackLists.access.delete", json=kwargs)
+
+

Revoke access to a List for specified entities. +https://docs.slack.dev/reference/methods/slackLists.access.delete

+
+
+def slackLists_access_set(self,
*,
list_id: str,
access_level: str,
channel_ids: List[str] | None = None,
user_ids: List[str] | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def slackLists_access_set(
+    self,
+    *,
+    list_id: str,
+    access_level: str,
+    channel_ids: Optional[List[str]] = None,
+    user_ids: Optional[List[str]] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Set the access level to a List for specified entities.
+    https://docs.slack.dev/reference/methods/slackLists.access.set
+    """
+    kwargs.update({"list_id": list_id, "access_level": access_level, "channel_ids": channel_ids, "user_ids": user_ids})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("slackLists.access.set", json=kwargs)
+
+

Set the access level to a List for specified entities. +https://docs.slack.dev/reference/methods/slackLists.access.set

+
+
+def slackLists_create(self,
*,
name: str,
description_blocks: str | Sequence[Dict | RichTextBlock] | None = None,
schema: List[Dict[str, Any]] | None = None,
copy_from_list_id: str | None = None,
include_copied_list_records: bool | None = None,
todo_mode: bool | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def slackLists_create(
+    self,
+    *,
+    name: str,
+    description_blocks: Optional[Union[str, Sequence[Union[Dict, RichTextBlock]]]] = None,
+    schema: Optional[List[Dict[str, Any]]] = None,
+    copy_from_list_id: Optional[str] = None,
+    include_copied_list_records: Optional[bool] = None,
+    todo_mode: Optional[bool] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Creates a List.
+    https://docs.slack.dev/reference/methods/slackLists.create
+    """
+    kwargs.update(
+        {
+            "name": name,
+            "description_blocks": description_blocks,
+            "schema": schema,
+            "copy_from_list_id": copy_from_list_id,
+            "include_copied_list_records": include_copied_list_records,
+            "todo_mode": todo_mode,
+        }
+    )
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("slackLists.create", json=kwargs)
+
+ +
+
+def slackLists_download_get(self, *, list_id: str, job_id: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def slackLists_download_get(
+    self,
+    *,
+    list_id: str,
+    job_id: str,
+    **kwargs,
+) -> SlackResponse:
+    """Retrieve List download URL from an export job to download List contents.
+    https://docs.slack.dev/reference/methods/slackLists.download.get
+    """
+    kwargs.update(
+        {
+            "list_id": list_id,
+            "job_id": job_id,
+        }
+    )
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("slackLists.download.get", json=kwargs)
+
+

Retrieve List download URL from an export job to download List contents. +https://docs.slack.dev/reference/methods/slackLists.download.get

+
+
+def slackLists_download_start(self, *, list_id: str, include_archived: bool | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def slackLists_download_start(
+    self,
+    *,
+    list_id: str,
+    include_archived: Optional[bool] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Initiate a job to export List contents.
+    https://docs.slack.dev/reference/methods/slackLists.download.start
+    """
+    kwargs.update(
+        {
+            "list_id": list_id,
+            "include_archived": include_archived,
+        }
+    )
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("slackLists.download.start", json=kwargs)
+
+ +
+
+def slackLists_items_create(self,
*,
list_id: str,
duplicated_item_id: str | None = None,
parent_item_id: str | None = None,
initial_fields: List[Dict[str, Any]] | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def slackLists_items_create(
+    self,
+    *,
+    list_id: str,
+    duplicated_item_id: Optional[str] = None,
+    parent_item_id: Optional[str] = None,
+    initial_fields: Optional[List[Dict[str, Any]]] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Add a new item to an existing List.
+    https://docs.slack.dev/reference/methods/slackLists.items.create
+    """
+    kwargs.update(
+        {
+            "list_id": list_id,
+            "duplicated_item_id": duplicated_item_id,
+            "parent_item_id": parent_item_id,
+            "initial_fields": initial_fields,
+        }
+    )
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("slackLists.items.create", json=kwargs)
+
+ +
+
+def slackLists_items_delete(self, *, list_id: str, id: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def slackLists_items_delete(
+    self,
+    *,
+    list_id: str,
+    id: str,
+    **kwargs,
+) -> SlackResponse:
+    """Deletes an item from an existing List.
+    https://docs.slack.dev/reference/methods/slackLists.items.delete
+    """
+    kwargs.update(
+        {
+            "list_id": list_id,
+            "id": id,
+        }
+    )
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("slackLists.items.delete", json=kwargs)
+
+ +
+
+def slackLists_items_deleteMultiple(self, *, list_id: str, ids: List[str], **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def slackLists_items_deleteMultiple(
+    self,
+    *,
+    list_id: str,
+    ids: List[str],
+    **kwargs,
+) -> SlackResponse:
+    """Deletes multiple items from an existing List.
+    https://docs.slack.dev/reference/methods/slackLists.items.deleteMultiple
+    """
+    kwargs.update(
+        {
+            "list_id": list_id,
+            "ids": ids,
+        }
+    )
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("slackLists.items.deleteMultiple", json=kwargs)
+
+ +
+
+def slackLists_items_info(self, *, list_id: str, id: str, include_is_subscribed: bool | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def slackLists_items_info(
+    self,
+    *,
+    list_id: str,
+    id: str,
+    include_is_subscribed: Optional[bool] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Get a row from a List.
+    https://docs.slack.dev/reference/methods/slackLists.items.info
+    """
+    kwargs.update(
+        {
+            "list_id": list_id,
+            "id": id,
+            "include_is_subscribed": include_is_subscribed,
+        }
+    )
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("slackLists.items.info", json=kwargs)
+
+ +
+
+def slackLists_items_list(self,
*,
list_id: str,
limit: int | None = None,
cursor: str | None = None,
archived: bool | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def slackLists_items_list(
+    self,
+    *,
+    list_id: str,
+    limit: Optional[int] = None,
+    cursor: Optional[str] = None,
+    archived: Optional[bool] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Get records from a List.
+    https://docs.slack.dev/reference/methods/slackLists.items.list
+    """
+    kwargs.update(
+        {
+            "list_id": list_id,
+            "limit": limit,
+            "cursor": cursor,
+            "archived": archived,
+        }
+    )
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("slackLists.items.list", json=kwargs)
+
+ +
+
+def slackLists_items_update(self, *, list_id: str, cells: List[Dict[str, Any]], **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def slackLists_items_update(
+    self,
+    *,
+    list_id: str,
+    cells: List[Dict[str, Any]],
+    **kwargs,
+) -> SlackResponse:
+    """Updates cells in a List.
+    https://docs.slack.dev/reference/methods/slackLists.items.update
+    """
+    kwargs.update(
+        {
+            "list_id": list_id,
+            "cells": cells,
+        }
+    )
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("slackLists.items.update", json=kwargs)
+
+ +
+
+def slackLists_update(self,
*,
id: str,
name: str | None = None,
description_blocks: str | Sequence[Dict | RichTextBlock] | None = None,
todo_mode: bool | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def slackLists_update(
+    self,
+    *,
+    id: str,
+    name: Optional[str] = None,
+    description_blocks: Optional[Union[str, Sequence[Union[Dict, RichTextBlock]]]] = None,
+    todo_mode: Optional[bool] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Update a List.
+    https://docs.slack.dev/reference/methods/slackLists.update
+    """
+    kwargs.update(
+        {
+            "id": id,
+            "name": name,
+            "description_blocks": description_blocks,
+            "todo_mode": todo_mode,
+        }
+    )
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("slackLists.update", json=kwargs)
+
+ +
+
+def stars_add(self,
*,
channel: str | None = None,
file: str | None = None,
file_comment: str | None = None,
timestamp: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def stars_add(
+    self,
+    *,
+    channel: Optional[str] = None,
+    file: Optional[str] = None,
+    file_comment: Optional[str] = None,
+    timestamp: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Adds a star to an item.
+    https://docs.slack.dev/reference/methods/stars.add
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "file": file,
+            "file_comment": file_comment,
+            "timestamp": timestamp,
+        }
+    )
+    return self.api_call("stars.add", params=kwargs)
+
+ +
+
+def stars_list(self,
*,
count: int | None = None,
cursor: str | None = None,
limit: int | None = None,
page: int | None = None,
team_id: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def stars_list(
+    self,
+    *,
+    count: Optional[int] = None,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    page: Optional[int] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Lists stars for a user.
+    https://docs.slack.dev/reference/methods/stars.list
+    """
+    kwargs.update(
+        {
+            "count": count,
+            "cursor": cursor,
+            "limit": limit,
+            "page": page,
+            "team_id": team_id,
+        }
+    )
+    return self.api_call("stars.list", http_verb="GET", params=kwargs)
+
+ +
+
+def stars_remove(self,
*,
channel: str | None = None,
file: str | None = None,
file_comment: str | None = None,
timestamp: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def stars_remove(
+    self,
+    *,
+    channel: Optional[str] = None,
+    file: Optional[str] = None,
+    file_comment: Optional[str] = None,
+    timestamp: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Removes a star from an item.
+    https://docs.slack.dev/reference/methods/stars.remove
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "file": file,
+            "file_comment": file_comment,
+            "timestamp": timestamp,
+        }
+    )
+    return self.api_call("stars.remove", params=kwargs)
+
+ +
+
+def team_accessLogs(self,
*,
before: str | int | None = None,
count: str | int | None = None,
page: str | int | None = None,
team_id: str | None = None,
cursor: str | None = None,
limit: int | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def team_accessLogs(
+    self,
+    *,
+    before: Optional[Union[int, str]] = None,
+    count: Optional[Union[int, str]] = None,
+    page: Optional[Union[int, str]] = None,
+    team_id: Optional[str] = None,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Gets the access logs for the current team.
+    https://docs.slack.dev/reference/methods/team.accessLogs
+    """
+    kwargs.update(
+        {
+            "before": before,
+            "count": count,
+            "page": page,
+            "team_id": team_id,
+            "cursor": cursor,
+            "limit": limit,
+        }
+    )
+    return self.api_call("team.accessLogs", http_verb="GET", params=kwargs)
+
+

Gets the access logs for the current team. +https://docs.slack.dev/reference/methods/team.accessLogs

+
+
+def team_billableInfo(self, *, team_id: str | None = None, user: str | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def team_billableInfo(
+    self,
+    *,
+    team_id: Optional[str] = None,
+    user: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Gets billable users information for the current team.
+    https://docs.slack.dev/reference/methods/team.billableInfo
+    """
+    kwargs.update({"team_id": team_id, "user": user})
+    return self.api_call("team.billableInfo", http_verb="GET", params=kwargs)
+
+

Gets billable users information for the current team. +https://docs.slack.dev/reference/methods/team.billableInfo

+
+
+def team_billing_info(self, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def team_billing_info(
+    self,
+    **kwargs,
+) -> SlackResponse:
+    """Reads a workspace's billing plan information.
+    https://docs.slack.dev/reference/methods/team.billing.info
+    """
+    return self.api_call("team.billing.info", params=kwargs)
+
+

Reads a workspace's billing plan information. +https://docs.slack.dev/reference/methods/team.billing.info

+
+
+def team_externalTeams_disconnect(self, *, target_team: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def team_externalTeams_disconnect(
+    self,
+    *,
+    target_team: str,
+    **kwargs,
+) -> SlackResponse:
+    """Disconnects an external organization.
+    https://docs.slack.dev/reference/methods/team.externalTeams.disconnect
+    """
+    kwargs.update(
+        {
+            "target_team": target_team,
+        }
+    )
+    return self.api_call("team.externalTeams.disconnect", params=kwargs)
+
+ +
+
+def team_externalTeams_list(self,
*,
connection_status_filter: str | None = None,
slack_connect_pref_filter: Sequence[str] | None = None,
sort_direction: str | None = None,
sort_field: str | None = None,
workspace_filter: Sequence[str] | None = None,
cursor: str | None = None,
limit: int | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def team_externalTeams_list(
+    self,
+    *,
+    connection_status_filter: Optional[str] = None,
+    slack_connect_pref_filter: Optional[Sequence[str]] = None,
+    sort_direction: Optional[str] = None,
+    sort_field: Optional[str] = None,
+    workspace_filter: Optional[Sequence[str]] = None,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Returns a list of all the external teams connected and details about the connection.
+    https://docs.slack.dev/reference/methods/team.externalTeams.list
+    """
+    kwargs.update(
+        {
+            "connection_status_filter": connection_status_filter,
+            "sort_direction": sort_direction,
+            "sort_field": sort_field,
+            "cursor": cursor,
+            "limit": limit,
+        }
+    )
+    if slack_connect_pref_filter is not None:
+        if isinstance(slack_connect_pref_filter, (list, tuple)):
+            kwargs.update({"slack_connect_pref_filter": ",".join(slack_connect_pref_filter)})
+        else:
+            kwargs.update({"slack_connect_pref_filter": slack_connect_pref_filter})
+    if workspace_filter is not None:
+        if isinstance(workspace_filter, (list, tuple)):
+            kwargs.update({"workspace_filter": ",".join(workspace_filter)})
+        else:
+            kwargs.update({"workspace_filter": workspace_filter})
+    return self.api_call("team.externalTeams.list", http_verb="GET", params=kwargs)
+
+

Returns a list of all the external teams connected and details about the connection. +https://docs.slack.dev/reference/methods/team.externalTeams.list

+
+
+def team_info(self, *, team: str | None = None, domain: str | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def team_info(
+    self,
+    *,
+    team: Optional[str] = None,
+    domain: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Gets information about the current team.
+    https://docs.slack.dev/reference/methods/team.info
+    """
+    kwargs.update({"team": team, "domain": domain})
+    return self.api_call("team.info", http_verb="GET", params=kwargs)
+
+

Gets information about the current team. +https://docs.slack.dev/reference/methods/team.info

+
+
+def team_integrationLogs(self,
*,
app_id: str | None = None,
change_type: str | None = None,
count: str | int | None = None,
page: str | int | None = None,
service_id: str | None = None,
team_id: str | None = None,
user: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def team_integrationLogs(
+    self,
+    *,
+    app_id: Optional[str] = None,
+    change_type: Optional[str] = None,
+    count: Optional[Union[int, str]] = None,
+    page: Optional[Union[int, str]] = None,
+    service_id: Optional[str] = None,
+    team_id: Optional[str] = None,
+    user: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Gets the integration logs for the current team.
+    https://docs.slack.dev/reference/methods/team.integrationLogs
+    """
+    kwargs.update(
+        {
+            "app_id": app_id,
+            "change_type": change_type,
+            "count": count,
+            "page": page,
+            "service_id": service_id,
+            "team_id": team_id,
+            "user": user,
+        }
+    )
+    return self.api_call("team.integrationLogs", http_verb="GET", params=kwargs)
+
+

Gets the integration logs for the current team. +https://docs.slack.dev/reference/methods/team.integrationLogs

+
+
+def team_preferences_list(self, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def team_preferences_list(
+    self,
+    **kwargs,
+) -> SlackResponse:
+    """Retrieve a list of a workspace's team preferences.
+    https://docs.slack.dev/reference/methods/team.preferences.list
+    """
+    return self.api_call("team.preferences.list", params=kwargs)
+
+

Retrieve a list of a workspace's team preferences. +https://docs.slack.dev/reference/methods/team.preferences.list

+
+
+def team_profile_get(self, *, visibility: str | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def team_profile_get(
+    self,
+    *,
+    visibility: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Retrieve a team's profile.
+    https://docs.slack.dev/reference/methods/team.profile.get
+    """
+    kwargs.update({"visibility": visibility})
+    return self.api_call("team.profile.get", http_verb="GET", params=kwargs)
+
+ +
+
+def tooling_tokens_rotate(self, *, refresh_token: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def tooling_tokens_rotate(
+    self,
+    *,
+    refresh_token: str,
+    **kwargs,
+) -> SlackResponse:
+    """Exchanges a refresh token for a new app configuration token
+    https://docs.slack.dev/reference/methods/tooling.tokens.rotate
+    """
+    kwargs.update({"refresh_token": refresh_token})
+    return self.api_call("tooling.tokens.rotate", params=kwargs)
+
+

Exchanges a refresh token for a new app configuration token +https://docs.slack.dev/reference/methods/tooling.tokens.rotate

+
+
+def usergroups_create(self,
*,
name: str,
channels: str | Sequence[str] | None = None,
description: str | None = None,
handle: str | None = None,
include_count: bool | None = None,
team_id: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def usergroups_create(
+    self,
+    *,
+    name: str,
+    channels: Optional[Union[str, Sequence[str]]] = None,
+    description: Optional[str] = None,
+    handle: Optional[str] = None,
+    include_count: Optional[bool] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Create a User Group
+    https://docs.slack.dev/reference/methods/usergroups.create
+    """
+    kwargs.update(
+        {
+            "name": name,
+            "description": description,
+            "handle": handle,
+            "include_count": include_count,
+            "team_id": team_id,
+        }
+    )
+    if isinstance(channels, (list, tuple)):
+        kwargs.update({"channels": ",".join(channels)})
+    else:
+        kwargs.update({"channels": channels})
+    return self.api_call("usergroups.create", params=kwargs)
+
+ +
+
+def usergroups_disable(self,
*,
usergroup: str,
include_count: bool | None = None,
team_id: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def usergroups_disable(
+    self,
+    *,
+    usergroup: str,
+    include_count: Optional[bool] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Disable an existing User Group
+    https://docs.slack.dev/reference/methods/usergroups.disable
+    """
+    kwargs.update({"usergroup": usergroup, "include_count": include_count, "team_id": team_id})
+    return self.api_call("usergroups.disable", params=kwargs)
+
+ +
+
+def usergroups_enable(self,
*,
usergroup: str,
include_count: bool | None = None,
team_id: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def usergroups_enable(
+    self,
+    *,
+    usergroup: str,
+    include_count: Optional[bool] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Enable a User Group
+    https://docs.slack.dev/reference/methods/usergroups.enable
+    """
+    kwargs.update({"usergroup": usergroup, "include_count": include_count, "team_id": team_id})
+    return self.api_call("usergroups.enable", params=kwargs)
+
+ +
+
+def usergroups_list(self,
*,
include_count: bool | None = None,
include_disabled: bool | None = None,
include_users: bool | None = None,
team_id: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def usergroups_list(
+    self,
+    *,
+    include_count: Optional[bool] = None,
+    include_disabled: Optional[bool] = None,
+    include_users: Optional[bool] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """List all User Groups for a team
+    https://docs.slack.dev/reference/methods/usergroups.list
+    """
+    kwargs.update(
+        {
+            "include_count": include_count,
+            "include_disabled": include_disabled,
+            "include_users": include_users,
+            "team_id": team_id,
+        }
+    )
+    return self.api_call("usergroups.list", http_verb="GET", params=kwargs)
+
+ +
+
+def usergroups_update(self,
*,
usergroup: str,
channels: str | Sequence[str] | None = None,
description: str | None = None,
handle: str | None = None,
include_count: bool | None = None,
name: str | None = None,
team_id: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def usergroups_update(
+    self,
+    *,
+    usergroup: str,
+    channels: Optional[Union[str, Sequence[str]]] = None,
+    description: Optional[str] = None,
+    handle: Optional[str] = None,
+    include_count: Optional[bool] = None,
+    name: Optional[str] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Update an existing User Group
+    https://docs.slack.dev/reference/methods/usergroups.update
+    """
+    kwargs.update(
+        {
+            "usergroup": usergroup,
+            "description": description,
+            "handle": handle,
+            "include_count": include_count,
+            "name": name,
+            "team_id": team_id,
+        }
+    )
+    if isinstance(channels, (list, tuple)):
+        kwargs.update({"channels": ",".join(channels)})
+    else:
+        kwargs.update({"channels": channels})
+    return self.api_call("usergroups.update", params=kwargs)
+
+ +
+
+def usergroups_users_list(self,
*,
usergroup: str,
include_disabled: bool | None = None,
team_id: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def usergroups_users_list(
+    self,
+    *,
+    usergroup: str,
+    include_disabled: Optional[bool] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """List all users in a User Group
+    https://docs.slack.dev/reference/methods/usergroups.users.list
+    """
+    kwargs.update(
+        {
+            "usergroup": usergroup,
+            "include_disabled": include_disabled,
+            "team_id": team_id,
+        }
+    )
+    return self.api_call("usergroups.users.list", http_verb="GET", params=kwargs)
+
+ +
+
+def usergroups_users_update(self,
*,
usergroup: str,
users: str | Sequence[str],
include_count: bool | None = None,
team_id: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def usergroups_users_update(
+    self,
+    *,
+    usergroup: str,
+    users: Union[str, Sequence[str]],
+    include_count: Optional[bool] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Update the list of users for a User Group
+    https://docs.slack.dev/reference/methods/usergroups.users.update
+    """
+    kwargs.update(
+        {
+            "usergroup": usergroup,
+            "include_count": include_count,
+            "team_id": team_id,
+        }
+    )
+    if isinstance(users, (list, tuple)):
+        kwargs.update({"users": ",".join(users)})
+    else:
+        kwargs.update({"users": users})
+    return self.api_call("usergroups.users.update", params=kwargs)
+
+

Update the list of users for a User Group +https://docs.slack.dev/reference/methods/usergroups.users.update

+
+
+def users_conversations(self,
*,
cursor: str | None = None,
exclude_archived: bool | None = None,
limit: int | None = None,
team_id: str | None = None,
types: str | Sequence[str] | None = None,
user: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def users_conversations(
+    self,
+    *,
+    cursor: Optional[str] = None,
+    exclude_archived: Optional[bool] = None,
+    limit: Optional[int] = None,
+    team_id: Optional[str] = None,
+    types: Optional[Union[str, Sequence[str]]] = None,
+    user: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """List conversations the calling user may access.
+    https://docs.slack.dev/reference/methods/users.conversations
+    """
+    kwargs.update(
+        {
+            "cursor": cursor,
+            "exclude_archived": exclude_archived,
+            "limit": limit,
+            "team_id": team_id,
+            "user": user,
+        }
+    )
+    if isinstance(types, (list, tuple)):
+        kwargs.update({"types": ",".join(types)})
+    else:
+        kwargs.update({"types": types})
+    return self.api_call("users.conversations", http_verb="GET", params=kwargs)
+
+

List conversations the calling user may access. +https://docs.slack.dev/reference/methods/users.conversations

+
+
+def users_deletePhoto(self, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def users_deletePhoto(
+    self,
+    **kwargs,
+) -> SlackResponse:
+    """Delete the user profile photo
+    https://docs.slack.dev/reference/methods/users.deletePhoto
+    """
+    return self.api_call("users.deletePhoto", http_verb="GET", params=kwargs)
+
+ +
+
+def users_discoverableContacts_lookup(self, email: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def users_discoverableContacts_lookup(
+    self,
+    email: str,
+    **kwargs,
+) -> SlackResponse:
+    """Lookup an email address to see if someone is on Slack
+    https://docs.slack.dev/reference/methods/users.discoverableContacts.lookup
+    """
+    kwargs.update({"email": email})
+    return self.api_call("users.discoverableContacts.lookup", params=kwargs)
+
+

Lookup an email address to see if someone is on Slack +https://docs.slack.dev/reference/methods/users.discoverableContacts.lookup

+
+
+def users_getPresence(self, *, user: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def users_getPresence(
+    self,
+    *,
+    user: str,
+    **kwargs,
+) -> SlackResponse:
+    """Gets user presence information.
+    https://docs.slack.dev/reference/methods/users.getPresence
+    """
+    kwargs.update({"user": user})
+    return self.api_call("users.getPresence", http_verb="GET", params=kwargs)
+
+ +
+
+def users_identity(self, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def users_identity(
+    self,
+    **kwargs,
+) -> SlackResponse:
+    """Get a user's identity.
+    https://docs.slack.dev/reference/methods/users.identity
+    """
+    return self.api_call("users.identity", http_verb="GET", params=kwargs)
+
+ +
+
+def users_info(self, *, user: str, include_locale: bool | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def users_info(
+    self,
+    *,
+    user: str,
+    include_locale: Optional[bool] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Gets information about a user.
+    https://docs.slack.dev/reference/methods/users.info
+    """
+    kwargs.update({"user": user, "include_locale": include_locale})
+    return self.api_call("users.info", http_verb="GET", params=kwargs)
+
+ +
+
+def users_list(self,
*,
cursor: str | None = None,
include_locale: bool | None = None,
limit: int | None = None,
team_id: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def users_list(
+    self,
+    *,
+    cursor: Optional[str] = None,
+    include_locale: Optional[bool] = None,
+    limit: Optional[int] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Lists all users in a Slack team.
+    https://docs.slack.dev/reference/methods/users.list
+    """
+    kwargs.update(
+        {
+            "cursor": cursor,
+            "include_locale": include_locale,
+            "limit": limit,
+            "team_id": team_id,
+        }
+    )
+    return self.api_call("users.list", http_verb="GET", params=kwargs)
+
+

Lists all users in a Slack team. +https://docs.slack.dev/reference/methods/users.list

+
+
+def users_lookupByEmail(self, *, email: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def users_lookupByEmail(
+    self,
+    *,
+    email: str,
+    **kwargs,
+) -> SlackResponse:
+    """Find a user with an email address.
+    https://docs.slack.dev/reference/methods/users.lookupByEmail
+    """
+    kwargs.update({"email": email})
+    return self.api_call("users.lookupByEmail", http_verb="GET", params=kwargs)
+
+ +
+
+def users_profile_get(self, *, user: str | None = None, include_labels: bool | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def users_profile_get(
+    self,
+    *,
+    user: Optional[str] = None,
+    include_labels: Optional[bool] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Retrieves a user's profile information.
+    https://docs.slack.dev/reference/methods/users.profile.get
+    """
+    kwargs.update({"user": user, "include_labels": include_labels})
+    return self.api_call("users.profile.get", http_verb="GET", params=kwargs)
+
+

Retrieves a user's profile information. +https://docs.slack.dev/reference/methods/users.profile.get

+
+
+def users_profile_set(self,
*,
name: str | None = None,
value: str | None = None,
user: str | None = None,
profile: Dict | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def users_profile_set(
+    self,
+    *,
+    name: Optional[str] = None,
+    value: Optional[str] = None,
+    user: Optional[str] = None,
+    profile: Optional[Dict] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Set the profile information for a user.
+    https://docs.slack.dev/reference/methods/users.profile.set
+    """
+    kwargs.update(
+        {
+            "name": name,
+            "profile": profile,
+            "user": user,
+            "value": value,
+        }
+    )
+    kwargs = _remove_none_values(kwargs)
+    # NOTE: Intentionally using json for the "profile" parameter
+    return self.api_call("users.profile.set", json=kwargs)
+
+

Set the profile information for a user. +https://docs.slack.dev/reference/methods/users.profile.set

+
+
+def users_setPhoto(self,
*,
image: str | io.IOBase,
crop_w: str | int | None = None,
crop_x: str | int | None = None,
crop_y: str | int | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def users_setPhoto(
+    self,
+    *,
+    image: Union[str, IOBase],
+    crop_w: Optional[Union[int, str]] = None,
+    crop_x: Optional[Union[int, str]] = None,
+    crop_y: Optional[Union[int, str]] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Set the user profile photo
+    https://docs.slack.dev/reference/methods/users.setPhoto
+    """
+    kwargs.update({"crop_w": crop_w, "crop_x": crop_x, "crop_y": crop_y})
+    return self.api_call("users.setPhoto", files={"image": image}, data=kwargs)
+
+ +
+
+def users_setPresence(self, *, presence: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def users_setPresence(
+    self,
+    *,
+    presence: str,
+    **kwargs,
+) -> SlackResponse:
+    """Manually sets user presence.
+    https://docs.slack.dev/reference/methods/users.setPresence
+    """
+    kwargs.update({"presence": presence})
+    return self.api_call("users.setPresence", params=kwargs)
+
+ +
+
+def views_open(self,
*,
trigger_id: str | None = None,
interactivity_pointer: str | None = None,
view: dict | View,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def views_open(
+    self,
+    *,
+    trigger_id: Optional[str] = None,
+    interactivity_pointer: Optional[str] = None,
+    view: Union[dict, View],
+    **kwargs,
+) -> SlackResponse:
+    """Open a view for a user.
+    https://docs.slack.dev/reference/methods/views.open
+    See https://docs.slack.dev/surfaces/modals/ for details.
+    """
+    kwargs.update({"trigger_id": trigger_id, "interactivity_pointer": interactivity_pointer})
+    if isinstance(view, View):
+        kwargs.update({"view": view.to_dict()})
+    else:
+        kwargs.update({"view": view})
+    kwargs = _remove_none_values(kwargs)
+    # NOTE: Intentionally using json for the "view" parameter
+    return self.api_call("views.open", json=kwargs)
+
+ +
+
+def views_publish(self,
*,
user_id: str,
view: dict | View,
hash: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def views_publish(
+    self,
+    *,
+    user_id: str,
+    view: Union[dict, View],
+    hash: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Publish a static view for a User.
+    Create or update the view that comprises an
+    app's Home tab (https://docs.slack.dev/surfaces/app-home/)
+    https://docs.slack.dev/reference/methods/views.publish
+    """
+    kwargs.update({"user_id": user_id, "hash": hash})
+    if isinstance(view, View):
+        kwargs.update({"view": view.to_dict()})
+    else:
+        kwargs.update({"view": view})
+    kwargs = _remove_none_values(kwargs)
+    # NOTE: Intentionally using json for the "view" parameter
+    return self.api_call("views.publish", json=kwargs)
+
+

Publish a static view for a User. +Create or update the view that comprises an +app's Home tab (https://docs.slack.dev/surfaces/app-home/) +https://docs.slack.dev/reference/methods/views.publish

+
+
+def views_push(self,
*,
trigger_id: str | None = None,
interactivity_pointer: str | None = None,
view: dict | View,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def views_push(
+    self,
+    *,
+    trigger_id: Optional[str] = None,
+    interactivity_pointer: Optional[str] = None,
+    view: Union[dict, View],
+    **kwargs,
+) -> SlackResponse:
+    """Push a view onto the stack of a root view.
+    Push a new view onto the existing view stack by passing a view
+    payload and a valid trigger_id generated from an interaction
+    within the existing modal.
+    Read the modals documentation (https://docs.slack.dev/surfaces/modals/)
+    to learn more about the lifecycle and intricacies of views.
+    https://docs.slack.dev/reference/methods/views.push
+    """
+    kwargs.update({"trigger_id": trigger_id, "interactivity_pointer": interactivity_pointer})
+    if isinstance(view, View):
+        kwargs.update({"view": view.to_dict()})
+    else:
+        kwargs.update({"view": view})
+    kwargs = _remove_none_values(kwargs)
+    # NOTE: Intentionally using json for the "view" parameter
+    return self.api_call("views.push", json=kwargs)
+
+

Push a view onto the stack of a root view. +Push a new view onto the existing view stack by passing a view +payload and a valid trigger_id generated from an interaction +within the existing modal. +Read the modals documentation (https://docs.slack.dev/surfaces/modals/) +to learn more about the lifecycle and intricacies of views. +https://docs.slack.dev/reference/methods/views.push

+
+
+def views_update(self,
*,
view: dict | View,
external_id: str | None = None,
view_id: str | None = None,
hash: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def views_update(
+    self,
+    *,
+    view: Union[dict, View],
+    external_id: Optional[str] = None,
+    view_id: Optional[str] = None,
+    hash: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Update an existing view.
+    Update a view by passing a new view definition along with the
+    view_id returned in views.open or the external_id.
+    See the modals documentation (https://docs.slack.dev/surfaces/modals/#updating_views)
+    to learn more about updating views and avoiding race conditions with the hash argument.
+    https://docs.slack.dev/reference/methods/views.update
+    """
+    if isinstance(view, View):
+        kwargs.update({"view": view.to_dict()})
+    else:
+        kwargs.update({"view": view})
+    if external_id:
+        kwargs.update({"external_id": external_id})
+    elif view_id:
+        kwargs.update({"view_id": view_id})
+    else:
+        raise e.SlackRequestError("Either view_id or external_id is required.")
+    kwargs.update({"hash": hash})
+    kwargs = _remove_none_values(kwargs)
+    # NOTE: Intentionally using json for the "view" parameter
+    return self.api_call("views.update", json=kwargs)
+
+

Update an existing view. +Update a view by passing a new view definition along with the +view_id returned in views.open or the external_id. +See the modals documentation (https://docs.slack.dev/surfaces/modals/#updating_views) +to learn more about updating views and avoiding race conditions with the hash argument. +https://docs.slack.dev/reference/methods/views.update

+
+ +
+
+ +Expand source code + +
def workflows_featured_add(
+    self,
+    *,
+    channel_id: str,
+    trigger_ids: Union[str, Sequence[str]],
+    **kwargs,
+) -> SlackResponse:
+    """Add featured workflows to a channel.
+    https://docs.slack.dev/reference/methods/workflows.featured.add
+    """
+    kwargs.update({"channel_id": channel_id})
+    if isinstance(trigger_ids, (list, tuple)):
+        kwargs.update({"trigger_ids": ",".join(trigger_ids)})
+    else:
+        kwargs.update({"trigger_ids": trigger_ids})
+    return self.api_call("workflows.featured.add", params=kwargs)
+
+ +
+ +
+
+ +Expand source code + +
def workflows_featured_list(
+    self,
+    *,
+    channel_ids: Union[str, Sequence[str]],
+    **kwargs,
+) -> SlackResponse:
+    """List the featured workflows for specified channels.
+    https://docs.slack.dev/reference/methods/workflows.featured.list
+    """
+    if isinstance(channel_ids, (list, tuple)):
+        kwargs.update({"channel_ids": ",".join(channel_ids)})
+    else:
+        kwargs.update({"channel_ids": channel_ids})
+    return self.api_call("workflows.featured.list", params=kwargs)
+
+

List the featured workflows for specified channels. +https://docs.slack.dev/reference/methods/workflows.featured.list

+
+ +
+
+ +Expand source code + +
def workflows_featured_remove(
+    self,
+    *,
+    channel_id: str,
+    trigger_ids: Union[str, Sequence[str]],
+    **kwargs,
+) -> SlackResponse:
+    """Remove featured workflows from a channel.
+    https://docs.slack.dev/reference/methods/workflows.featured.remove
+    """
+    kwargs.update({"channel_id": channel_id})
+    if isinstance(trigger_ids, (list, tuple)):
+        kwargs.update({"trigger_ids": ",".join(trigger_ids)})
+    else:
+        kwargs.update({"trigger_ids": trigger_ids})
+    return self.api_call("workflows.featured.remove", params=kwargs)
+
+ +
+ +
+
+ +Expand source code + +
def workflows_featured_set(
+    self,
+    *,
+    channel_id: str,
+    trigger_ids: Union[str, Sequence[str]],
+    **kwargs,
+) -> SlackResponse:
+    """Set featured workflows for a channel.
+    https://docs.slack.dev/reference/methods/workflows.featured.set
+    """
+    kwargs.update({"channel_id": channel_id})
+    if isinstance(trigger_ids, (list, tuple)):
+        kwargs.update({"trigger_ids": ",".join(trigger_ids)})
+    else:
+        kwargs.update({"trigger_ids": trigger_ids})
+    return self.api_call("workflows.featured.set", params=kwargs)
+
+ +
+
+def workflows_stepCompleted(self, *, workflow_step_execute_id: str, outputs: dict | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def workflows_stepCompleted(
+    self,
+    *,
+    workflow_step_execute_id: str,
+    outputs: Optional[dict] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Indicate a successful outcome of a workflow step's execution.
+    https://docs.slack.dev/reference/methods/workflows.stepCompleted
+    """
+    kwargs.update({"workflow_step_execute_id": workflow_step_execute_id})
+    if outputs is not None:
+        kwargs.update({"outputs": outputs})
+    kwargs = _remove_none_values(kwargs)
+    # NOTE: Intentionally using json for the "outputs" parameter
+    return self.api_call("workflows.stepCompleted", json=kwargs)
+
+

Indicate a successful outcome of a workflow step's execution. +https://docs.slack.dev/reference/methods/workflows.stepCompleted

+
+
+def workflows_stepFailed(self, *, workflow_step_execute_id: str, error: Dict[str, str], **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def workflows_stepFailed(
+    self,
+    *,
+    workflow_step_execute_id: str,
+    error: Dict[str, str],
+    **kwargs,
+) -> SlackResponse:
+    """Indicate an unsuccessful outcome of a workflow step's execution.
+    https://docs.slack.dev/reference/methods/workflows.stepFailed
+    """
+    kwargs.update(
+        {
+            "workflow_step_execute_id": workflow_step_execute_id,
+            "error": error,
+        }
+    )
+    kwargs = _remove_none_values(kwargs)
+    # NOTE: Intentionally using json for the "error" parameter
+    return self.api_call("workflows.stepFailed", json=kwargs)
+
+

Indicate an unsuccessful outcome of a workflow step's execution. +https://docs.slack.dev/reference/methods/workflows.stepFailed

+
+
+def workflows_updateStep(self,
*,
workflow_step_edit_id: str,
inputs: Dict[str, Any] | None = None,
outputs: List[Dict[str, str]] | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def workflows_updateStep(
+    self,
+    *,
+    workflow_step_edit_id: str,
+    inputs: Optional[Dict[str, Any]] = None,
+    outputs: Optional[List[Dict[str, str]]] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Update the configuration for a workflow extension step.
+    https://docs.slack.dev/reference/methods/workflows.updateStep
+    """
+    kwargs.update({"workflow_step_edit_id": workflow_step_edit_id})
+    if inputs is not None:
+        kwargs.update({"inputs": inputs})
+    if outputs is not None:
+        kwargs.update({"outputs": outputs})
+    kwargs = _remove_none_values(kwargs)
+    # NOTE: Intentionally using json for the "inputs" / "outputs" parameters
+    return self.api_call("workflows.updateStep", json=kwargs)
+
+

Update the configuration for a workflow extension step. +https://docs.slack.dev/reference/methods/workflows.updateStep

+
+
+

Inherited members

+ +
+
+class WebhookClient +(url: str,
timeout: int = 30,
ssl: ssl.SSLContext | None = None,
proxy: str | None = None,
default_headers: Dict[str, str] | None = None,
user_agent_prefix: str | None = None,
user_agent_suffix: str | None = None,
logger: logging.Logger | None = None,
retry_handlers: List[RetryHandler] | None = None)
+
+
+
+ +Expand source code + +
class WebhookClient:
+    url: str
+    timeout: int
+    ssl: Optional[SSLContext]
+    proxy: Optional[str]
+    default_headers: Dict[str, str]
+    logger: logging.Logger
+    retry_handlers: List[RetryHandler]
+
+    def __init__(
+        self,
+        url: str,
+        timeout: int = 30,
+        ssl: Optional[SSLContext] = None,
+        proxy: Optional[str] = None,
+        default_headers: Optional[Dict[str, str]] = None,
+        user_agent_prefix: Optional[str] = None,
+        user_agent_suffix: Optional[str] = None,
+        logger: Optional[logging.Logger] = None,
+        retry_handlers: Optional[List[RetryHandler]] = None,
+    ):
+        """API client for Incoming Webhooks and `response_url`
+
+        https://docs.slack.dev/messaging/sending-messages-using-incoming-webhooks/
+
+        Args:
+            url: Complete URL to send data (e.g., `https://hooks.slack.com/XXX`)
+            timeout: Request timeout (in seconds)
+            ssl: `ssl.SSLContext` to use for requests
+            proxy: Proxy URL (e.g., `localhost:9000`, `http://localhost:9000`)
+            default_headers: Request headers to add to all requests
+            user_agent_prefix: Prefix for User-Agent header value
+            user_agent_suffix: Suffix for User-Agent header value
+            logger: Custom logger
+            retry_handlers: Retry handlers
+        """
+        self.url = url
+        self.timeout = timeout
+        self.ssl = ssl
+        self.proxy = proxy
+        self.default_headers = default_headers if default_headers else {}
+        self.default_headers["User-Agent"] = get_user_agent(user_agent_prefix, user_agent_suffix)
+        self.logger = logger if logger is not None else logging.getLogger(__name__)
+        self.retry_handlers = retry_handlers if retry_handlers is not None else default_retry_handlers()
+
+        if self.proxy is None or len(self.proxy.strip()) == 0:
+            env_variable = load_http_proxy_from_env(self.logger)
+            if env_variable is not None:
+                self.proxy = env_variable
+
+    def send(
+        self,
+        *,
+        text: Optional[str] = None,
+        attachments: Optional[Sequence[Union[Dict[str, Any], Attachment]]] = None,
+        blocks: Optional[Sequence[Union[Dict[str, Any], Block]]] = None,
+        response_type: Optional[str] = None,
+        replace_original: Optional[bool] = None,
+        delete_original: Optional[bool] = None,
+        unfurl_links: Optional[bool] = None,
+        unfurl_media: Optional[bool] = None,
+        metadata: Optional[Dict[str, Any]] = None,
+        headers: Optional[Dict[str, str]] = None,
+    ) -> WebhookResponse:
+        """Performs a Slack API request and returns the result.
+
+        Args:
+            text: The text message
+                (even when having blocks, setting this as well is recommended as it works as fallback)
+            attachments: A collection of attachments
+            blocks: A collection of Block Kit UI components
+            response_type: The type of message (either 'in_channel' or 'ephemeral')
+            replace_original: True if you use this option for response_url requests
+            delete_original: True if you use this option for response_url requests
+            unfurl_links: Option to indicate whether text url should unfurl
+            unfurl_media: Option to indicate whether media url should unfurl
+            metadata: Metadata attached to the message
+            headers: Request headers to append only for this request
+
+        Returns:
+            Webhook response
+        """
+        return self.send_dict(
+            # It's fine to have None value elements here
+            # because _build_body() filters them out when constructing the actual body data
+            body={
+                "text": text,
+                "attachments": attachments,
+                "blocks": blocks,
+                "response_type": response_type,
+                "replace_original": replace_original,
+                "delete_original": delete_original,
+                "unfurl_links": unfurl_links,
+                "unfurl_media": unfurl_media,
+                "metadata": metadata,
+            },
+            headers=headers,
+        )
+
+    def send_dict(self, body: Dict[str, Any], headers: Optional[Dict[str, str]] = None) -> WebhookResponse:
+        """Performs a Slack API request and returns the result.
+
+        Args:
+            body: JSON data structure (it's still a dict at this point),
+                if you give this argument, body_params and files will be skipped
+            headers: Request headers to append only for this request
+        Returns:
+            Webhook response
+        """
+        return self._perform_http_request(
+            body=_build_body(body),  # type: ignore[arg-type]
+            headers=_build_request_headers(self.default_headers, headers),
+        )
+
+    def _perform_http_request(self, *, body: Dict[str, Any], headers: Dict[str, str]) -> WebhookResponse:
+        raw_body = json.dumps(body)
+        headers["Content-Type"] = "application/json;charset=utf-8"
+
+        if self.logger.level <= logging.DEBUG:
+            self.logger.debug(f"Sending a request - url: {self.url}, body: {raw_body}, headers: {headers}")
+
+        url = self.url
+        # NOTE: Intentionally ignore the `http_verb` here
+        # Slack APIs accepts any API method requests with POST methods
+        req = Request(method="POST", url=url, data=raw_body.encode("utf-8"), headers=headers)
+        resp = None
+        last_error = Exception("undefined internal error")
+
+        retry_state = RetryState()
+        counter_for_safety = 0
+        while counter_for_safety < 100:
+            counter_for_safety += 1
+            # If this is a retry, the next try started here. We can reset the flag.
+            retry_state.next_attempt_requested = False
+
+            try:
+                resp = self._perform_http_request_internal(url, req)
+                # The resp is a 200 OK response
+                return resp
+
+            except HTTPError as e:
+                # read the response body here
+                charset = e.headers.get_content_charset() or "utf-8"
+                response_body: str = e.read().decode(charset)
+                # As adding new values to HTTPError#headers can be ignored, building a new dict object here
+                response_headers = dict(e.headers.items())
+                resp = WebhookResponse(
+                    url=url,
+                    status_code=e.code,
+                    body=response_body,
+                    headers=response_headers,
+                )
+                if e.code == 429:
+                    # for backward-compatibility with WebClient (v.2.5.0 or older)
+                    if "retry-after" not in resp.headers and "Retry-After" in resp.headers:
+                        resp.headers["retry-after"] = resp.headers["Retry-After"]
+                    if "Retry-After" not in resp.headers and "retry-after" in resp.headers:
+                        resp.headers["Retry-After"] = resp.headers["retry-after"]
+                _debug_log_response(self.logger, resp)
+
+                # Try to find a retry handler for this error
+                retry_request = RetryHttpRequest.from_urllib_http_request(req)
+                retry_response = RetryHttpResponse(
+                    status_code=e.code,
+                    headers={k: [v] for k, v in e.headers.items()},
+                    data=response_body.encode("utf-8") if response_body is not None else None,
+                )
+                for handler in self.retry_handlers:
+                    if handler.can_retry(
+                        state=retry_state,
+                        request=retry_request,
+                        response=retry_response,
+                        error=e,
+                    ):
+                        if self.logger.level <= logging.DEBUG:
+                            self.logger.info(
+                                f"A retry handler found: {type(handler).__name__} for {req.method} {req.full_url} - {e}"
+                            )
+                        handler.prepare_for_next_attempt(
+                            state=retry_state,
+                            request=retry_request,
+                            response=retry_response,
+                            error=e,
+                        )
+                        break
+
+                if retry_state.next_attempt_requested is False:
+                    return resp
+
+            except Exception as err:
+                last_error = err
+                self.logger.error(f"Failed to send a request to Slack API server: {err}")
+
+                # Try to find a retry handler for this error
+                retry_request = RetryHttpRequest.from_urllib_http_request(req)
+                for handler in self.retry_handlers:
+                    if handler.can_retry(
+                        state=retry_state,
+                        request=retry_request,
+                        response=None,
+                        error=err,
+                    ):
+                        if self.logger.level <= logging.DEBUG:
+                            self.logger.info(
+                                f"A retry handler found: {type(handler).__name__} for {req.method} {req.full_url} - {err}"
+                            )
+                        handler.prepare_for_next_attempt(
+                            state=retry_state,
+                            request=retry_request,
+                            response=None,
+                            error=err,
+                        )
+                        self.logger.info(f"Going to retry the same request: {req.method} {req.full_url}")
+                        break
+
+                if retry_state.next_attempt_requested is False:
+                    raise err
+
+        if resp is not None:
+            return resp
+        raise last_error
+
+    def _perform_http_request_internal(self, url: str, req: Request):
+        opener: Optional[OpenerDirector] = None
+        # for security (BAN-B310)
+        if url.lower().startswith("http"):
+            if self.proxy is not None:
+                if isinstance(self.proxy, str):
+                    opener = urllib.request.build_opener(
+                        ProxyHandler({"http": self.proxy, "https": self.proxy}),
+                        HTTPSHandler(context=self.ssl),
+                    )
+                else:
+                    raise SlackRequestError(f"Invalid proxy detected: {self.proxy} must be a str value")
+        else:
+            raise SlackRequestError(f"Invalid URL detected: {url}")
+
+        http_resp: Optional[HTTPResponse] = None
+        if opener:
+            http_resp = opener.open(req, timeout=self.timeout)
+        else:
+            http_resp = urlopen(req, context=self.ssl, timeout=self.timeout)
+        charset: str = http_resp.headers.get_content_charset() or "utf-8"
+        response_body: str = http_resp.read().decode(charset)
+        resp = WebhookResponse(
+            url=url,
+            status_code=http_resp.status,
+            body=response_body,
+            headers=http_resp.headers,  # type: ignore[arg-type]
+        )
+        _debug_log_response(self.logger, resp)
+        return resp
+
+

API client for Incoming Webhooks and response_url

+

https://docs.slack.dev/messaging/sending-messages-using-incoming-webhooks/

+

Args

+
+
url
+
Complete URL to send data (e.g., https://hooks.slack.com/XXX)
+
timeout
+
Request timeout (in seconds)
+
ssl
+
ssl.SSLContext to use for requests
+
proxy
+
Proxy URL (e.g., localhost:9000, http://localhost:9000)
+
default_headers
+
Request headers to add to all requests
+
user_agent_prefix
+
Prefix for User-Agent header value
+
user_agent_suffix
+
Suffix for User-Agent header value
+
logger
+
Custom logger
+
retry_handlers
+
Retry handlers
+
+

Class variables

+
+
var default_headers : Dict[str, str]
+
+

The type of the None singleton.

+
+
var logger : logging.Logger
+
+

The type of the None singleton.

+
+
var proxy : str | None
+
+

The type of the None singleton.

+
+
var retry_handlers : List[RetryHandler]
+
+

The type of the None singleton.

+
+
var ssl : ssl.SSLContext | None
+
+

The type of the None singleton.

+
+
var timeout : int
+
+

The type of the None singleton.

+
+
var url : str
+
+

The type of the None singleton.

+
+
+

Methods

+
+
+def send(self,
*,
text: str | None = None,
attachments: Sequence[Dict[str, Any] | Attachment] | None = None,
blocks: Sequence[Dict[str, Any] | Block] | None = None,
response_type: str | None = None,
replace_original: bool | None = None,
delete_original: bool | None = None,
unfurl_links: bool | None = None,
unfurl_media: bool | None = None,
metadata: Dict[str, Any] | None = None,
headers: Dict[str, str] | None = None) ‑> WebhookResponse
+
+
+
+ +Expand source code + +
def send(
+    self,
+    *,
+    text: Optional[str] = None,
+    attachments: Optional[Sequence[Union[Dict[str, Any], Attachment]]] = None,
+    blocks: Optional[Sequence[Union[Dict[str, Any], Block]]] = None,
+    response_type: Optional[str] = None,
+    replace_original: Optional[bool] = None,
+    delete_original: Optional[bool] = None,
+    unfurl_links: Optional[bool] = None,
+    unfurl_media: Optional[bool] = None,
+    metadata: Optional[Dict[str, Any]] = None,
+    headers: Optional[Dict[str, str]] = None,
+) -> WebhookResponse:
+    """Performs a Slack API request and returns the result.
+
+    Args:
+        text: The text message
+            (even when having blocks, setting this as well is recommended as it works as fallback)
+        attachments: A collection of attachments
+        blocks: A collection of Block Kit UI components
+        response_type: The type of message (either 'in_channel' or 'ephemeral')
+        replace_original: True if you use this option for response_url requests
+        delete_original: True if you use this option for response_url requests
+        unfurl_links: Option to indicate whether text url should unfurl
+        unfurl_media: Option to indicate whether media url should unfurl
+        metadata: Metadata attached to the message
+        headers: Request headers to append only for this request
+
+    Returns:
+        Webhook response
+    """
+    return self.send_dict(
+        # It's fine to have None value elements here
+        # because _build_body() filters them out when constructing the actual body data
+        body={
+            "text": text,
+            "attachments": attachments,
+            "blocks": blocks,
+            "response_type": response_type,
+            "replace_original": replace_original,
+            "delete_original": delete_original,
+            "unfurl_links": unfurl_links,
+            "unfurl_media": unfurl_media,
+            "metadata": metadata,
+        },
+        headers=headers,
+    )
+
+

Performs a Slack API request and returns the result.

+

Args

+
+
text
+
The text message +(even when having blocks, setting this as well is recommended as it works as fallback)
+
attachments
+
A collection of attachments
+
blocks
+
A collection of Block Kit UI components
+
response_type
+
The type of message (either 'in_channel' or 'ephemeral')
+
replace_original
+
True if you use this option for response_url requests
+
delete_original
+
True if you use this option for response_url requests
+
unfurl_links
+
Option to indicate whether text url should unfurl
+
unfurl_media
+
Option to indicate whether media url should unfurl
+
metadata
+
Metadata attached to the message
+
headers
+
Request headers to append only for this request
+
+

Returns

+

Webhook response

+
+
+def send_dict(self, body: Dict[str, Any], headers: Dict[str, str] | None = None) ‑> WebhookResponse +
+
+
+ +Expand source code + +
def send_dict(self, body: Dict[str, Any], headers: Optional[Dict[str, str]] = None) -> WebhookResponse:
+    """Performs a Slack API request and returns the result.
+
+    Args:
+        body: JSON data structure (it's still a dict at this point),
+            if you give this argument, body_params and files will be skipped
+        headers: Request headers to append only for this request
+    Returns:
+        Webhook response
+    """
+    return self._perform_http_request(
+        body=_build_body(body),  # type: ignore[arg-type]
+        headers=_build_request_headers(self.default_headers, headers),
+    )
+
+

Performs a Slack API request and returns the result.

+

Args

+
+
body
+
JSON data structure (it's still a dict at this point), +if you give this argument, body_params and files will be skipped
+
headers
+
Request headers to append only for this request
+
+

Returns

+

Webhook response

+
+
+
+
+
+
+ +
+ + + diff --git a/docs/reference/models/attachments/index.html b/docs/reference/models/attachments/index.html new file mode 100644 index 000000000..5a7d10c20 --- /dev/null +++ b/docs/reference/models/attachments/index.html @@ -0,0 +1,1737 @@ + + + + + + +slack_sdk.models.attachments API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.models.attachments

+
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class AbstractActionSelector +(*,
name: str,
text: str,
selected_option: Option | None = None)
+
+
+
+ +Expand source code + +
class AbstractActionSelector(Action, metaclass=ABCMeta):
+    DataSourceTypes = DynamicSelectElementTypes.union({"external", "static"})
+
+    attributes = {"data_source", "name", "text", "type"}
+
+    @property
+    @abstractmethod
+    def data_source(self) -> str:
+        pass
+
+    def __init__(self, *, name: str, text: str, selected_option: Optional[Option] = None):
+        super().__init__(text=text, name=name, subtype="select")
+        self.selected_option = selected_option
+
+    @EnumValidator("data_source", DataSourceTypes)
+    def data_source_valid(self):
+        return self.data_source in self.DataSourceTypes
+
+    def to_dict(self) -> dict:
+        json = super().to_dict()
+        if self.selected_option is not None:
+            # this is a special case for ExternalActionSelectElement - in that case,
+            # you pass the initial value of the selector as a selected_options array
+            json["selected_options"] = extract_json([self.selected_option], "action")
+        return json
+
+ +

Ancestors

+ +

Subclasses

+ +

Class variables

+
+
var DataSourceTypes
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
prop data_source : str
+
+
+ +Expand source code + +
@property
+@abstractmethod
+def data_source(self) -> str:
+    pass
+
+
+
+
+

Methods

+
+
+def data_source_valid(self) +
+
+
+ +Expand source code + +
@EnumValidator("data_source", DataSourceTypes)
+def data_source_valid(self):
+    return self.data_source in self.DataSourceTypes
+
+
+
+
+

Inherited members

+ +
+
+class Action +(*, text: str, subtype: str, name: str | None = None, url: str | None = None) +
+
+
+ +Expand source code + +
class Action(JsonObject):
+    """Action in attachments
+    https://docs.slack.dev/messaging/formatting-message-text/#rich-layouts
+    https://docs.slack.dev/legacy/legacy-messaging/legacy-interactive-message-field-guide/#message_action_fields
+    """
+
+    attributes = {"name", "text", "url"}
+
+    def __init__(
+        self,
+        *,
+        text: str,
+        subtype: str,
+        name: Optional[str] = None,
+        url: Optional[str] = None,
+    ):
+        self.name = name
+        self.url = url
+        self.text = text
+        self.subtype = subtype
+
+    @JsonValidator("name or url attribute is required")
+    def name_or_url_present(self):
+        return self.name is not None or self.url is not None
+
+    def to_dict(self) -> dict:
+        json = super().to_dict()
+        json["type"] = self.subtype
+        return json
+
+ +

Ancestors

+ +

Subclasses

+ +

Class variables

+
+
var attributes
+
+

The type of the None singleton.

+
+
+

Methods

+
+
+def name_or_url_present(self) +
+
+
+ +Expand source code + +
@JsonValidator("name or url attribute is required")
+def name_or_url_present(self):
+    return self.name is not None or self.url is not None
+
+
+
+
+

Inherited members

+ +
+
+class ActionButton +(*,
name: str,
text: str,
value: str,
confirm: ConfirmObject | None = None,
style: str | None = None)
+
+
+
+ +Expand source code + +
class ActionButton(Action):
+    @property
+    def attributes(self):
+        return super().attributes.union({"style", "value"})
+
+    value_max_length = 2000
+
+    def __init__(
+        self,
+        *,
+        name: str,
+        text: str,
+        value: str,
+        confirm: Optional[ConfirmObject] = None,
+        style: Optional[str] = None,
+    ):
+        """Simple button for use inside attachments
+
+        https://docs.slack.dev/legacy/legacy-messaging/legacy-message-buttons/
+
+        Args:
+            name: Name this specific action. The name will be returned to your
+                Action URL along with the message's callback_id when this action is
+                invoked. Use it to identify this particular response path.
+            text: The user-facing label for the message button or menu
+                representing this action. Cannot contain markup.
+            value: Provide a string identifying this specific action. It will be
+                sent to your Action URL along with the name and attachment's
+                callback_id . If providing multiple actions with the same name, value
+                can be strategically used to differentiate intent. Cannot exceed 2000
+                characters.
+            confirm: a ConfirmObject that will appear in a dialog to confirm
+                user's choice.
+            style: Leave blank to indicate that this is an ordinary button. Use
+                "primary" or "danger" to mark important buttons.
+        """
+        super().__init__(name=name, text=text, subtype="button")
+        self.value = value
+        self.confirm = confirm
+        self.style = style
+
+    @JsonValidator(f"value attribute cannot exceed {value_max_length} characters")
+    def value_length(self):
+        return len(self.value) <= self.value_max_length
+
+    @EnumValidator("style", ButtonStyles)
+    def style_valid(self):
+        return self.style is None or self.style in ButtonStyles
+
+    def to_dict(self) -> dict:
+        json = super().to_dict()
+        if self.confirm is not None:
+            json["confirm"] = extract_json(self.confirm, "action")
+        return json
+
+

Action in attachments +https://docs.slack.dev/messaging/formatting-message-text/#rich-layouts +https://docs.slack.dev/legacy/legacy-messaging/legacy-interactive-message-field-guide/#message_action_fields

+

Simple button for use inside attachments

+

https://docs.slack.dev/legacy/legacy-messaging/legacy-message-buttons/

+

Args

+
+
name
+
Name this specific action. The name will be returned to your +Action URL along with the message's callback_id when this action is +invoked. Use it to identify this particular response path.
+
text
+
The user-facing label for the message button or menu +representing this action. Cannot contain markup.
+
value
+
Provide a string identifying this specific action. It will be +sent to your Action URL along with the name and attachment's +callback_id . If providing multiple actions with the same name, value +can be strategically used to differentiate intent. Cannot exceed 2000 +characters.
+
confirm
+
a ConfirmObject that will appear in a dialog to confirm +user's choice.
+
style
+
Leave blank to indicate that this is an ordinary button. Use +"primary" or "danger" to mark important buttons.
+
+

Ancestors

+ +

Class variables

+
+
var value_max_length
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
prop attributes
+
+
+ +Expand source code + +
@property
+def attributes(self):
+    return super().attributes.union({"style", "value"})
+
+

Build an unordered collection of unique elements.

+
+
+

Methods

+
+
+def style_valid(self) +
+
+
+ +Expand source code + +
@EnumValidator("style", ButtonStyles)
+def style_valid(self):
+    return self.style is None or self.style in ButtonStyles
+
+
+
+
+def value_length(self) +
+
+
+ +Expand source code + +
@JsonValidator(f"value attribute cannot exceed {value_max_length} characters")
+def value_length(self):
+    return len(self.value) <= self.value_max_length
+
+
+
+
+

Inherited members

+ +
+
+class ActionChannelSelector +(name: str,
text: str,
selected_channel: Option | None = None)
+
+
+
+ +Expand source code + +
class ActionChannelSelector(AbstractActionSelector):
+    data_source = "channels"
+
+    def __init__(self, name: str, text: str, selected_channel: Optional[Option] = None):
+        """
+        Automatically populate the selector with a list of public channels in the
+        workspace.
+
+        https://docs.slack.dev/legacy/legacy-messaging/legacy-adding-menus-to-messages/#menu_channels
+
+        Args:
+            name: Name this specific action. The name will be returned to your
+                Action URL along with the message's callback_id when this action is
+                invoked. Use it to identify this particular response path.
+            text: The user-facing label for the message button or menu
+                representing this action. Cannot contain markup.
+            selected_channel: An Option object to pre-select as the default
+                value.
+        """
+        super().__init__(name=name, text=text, selected_option=selected_channel)
+
+

Action in attachments +https://docs.slack.dev/messaging/formatting-message-text/#rich-layouts +https://docs.slack.dev/legacy/legacy-messaging/legacy-interactive-message-field-guide/#message_action_fields

+

Automatically populate the selector with a list of public channels in the +workspace.

+

https://docs.slack.dev/legacy/legacy-messaging/legacy-adding-menus-to-messages/#menu_channels

+

Args

+
+
name
+
Name this specific action. The name will be returned to your +Action URL along with the message's callback_id when this action is +invoked. Use it to identify this particular response path.
+
text
+
The user-facing label for the message button or menu +representing this action. Cannot contain markup.
+
selected_channel
+
An Option object to pre-select as the default +value.
+
+

Ancestors

+ +

Class variables

+
+
var data_source
+
+

The type of the None singleton.

+
+
+

Inherited members

+ +
+
+class ActionConversationSelector +(name: str,
text: str,
selected_conversation: Option | None = None)
+
+
+
+ +Expand source code + +
class ActionConversationSelector(AbstractActionSelector):
+    data_source = "conversations"
+
+    def __init__(self, name: str, text: str, selected_conversation: Optional[Option] = None):
+        """
+        Automatically populate the selector with a list of conversations they have in
+        the workspace.
+
+        https://docs.slack.dev/legacy/legacy-messaging/legacy-adding-menus-to-messages/#menu_conversations
+
+        Args:
+            name: Name this specific action. The name will be returned to your
+                Action URL along with the message's callback_id when this action is
+                invoked. Use it to identify this particular response path.
+            text: The user-facing label for the message button or menu
+                representing this action. Cannot contain markup.
+            selected_conversation: An Option object to pre-select as the default
+                value.
+        """
+        super().__init__(name=name, text=text, selected_option=selected_conversation)
+
+

Action in attachments +https://docs.slack.dev/messaging/formatting-message-text/#rich-layouts +https://docs.slack.dev/legacy/legacy-messaging/legacy-interactive-message-field-guide/#message_action_fields

+

Automatically populate the selector with a list of conversations they have in +the workspace.

+

https://docs.slack.dev/legacy/legacy-messaging/legacy-adding-menus-to-messages/#menu_conversations

+

Args

+
+
name
+
Name this specific action. The name will be returned to your +Action URL along with the message's callback_id when this action is +invoked. Use it to identify this particular response path.
+
text
+
The user-facing label for the message button or menu +representing this action. Cannot contain markup.
+
selected_conversation
+
An Option object to pre-select as the default +value.
+
+

Ancestors

+ +

Class variables

+
+
var data_source
+
+

The type of the None singleton.

+
+
+

Inherited members

+ +
+
+class ActionExternalSelector +(*,
name: str,
text: str,
selected_option: Option | None = None,
min_query_length: int | None = None)
+
+
+
+ +Expand source code + +
class ActionExternalSelector(AbstractActionSelector):
+    data_source = "external"
+
+    @property
+    def attributes(self) -> Set[str]:  # type: ignore[override]
+        return super().attributes.union({"min_query_length"})
+
+    def __init__(
+        self,
+        *,
+        name: str,
+        text: str,
+        selected_option: Optional[Option] = None,
+        min_query_length: Optional[int] = None,
+    ):
+        """
+        Populate a message select menu from your own application dynamically.
+
+        https://docs.slack.dev/legacy/legacy-messaging/legacy-adding-menus-to-messages/#menu_dynamic
+
+        Args:
+            name: Name this specific action. The name will be returned to your
+                Action URL along with the message's callback_id when this action is
+                invoked. Use it to identify this particular response path.
+            text: The user-facing label for the message button or menu
+                representing this action. Cannot contain markup.
+            selected_option: An Option object to pre-select as the default
+                value.
+            min_query_length: Specify the number of characters that must be typed
+                by a user into a dynamic select menu before dispatching to the app.
+        """
+        super().__init__(name=name, text=text, selected_option=selected_option)
+        self.min_query_length = min_query_length
+
+

Action in attachments +https://docs.slack.dev/messaging/formatting-message-text/#rich-layouts +https://docs.slack.dev/legacy/legacy-messaging/legacy-interactive-message-field-guide/#message_action_fields

+

Populate a message select menu from your own application dynamically.

+

https://docs.slack.dev/legacy/legacy-messaging/legacy-adding-menus-to-messages/#menu_dynamic

+

Args

+
+
name
+
Name this specific action. The name will be returned to your +Action URL along with the message's callback_id when this action is +invoked. Use it to identify this particular response path.
+
text
+
The user-facing label for the message button or menu +representing this action. Cannot contain markup.
+
selected_option
+
An Option object to pre-select as the default +value.
+
min_query_length
+
Specify the number of characters that must be typed +by a user into a dynamic select menu before dispatching to the app.
+
+

Ancestors

+ +

Class variables

+
+
var data_source
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
prop attributes : Set[str]
+
+
+ +Expand source code + +
@property
+def attributes(self) -> Set[str]:  # type: ignore[override]
+    return super().attributes.union({"min_query_length"})
+
+

Build an unordered collection of unique elements.

+
+
+

Inherited members

+ +
+
+class ActionLinkButton +(*, text: str, url: str) +
+
+
+ +Expand source code + +
class ActionLinkButton(Action):
+    def __init__(self, *, text: str, url: str):
+        """A simple interactive button that just opens a URL
+
+        https://docs.slack.dev/messaging/formatting-message-text/#rich-layouts
+
+        Args:
+          text: text to display on the button, eg 'Click Me!"
+          url: the URL to open
+        """
+        super().__init__(text=text, url=url, subtype="button")
+
+ +

Ancestors

+ +

Inherited members

+ +
+
+class ActionUserSelector +(name: str,
text: str,
selected_user: Option | None = None)
+
+
+
+ +Expand source code + +
class ActionUserSelector(AbstractActionSelector):
+    data_source = "users"
+
+    def __init__(self, name: str, text: str, selected_user: Optional[Option] = None):
+        """Automatically populate the selector with a list of users in the workspace.
+
+        https://docs.slack.dev/legacy/legacy-messaging/legacy-adding-menus-to-messages/#menu_team_members
+
+        Args:
+            name: Name this specific action. The name will be returned to your
+                Action URL along with the message's callback_id when this action is
+                invoked. Use it to identify this particular response path.
+            text: The user-facing label for the message button or menu
+                representing this action. Cannot contain markup.
+            selected_user: An Option object to pre-select as the default
+                value.
+        """
+        super().__init__(name=name, text=text, selected_option=selected_user)
+
+

Action in attachments +https://docs.slack.dev/messaging/formatting-message-text/#rich-layouts +https://docs.slack.dev/legacy/legacy-messaging/legacy-interactive-message-field-guide/#message_action_fields

+

Automatically populate the selector with a list of users in the workspace.

+

https://docs.slack.dev/legacy/legacy-messaging/legacy-adding-menus-to-messages/#menu_team_members

+

Args

+
+
name
+
Name this specific action. The name will be returned to your +Action URL along with the message's callback_id when this action is +invoked. Use it to identify this particular response path.
+
text
+
The user-facing label for the message button or menu +representing this action. Cannot contain markup.
+
selected_user
+
An Option object to pre-select as the default +value.
+
+

Ancestors

+ +

Class variables

+
+
var data_source
+
+

The type of the None singleton.

+
+
+

Inherited members

+ +
+
+class Attachment +(*,
text: str,
fallback: str | None = None,
fields: Sequence[AttachmentField] | None = None,
color: str | None = None,
markdown_in: Sequence[str] | None = None,
title: str | None = None,
title_link: str | None = None,
pretext: str | None = None,
author_name: str | None = None,
author_subname: str | None = None,
author_link: str | None = None,
author_icon: str | None = None,
image_url: str | None = None,
thumb_url: str | None = None,
footer: str | None = None,
footer_icon: str | None = None,
ts: int | None = None)
+
+
+
+ +Expand source code + +
class Attachment(JsonObject):
+    attributes = {
+        "author_icon",
+        "author_link",
+        "author_name",
+        "author_subname",
+        "color",
+        "fallback",
+        "fields",
+        "footer",
+        "footer_icon",
+        "image_url",
+        "pretext",
+        "text",
+        "thumb_url",
+        "title",
+        "title_link",
+        "ts",
+    }
+
+    fields: Sequence[AttachmentField]
+
+    MarkdownFields = {"fields", "pretext", "text"}
+
+    footer_max_length = 300
+
+    def __init__(
+        self,
+        *,
+        text: str,
+        fallback: Optional[str] = None,
+        fields: Optional[Sequence[AttachmentField]] = None,
+        color: Optional[str] = None,
+        markdown_in: Optional[Sequence[str]] = None,
+        title: Optional[str] = None,
+        title_link: Optional[str] = None,
+        pretext: Optional[str] = None,
+        author_name: Optional[str] = None,
+        author_subname: Optional[str] = None,
+        author_link: Optional[str] = None,
+        author_icon: Optional[str] = None,
+        image_url: Optional[str] = None,
+        thumb_url: Optional[str] = None,
+        footer: Optional[str] = None,
+        footer_icon: Optional[str] = None,
+        ts: Optional[int] = None,
+    ):
+        """
+        A supplemental object that will display after the rest of the message.
+        Considered legacy - recommended replacement is to use message blocks instead.
+
+        https://docs.slack.dev/legacy/legacy-messaging/legacy-secondary-message-attachments#fields
+
+        Args:
+            text: The main body text of the attachment. It can be formatted as
+                plain text, or with markdown by including it in the markdown_in
+                parameter. The content will automatically collapse if it contains 700+
+                characters or 5+ linebreaks, and will display a "Show more..." link to
+                expand the content.
+            fallback: A plain text summary of the attachment used in clients that
+                don't show formatted text (eg. IRC, mobile notifications).
+            fields: An array of AttachmentField objects that get displayed in a
+                table-like way. For best results, include no more than 2-3 field
+                objects.
+            color: Changes the color of the border on the left side of this attachment
+                from the default gray. Can be any hex color code (eg. #439FE0)
+            markdown_in: An array of field names that should be formatted by
+                markdown syntax - allowed values: "pretext", "text", "fields"
+            title: Large title text near the top of the attachment.
+            title_link: A valid URL that turns the title text into a hyperlink.
+            pretext: Text that appears above the message attachment block. It can
+                be formatted as plain text, or with markdown by including it in the
+                markdown_in parameter.
+            author_name: Small text used to display the author's name.
+            author_subname: Small text used to display the author's sub name.
+            author_link: A valid URL that will hyperlink the author_name text.
+                Will only work if author_name is present.
+            author_icon: A valid URL that displays a small 16px by 16px image to
+                the left of the author_name text. Will only work if author_name is
+                present.
+            image_url: A valid URL to an image file that will be displayed at the
+                bottom of the attachment. We support GIF, JPEG, PNG, and BMP formats.
+                Large images will be resized to a maximum width of 360px or a maximum
+                height of 500px, while still maintaining the original aspect ratio.
+                Cannot be used with thumb_url.
+            thumb_url: A valid URL to an image file that will be displayed as a
+                thumbnail on the right side of a message attachment. We currently
+                support the following formats: GIF, JPEG, PNG, and BMP. The thumbnail's
+                longest dimension will be scaled down to 75px while maintaining the
+                aspect ratio of the image. The filesize of the image must also be less
+                than 500 KB. For best results, please use images that are already 75px
+                by 75px.
+            footer: Some brief text to help contextualize and identify an
+                attachment. Limited to 300 characters, and may be truncated further when
+                displayed to users in environments with limited screen real estate.
+            footer_icon: A valid URL to an image file that will be displayed
+                beside the footer text. Will only work if footer is present. We'll
+                render what you provide at 16px by 16px. It's best to use an image that
+                is similarly sized.
+            ts: An integer Unix timestamp that is used to related your attachment
+                to a specific time. The attachment will display the additional timestamp
+                value as part of the attachment's footer. Your message's timestamp will
+                be displayed in varying ways, depending on how far in the past or future
+                 it is, relative to the present. Form factors, like mobile versus
+                 desktop may also transform its rendered appearance.
+        """
+        self.text = text
+        self.title = title
+        self.fallback = fallback
+        self.pretext = pretext
+        self.title_link = title_link
+        self.color = color
+        self.author_name = author_name
+        self.author_subname = author_subname
+        self.author_link = author_link
+        self.author_icon = author_icon
+        self.image_url = image_url
+        self.thumb_url = thumb_url
+        self.footer = footer
+        self.footer_icon = footer_icon
+        self.ts = ts
+        self.fields = fields or []
+        self.markdown_in = markdown_in or []
+
+    @JsonValidator(f"footer attribute cannot exceed {footer_max_length} characters")
+    def footer_length(self) -> bool:
+        return self.footer is None or len(self.footer) <= self.footer_max_length
+
+    @JsonValidator("ts attribute cannot be present if footer attribute is absent")
+    def ts_without_footer(self) -> bool:
+        return self.ts is None or self.footer is not None
+
+    @EnumValidator("markdown_in", MarkdownFields)
+    def markdown_in_valid(self):
+        return not self.markdown_in or all(e in self.MarkdownFields for e in self.markdown_in)
+
+    @JsonValidator("color attribute must be 'good', 'warning', 'danger', or a hex color code")
+    def color_valid(self) -> bool:
+        return (
+            self.color is None
+            or self.color in SeededColors
+            or re.match("^#(?:[0-9A-F]{2}){3}$", self.color, re.IGNORECASE) is not None
+        )
+
+    @JsonValidator("image_url attribute cannot be present if thumb_url is populated")
+    def image_url_and_thumb_url_populated(self) -> bool:
+        return self.image_url is None or self.thumb_url is None
+
+    @JsonValidator("name must be present if link is present")
+    def author_link_without_author_name(self) -> bool:
+        return self.author_link is None or self.author_name is not None
+
+    @JsonValidator("icon must be present if link is present")
+    def author_link_without_author_icon(self) -> bool:
+        return self.author_link is None or self.author_icon is not None
+
+    def to_dict(self) -> dict:
+        json = super().to_dict()
+        if self.fields is not None:
+            json["fields"] = extract_json(self.fields)
+        if self.markdown_in:
+            json["mrkdwn_in"] = self.markdown_in
+        return json
+
+

The base class for JSON serializable class objects

+

A supplemental object that will display after the rest of the message. +Considered legacy - recommended replacement is to use message blocks instead.

+

https://docs.slack.dev/legacy/legacy-messaging/legacy-secondary-message-attachments#fields

+

Args

+
+
text
+
The main body text of the attachment. It can be formatted as +plain text, or with markdown by including it in the markdown_in +parameter. The content will automatically collapse if it contains 700+ +characters or 5+ linebreaks, and will display a "Show more…" link to +expand the content.
+
fallback
+
A plain text summary of the attachment used in clients that +don't show formatted text (eg. IRC, mobile notifications).
+
fields
+
An array of AttachmentField objects that get displayed in a +table-like way. For best results, include no more than 2-3 field +objects.
+
color
+
Changes the color of the border on the left side of this attachment +from the default gray. Can be any hex color code (eg. #439FE0)
+
markdown_in
+
An array of field names that should be formatted by +markdown syntax - allowed values: "pretext", "text", "fields"
+
title
+
Large title text near the top of the attachment.
+
title_link
+
A valid URL that turns the title text into a hyperlink.
+
pretext
+
Text that appears above the message attachment block. It can +be formatted as plain text, or with markdown by including it in the +markdown_in parameter.
+
author_name
+
Small text used to display the author's name.
+
author_subname
+
Small text used to display the author's sub name.
+
author_link
+
A valid URL that will hyperlink the author_name text. +Will only work if author_name is present.
+
author_icon
+
A valid URL that displays a small 16px by 16px image to +the left of the author_name text. Will only work if author_name is +present.
+
image_url
+
A valid URL to an image file that will be displayed at the +bottom of the attachment. We support GIF, JPEG, PNG, and BMP formats. +Large images will be resized to a maximum width of 360px or a maximum +height of 500px, while still maintaining the original aspect ratio. +Cannot be used with thumb_url.
+
thumb_url
+
A valid URL to an image file that will be displayed as a +thumbnail on the right side of a message attachment. We currently +support the following formats: GIF, JPEG, PNG, and BMP. The thumbnail's +longest dimension will be scaled down to 75px while maintaining the +aspect ratio of the image. The filesize of the image must also be less +than 500 KB. For best results, please use images that are already 75px +by 75px.
+
footer
+
Some brief text to help contextualize and identify an +attachment. Limited to 300 characters, and may be truncated further when +displayed to users in environments with limited screen real estate.
+
footer_icon
+
A valid URL to an image file that will be displayed +beside the footer text. Will only work if footer is present. We'll +render what you provide at 16px by 16px. It's best to use an image that +is similarly sized.
+
ts
+
An integer Unix timestamp that is used to related your attachment +to a specific time. The attachment will display the additional timestamp +value as part of the attachment's footer. Your message's timestamp will +be displayed in varying ways, depending on how far in the past or future +it is, relative to the present. Form factors, like mobile versus +desktop may also transform its rendered appearance.
+
+

Ancestors

+ +

Subclasses

+ +

Class variables

+
+
var MarkdownFields
+
+

The type of the None singleton.

+
+
var attributes
+
+

The type of the None singleton.

+
+
var fields : Sequence[AttachmentField]
+
+

The type of the None singleton.

+
+
var footer_max_length
+
+

The type of the None singleton.

+
+
+

Methods

+
+ +
+
+ +Expand source code + +
@JsonValidator("icon must be present if link is present")
+def author_link_without_author_icon(self) -> bool:
+    return self.author_link is None or self.author_icon is not None
+
+
+
+ +
+
+ +Expand source code + +
@JsonValidator("name must be present if link is present")
+def author_link_without_author_name(self) -> bool:
+    return self.author_link is None or self.author_name is not None
+
+
+
+
+def color_valid(self) ‑> bool +
+
+
+ +Expand source code + +
@JsonValidator("color attribute must be 'good', 'warning', 'danger', or a hex color code")
+def color_valid(self) -> bool:
+    return (
+        self.color is None
+        or self.color in SeededColors
+        or re.match("^#(?:[0-9A-F]{2}){3}$", self.color, re.IGNORECASE) is not None
+    )
+
+
+
+
+def footer_length(self) ‑> bool +
+
+
+ +Expand source code + +
@JsonValidator(f"footer attribute cannot exceed {footer_max_length} characters")
+def footer_length(self) -> bool:
+    return self.footer is None or len(self.footer) <= self.footer_max_length
+
+
+
+
+def image_url_and_thumb_url_populated(self) ‑> bool +
+
+
+ +Expand source code + +
@JsonValidator("image_url attribute cannot be present if thumb_url is populated")
+def image_url_and_thumb_url_populated(self) -> bool:
+    return self.image_url is None or self.thumb_url is None
+
+
+
+
+def markdown_in_valid(self) +
+
+
+ +Expand source code + +
@EnumValidator("markdown_in", MarkdownFields)
+def markdown_in_valid(self):
+    return not self.markdown_in or all(e in self.MarkdownFields for e in self.markdown_in)
+
+
+
+ +
+
+ +Expand source code + +
@JsonValidator("ts attribute cannot be present if footer attribute is absent")
+def ts_without_footer(self) -> bool:
+    return self.ts is None or self.footer is not None
+
+
+
+
+

Inherited members

+ +
+
+class AttachmentField +(*, title: str | None = None, value: str | None = None, short: bool = True) +
+
+
+ +Expand source code + +
class AttachmentField(JsonObject):
+    attributes = {"short", "title", "value"}
+
+    def __init__(
+        self,
+        *,
+        title: Optional[str] = None,
+        value: Optional[str] = None,
+        short: bool = True,
+    ):
+        self.title = title
+        self.value = value
+        self.short = short
+
+

The base class for JSON serializable class objects

+

Ancestors

+ +

Class variables

+
+
var attributes
+
+

The type of the None singleton.

+
+
+

Inherited members

+ +
+
+class BlockAttachment +(*,
blocks: Sequence[Block],
color: str | None = None,
fallback: str | None = None)
+
+
+
+ +Expand source code + +
class BlockAttachment(Attachment):
+    blocks: List[Block]
+
+    @property
+    def attributes(self):
+        return super().attributes.union({"blocks", "color"})
+
+    def __init__(
+        self,
+        *,
+        blocks: Sequence[Block],
+        color: Optional[str] = None,
+        fallback: Optional[str] = None,
+    ):
+        """
+        A bridge between legacy attachments and Block Kit formatting - pass a list of
+        Block objects directly to this attachment.
+
+        https://docs.slack.dev/legacy/legacy-messaging/legacy-secondary-message-attachments#fields
+
+        Args:
+            blocks: a sequence of Block objects
+            color: Changes the color of the border on the left side of this
+                attachment from the default gray. Can either be one of "good" (green),
+                "warning" (yellow), "danger" (red), or any hex color code (eg. #439FE0)
+            fallback: fallback text
+        """
+        super().__init__(text="", fallback=fallback, color=color)
+        self.blocks = list(blocks)
+
+    @JsonValidator("fields attribute cannot be populated on BlockAttachment")
+    def fields_attribute_absent(self) -> bool:
+        return not self.fields
+
+    def to_dict(self) -> dict:
+        json = super().to_dict()
+        json.update({"blocks": extract_json(self.blocks)})
+        del json["fields"]  # cannot supply fields and blocks at the same time
+        return json
+
+

The base class for JSON serializable class objects

+

A bridge between legacy attachments and Block Kit formatting - pass a list of +Block objects directly to this attachment.

+

https://docs.slack.dev/legacy/legacy-messaging/legacy-secondary-message-attachments#fields

+

Args

+
+
blocks
+
a sequence of Block objects
+
color
+
Changes the color of the border on the left side of this +attachment from the default gray. Can either be one of "good" (green), +"warning" (yellow), "danger" (red), or any hex color code (eg. #439FE0)
+
fallback
+
fallback text
+
+

Ancestors

+ +

Class variables

+
+
var blocks : List[Block]
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
prop attributes
+
+
+ +Expand source code + +
@property
+def attributes(self):
+    return super().attributes.union({"blocks", "color"})
+
+

Build an unordered collection of unique elements.

+
+
+

Methods

+
+
+def fields_attribute_absent(self) ‑> bool +
+
+
+ +Expand source code + +
@JsonValidator("fields attribute cannot be populated on BlockAttachment")
+def fields_attribute_absent(self) -> bool:
+    return not self.fields
+
+
+
+
+

Inherited members

+ +
+
+class InteractiveAttachment +(*,
actions: Sequence[Action],
callback_id: str,
text: str,
fallback: str | None = None,
fields: Sequence[AttachmentField] | None = None,
color: str | None = None,
markdown_in: Sequence[str] | None = None,
title: str | None = None,
title_link: str | None = None,
pretext: str | None = None,
author_name: str | None = None,
author_subname: str | None = None,
author_link: str | None = None,
author_icon: str | None = None,
image_url: str | None = None,
thumb_url: str | None = None,
footer: str | None = None,
footer_icon: str | None = None,
ts: int | None = None)
+
+
+
+ +Expand source code + +
class InteractiveAttachment(Attachment):
+    @property
+    def attributes(self) -> Set[str]:  # type: ignore[override]
+        return super().attributes.union({"callback_id"})
+
+    actions_max_length = 5
+
+    def __init__(
+        self,
+        *,
+        actions: Sequence[Action],
+        callback_id: str,
+        text: str,
+        fallback: Optional[str] = None,
+        fields: Optional[Sequence[AttachmentField]] = None,
+        color: Optional[str] = None,
+        markdown_in: Optional[Sequence[str]] = None,
+        title: Optional[str] = None,
+        title_link: Optional[str] = None,
+        pretext: Optional[str] = None,
+        author_name: Optional[str] = None,
+        author_subname: Optional[str] = None,
+        author_link: Optional[str] = None,
+        author_icon: Optional[str] = None,
+        image_url: Optional[str] = None,
+        thumb_url: Optional[str] = None,
+        footer: Optional[str] = None,
+        footer_icon: Optional[str] = None,
+        ts: Optional[int] = None,
+    ):
+        """
+        An Attachment, but designed to contain interactive Actions
+        Considered legacy - recommended replacement is to use message blocks instead.
+
+        https://docs.slack.dev/legacy/legacy-messaging/legacy-interactive-message-field-guide/#attachment_fields
+        https://docs.slack.dev/legacy/legacy-messaging/legacy-secondary-message-attachments#fields
+
+        Args:
+            actions: A collection of Action objects to include in the attachment.
+                Cannot exceed 5 elements.
+            callback_id: The ID used to identify this attachment. Will be part of the
+                payload sent back to your application.
+            text: The main body text of the attachment. It can be formatted as
+                plain text, or with markdown by including it in the markdown_in
+                parameter. The content will automatically collapse if it contains 700+
+                characters or 5+ linebreaks, and will display a "Show more..." link to
+                expand the content.
+            fallback: A plain text summary of the attachment used in clients that
+                don't show formatted text (eg. IRC, mobile notifications).
+            fields: An array of AttachmentField objects that get displayed in a
+                table-like way. For best results, include no more than 2-3 field
+                objects.
+            color: Changes the color of the border on the left side of this attachment
+                from the default gray. Can either be one of "good" (green), "warning"
+                (yellow), "danger" (red), or any hex color code (eg. #439FE0)
+            markdown_in: An array of field names that should be formatted by
+                markdown syntax - allowed values: "pretext", "text", "fields"
+            title: Large title text near the top of the attachment.
+            title_link: A valid URL that turns the title text into a hyperlink.
+            pretext: Text that appears above the message attachment block. It can
+                be formatted as plain text, or with markdown by including it in the
+                markdown_in parameter.
+            author_name: Small text used to display the author's name.
+            author_subname: Small text used to display the author's sub name.
+            author_link: A valid URL that will hyperlink the author_name text.
+                Will only work if author_name is present.
+            author_icon: A valid URL that displays a small 16px by 16px image to
+                the left of the author_name text. Will only work if author_name is
+                present.
+            image_url: A valid URL to an image file that will be displayed at the
+                bottom of the attachment. We support GIF, JPEG, PNG, and BMP formats.
+                Large images will be resized to a maximum width of 360px or a maximum
+                height of 500px, while still maintaining the original aspect ratio.
+                Cannot be used with thumb_url.
+            thumb_url: A valid URL to an image file that will be displayed as a
+                thumbnail on the right side of a message attachment. We currently
+                support the following formats: GIF, JPEG, PNG, and BMP. The thumbnail's
+                longest dimension will be scaled down to 75px while maintaining the
+                aspect ratio of the image. The filesize of the image must also be less
+                than 500 KB. For best results, please use images that are already 75px
+                by 75px.
+            footer: Some brief text to help contextualize and identify an
+                attachment. Limited to 300 characters, and may be truncated further when
+                displayed to users in environments with limited screen real estate.
+            footer_icon: A valid URL to an image file that will be displayed
+                beside the footer text. Will only work if footer is present. We'll
+                render what you provide at 16px by 16px. It's best to use an image that
+                is similarly sized.
+            ts: An integer Unix timestamp that is used to related your attachment
+                to a specific time. The attachment will display the additional timestamp
+                value as part of the attachment's footer. Your message's timestamp will
+                be displayed in varying ways, depending on how far in the past or future
+                 it is, relative to the present. Form factors, like mobile versus
+                 desktop may also transform its rendered appearance.
+        """
+        super().__init__(
+            text=text,
+            title=title,
+            fallback=fallback,
+            fields=fields,
+            pretext=pretext,
+            title_link=title_link,
+            color=color,
+            author_name=author_name,
+            author_subname=author_subname,
+            author_link=author_link,
+            author_icon=author_icon,
+            image_url=image_url,
+            thumb_url=thumb_url,
+            footer=footer,
+            footer_icon=footer_icon,
+            ts=ts,
+            markdown_in=markdown_in,
+        )
+        self.callback_id = callback_id
+        self.actions = actions or []
+
+    @JsonValidator(f"actions attribute cannot exceed {actions_max_length} elements")
+    def actions_length(self) -> bool:
+        return len(self.actions) <= self.actions_max_length
+
+    def to_dict(self) -> dict:
+        json = super().to_dict()
+        json["actions"] = extract_json(self.actions)
+        return json
+
+

The base class for JSON serializable class objects

+

An Attachment, but designed to contain interactive Actions +Considered legacy - recommended replacement is to use message blocks instead.

+

https://docs.slack.dev/legacy/legacy-messaging/legacy-interactive-message-field-guide/#attachment_fields +https://docs.slack.dev/legacy/legacy-messaging/legacy-secondary-message-attachments#fields

+

Args

+
+
actions
+
A collection of Action objects to include in the attachment. +Cannot exceed 5 elements.
+
callback_id
+
The ID used to identify this attachment. Will be part of the +payload sent back to your application.
+
text
+
The main body text of the attachment. It can be formatted as +plain text, or with markdown by including it in the markdown_in +parameter. The content will automatically collapse if it contains 700+ +characters or 5+ linebreaks, and will display a "Show more…" link to +expand the content.
+
fallback
+
A plain text summary of the attachment used in clients that +don't show formatted text (eg. IRC, mobile notifications).
+
fields
+
An array of AttachmentField objects that get displayed in a +table-like way. For best results, include no more than 2-3 field +objects.
+
color
+
Changes the color of the border on the left side of this attachment +from the default gray. Can either be one of "good" (green), "warning" +(yellow), "danger" (red), or any hex color code (eg. #439FE0)
+
markdown_in
+
An array of field names that should be formatted by +markdown syntax - allowed values: "pretext", "text", "fields"
+
title
+
Large title text near the top of the attachment.
+
title_link
+
A valid URL that turns the title text into a hyperlink.
+
pretext
+
Text that appears above the message attachment block. It can +be formatted as plain text, or with markdown by including it in the +markdown_in parameter.
+
author_name
+
Small text used to display the author's name.
+
author_subname
+
Small text used to display the author's sub name.
+
author_link
+
A valid URL that will hyperlink the author_name text. +Will only work if author_name is present.
+
author_icon
+
A valid URL that displays a small 16px by 16px image to +the left of the author_name text. Will only work if author_name is +present.
+
image_url
+
A valid URL to an image file that will be displayed at the +bottom of the attachment. We support GIF, JPEG, PNG, and BMP formats. +Large images will be resized to a maximum width of 360px or a maximum +height of 500px, while still maintaining the original aspect ratio. +Cannot be used with thumb_url.
+
thumb_url
+
A valid URL to an image file that will be displayed as a +thumbnail on the right side of a message attachment. We currently +support the following formats: GIF, JPEG, PNG, and BMP. The thumbnail's +longest dimension will be scaled down to 75px while maintaining the +aspect ratio of the image. The filesize of the image must also be less +than 500 KB. For best results, please use images that are already 75px +by 75px.
+
footer
+
Some brief text to help contextualize and identify an +attachment. Limited to 300 characters, and may be truncated further when +displayed to users in environments with limited screen real estate.
+
footer_icon
+
A valid URL to an image file that will be displayed +beside the footer text. Will only work if footer is present. We'll +render what you provide at 16px by 16px. It's best to use an image that +is similarly sized.
+
ts
+
An integer Unix timestamp that is used to related your attachment +to a specific time. The attachment will display the additional timestamp +value as part of the attachment's footer. Your message's timestamp will +be displayed in varying ways, depending on how far in the past or future +it is, relative to the present. Form factors, like mobile versus +desktop may also transform its rendered appearance.
+
+

Ancestors

+ +

Class variables

+
+
var actions_max_length
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
prop attributes : Set[str]
+
+
+ +Expand source code + +
@property
+def attributes(self) -> Set[str]:  # type: ignore[override]
+    return super().attributes.union({"callback_id"})
+
+

Build an unordered collection of unique elements.

+
+
+

Methods

+
+
+def actions_length(self) ‑> bool +
+
+
+ +Expand source code + +
@JsonValidator(f"actions attribute cannot exceed {actions_max_length} elements")
+def actions_length(self) -> bool:
+    return len(self.actions) <= self.actions_max_length
+
+
+
+
+

Inherited members

+ +
+
+
+
+ +
+ + + diff --git a/docs/reference/models/basic_objects.html b/docs/reference/models/basic_objects.html new file mode 100644 index 000000000..193200f84 --- /dev/null +++ b/docs/reference/models/basic_objects.html @@ -0,0 +1,474 @@ + + + + + + +slack_sdk.models.basic_objects API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.models.basic_objects

+
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class BaseObject +
+
+
+ +Expand source code + +
class BaseObject:
+    """The base class for all model objects in this module"""
+
+    def __str__(self):
+        return f"<slack_sdk.{self.__class__.__name__}>"
+
+

The base class for all model objects in this module

+

Subclasses

+ +
+
+class EnumValidator +(attribute: str, enum: Iterable[str]) +
+
+
+ +Expand source code + +
class EnumValidator(JsonValidator):
+    def __init__(self, attribute: str, enum: Iterable[str]):
+        super().__init__(f"{attribute} attribute must be one of the following values: " f"{', '.join(enum)}")
+
+

Decorate a method on a class to mark it as a JSON validator. Validation +functions should return true if valid, false if not.

+

Args

+
+
message
+
Message to be attached to the thrown SlackObjectFormationError
+
+

Ancestors

+ +
+
+class JsonObject +
+
+
+ +Expand source code + +
class JsonObject(BaseObject, metaclass=ABCMeta):
+    """The base class for JSON serializable class objects"""
+
+    @property
+    @abstractmethod
+    def attributes(self) -> Set[str]:
+        """Provide a set of attributes of this object that will make up its JSON structure"""
+        return set()
+
+    def validate_json(self) -> None:
+        """
+        Raises:
+          SlackObjectFormationError if the object was not valid
+        """
+        for attribute in (func for func in dir(self) if not func.startswith("__")):
+            method = getattr(self, attribute, None)
+            if callable(method) and hasattr(method, "validator"):
+                method()
+
+    def get_object_attribute(self, key: str):
+        return getattr(self, key, None)
+
+    def get_non_null_attributes(self) -> dict:
+        """
+        Construct a dictionary out of non-null keys (from attributes property)
+        present on this object
+        """
+
+        def to_dict_compatible(value: Union[dict, list, object, tuple]) -> Union[dict, list, Any]:
+            if isinstance(value, (list, tuple)):
+                return [to_dict_compatible(v) for v in value]
+            else:
+                to_dict = getattr(value, "to_dict", None)
+                if to_dict and callable(to_dict):
+                    return {k: to_dict_compatible(v) for k, v in value.to_dict().items()}  # type: ignore[attr-defined]
+                else:
+                    return value
+
+        def is_not_empty(self, key: str) -> bool:
+            value = self.get_object_attribute(key)
+            if value is None:
+                return False
+
+            # Usually, Block Kit components do not allow an empty array for a property value, but there are some exceptions.
+            # The following code deals with these exceptions:
+            type_value = getattr(self, "type", None)
+            for empty_allowed in EMPTY_ALLOWED_TYPE_AND_PROPERTY_LIST:
+                if type_value == empty_allowed["type"] and key == empty_allowed["property"]:
+                    return True
+
+            has_len = getattr(value, "__len__", None) is not None
+            if has_len:
+                return len(value) > 0
+            else:
+                return value is not None
+
+        return {
+            key: to_dict_compatible(value=self.get_object_attribute(key))
+            for key in sorted(self.attributes)
+            if is_not_empty(self, key)
+        }
+
+    def to_dict(self, *args) -> dict:
+        """
+        Extract this object as a JSON-compatible, Slack-API-valid dictionary
+
+        Args:
+          *args: Any specific formatting args (rare; generally not required)
+
+        Raises:
+          SlackObjectFormationError if the object was not valid
+        """
+        self.validate_json()
+        return self.get_non_null_attributes()
+
+    def __repr__(self):
+        dict_value = self.get_non_null_attributes()
+        if dict_value:
+            return f"<slack_sdk.{self.__class__.__name__}: {dict_value}>"
+        else:
+            return self.__str__()
+
+    def __eq__(self, other: Any) -> bool:
+        if not isinstance(other, JsonObject):
+            return False
+        return self.to_dict() == other.to_dict()
+
+

The base class for JSON serializable class objects

+

Ancestors

+ +

Subclasses

+ +

Instance variables

+
+
prop attributes : Set[str]
+
+
+ +Expand source code + +
@property
+@abstractmethod
+def attributes(self) -> Set[str]:
+    """Provide a set of attributes of this object that will make up its JSON structure"""
+    return set()
+
+

Provide a set of attributes of this object that will make up its JSON structure

+
+
+

Methods

+
+
+def get_non_null_attributes(self) ‑> dict +
+
+
+ +Expand source code + +
def get_non_null_attributes(self) -> dict:
+    """
+    Construct a dictionary out of non-null keys (from attributes property)
+    present on this object
+    """
+
+    def to_dict_compatible(value: Union[dict, list, object, tuple]) -> Union[dict, list, Any]:
+        if isinstance(value, (list, tuple)):
+            return [to_dict_compatible(v) for v in value]
+        else:
+            to_dict = getattr(value, "to_dict", None)
+            if to_dict and callable(to_dict):
+                return {k: to_dict_compatible(v) for k, v in value.to_dict().items()}  # type: ignore[attr-defined]
+            else:
+                return value
+
+    def is_not_empty(self, key: str) -> bool:
+        value = self.get_object_attribute(key)
+        if value is None:
+            return False
+
+        # Usually, Block Kit components do not allow an empty array for a property value, but there are some exceptions.
+        # The following code deals with these exceptions:
+        type_value = getattr(self, "type", None)
+        for empty_allowed in EMPTY_ALLOWED_TYPE_AND_PROPERTY_LIST:
+            if type_value == empty_allowed["type"] and key == empty_allowed["property"]:
+                return True
+
+        has_len = getattr(value, "__len__", None) is not None
+        if has_len:
+            return len(value) > 0
+        else:
+            return value is not None
+
+    return {
+        key: to_dict_compatible(value=self.get_object_attribute(key))
+        for key in sorted(self.attributes)
+        if is_not_empty(self, key)
+    }
+
+

Construct a dictionary out of non-null keys (from attributes property) +present on this object

+
+
+def get_object_attribute(self, key: str) +
+
+
+ +Expand source code + +
def get_object_attribute(self, key: str):
+    return getattr(self, key, None)
+
+
+
+
+def to_dict(self, *args) ‑> dict +
+
+
+ +Expand source code + +
def to_dict(self, *args) -> dict:
+    """
+    Extract this object as a JSON-compatible, Slack-API-valid dictionary
+
+    Args:
+      *args: Any specific formatting args (rare; generally not required)
+
+    Raises:
+      SlackObjectFormationError if the object was not valid
+    """
+    self.validate_json()
+    return self.get_non_null_attributes()
+
+

Extract this object as a JSON-compatible, Slack-API-valid dictionary

+

Args

+
+
*args
+
Any specific formatting args (rare; generally not required)
+
+

Raises

+

SlackObjectFormationError if the object was not valid

+
+
+def validate_json(self) ‑> None +
+
+
+ +Expand source code + +
def validate_json(self) -> None:
+    """
+    Raises:
+      SlackObjectFormationError if the object was not valid
+    """
+    for attribute in (func for func in dir(self) if not func.startswith("__")):
+        method = getattr(self, attribute, None)
+        if callable(method) and hasattr(method, "validator"):
+            method()
+
+

Raises

+

SlackObjectFormationError if the object was not valid

+
+
+
+
+class JsonValidator +(message: str) +
+
+
+ +Expand source code + +
class JsonValidator:
+    def __init__(self, message: str):
+        """
+        Decorate a method on a class to mark it as a JSON validator. Validation
+            functions should return true if valid, false if not.
+
+        Args:
+            message: Message to be attached to the thrown SlackObjectFormationError
+        """
+        self.message = message
+
+    def __call__(self, func: Callable) -> Callable[..., None]:
+        @wraps(func)
+        def wrapped_f(*args, **kwargs):
+            if not func(*args, **kwargs):
+                raise SlackObjectFormationError(self.message)
+
+        wrapped_f.validator = True  # type: ignore[attr-defined]
+        return wrapped_f
+
+

Decorate a method on a class to mark it as a JSON validator. Validation +functions should return true if valid, false if not.

+

Args

+
+
message
+
Message to be attached to the thrown SlackObjectFormationError
+
+

Subclasses

+ +
+
+
+
+ +
+ + + diff --git a/docs/reference/models/blocks/basic_components.html b/docs/reference/models/blocks/basic_components.html new file mode 100644 index 000000000..2821b6a4b --- /dev/null +++ b/docs/reference/models/blocks/basic_components.html @@ -0,0 +1,1805 @@ + + + + + + +slack_sdk.models.blocks.basic_components API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.models.blocks.basic_components

+
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class ConfirmObject +(*,
title: str | Dict[str, Any] | PlainTextObject,
text: str | Dict[str, Any] | TextObject,
confirm: str | Dict[str, Any] | PlainTextObject = 'Yes',
deny: str | Dict[str, Any] | PlainTextObject = 'No',
style: str | None = None)
+
+
+
+ +Expand source code + +
class ConfirmObject(JsonObject):
+    attributes: Set[str] = set()
+
+    title_max_length = 100
+    text_max_length = 300
+    confirm_max_length = 30
+    deny_max_length = 30
+
+    @classmethod
+    def parse(cls, confirm: Union["ConfirmObject", Dict[str, Any]]):
+        if confirm:
+            if isinstance(confirm, ConfirmObject):
+                return confirm
+            elif isinstance(confirm, dict):
+                return ConfirmObject(**confirm)
+            else:
+                # Not yet implemented: show some warning here
+                return None
+        return None
+
+    def __init__(
+        self,
+        *,
+        title: Union[str, Dict[str, Any], PlainTextObject],
+        text: Union[str, Dict[str, Any], TextObject],
+        confirm: Union[str, Dict[str, Any], PlainTextObject] = "Yes",
+        deny: Union[str, Dict[str, Any], PlainTextObject] = "No",
+        style: Optional[str] = None,
+    ):
+        """
+        An object that defines a dialog that provides a confirmation step to any
+        interactive element. This dialog will ask the user to confirm their action by
+        offering a confirm and deny button.
+        https://docs.slack.dev/reference/block-kit/composition-objects/confirmation-dialog-object/
+        """
+        self._title = TextObject.parse(title, default_type=PlainTextObject.type)
+        self._text = TextObject.parse(text, default_type=MarkdownTextObject.type)
+        self._confirm = TextObject.parse(confirm, default_type=PlainTextObject.type)
+        self._deny = TextObject.parse(deny, default_type=PlainTextObject.type)
+        self._style = style
+
+        # for backward-compatibility with version 2.0-2.5, the following fields return str values
+        self.title = self._title.text if self._title else None
+        self.text = self._text.text if self._text else None
+        self.confirm = self._confirm.text if self._confirm else None
+        self.deny = self._deny.text if self._deny else None
+        self.style = self._style
+
+    @JsonValidator(f"title attribute cannot exceed {title_max_length} characters")
+    def title_length(self) -> bool:
+        return self._title is None or len(self._title.text) <= self.title_max_length
+
+    @JsonValidator(f"text attribute cannot exceed {text_max_length} characters")
+    def text_length(self) -> bool:
+        return self._text is None or len(self._text.text) <= self.text_max_length
+
+    @JsonValidator(f"confirm attribute cannot exceed {confirm_max_length} characters")
+    def confirm_length(self) -> bool:
+        return self._confirm is None or len(self._confirm.text) <= self.confirm_max_length
+
+    @JsonValidator(f"deny attribute cannot exceed {deny_max_length} characters")
+    def deny_length(self) -> bool:
+        return self._deny is None or len(self._deny.text) <= self.deny_max_length
+
+    @JsonValidator('style for confirm must be either "primary" or "danger"')
+    def _validate_confirm_style(self) -> bool:
+        return self._style is None or self._style in ["primary", "danger"]
+
+    def to_dict(self, option_type: str = "block") -> Dict[str, Any]:
+        if option_type == "action":
+            # deliberately skipping JSON validators here - can't find documentation
+            # on actual limits here
+            json: Dict[str, Union[str, dict]] = {
+                "ok_text": self._confirm.text if self._confirm and self._confirm.text != "Yes" else "Okay",
+                "dismiss_text": self._deny.text if self._deny and self._deny.text != "No" else "Cancel",
+            }
+            if self._title:
+                json["title"] = self._title.text
+            if self._text:
+                json["text"] = self._text.text
+            return json
+
+        else:
+            self.validate_json()
+            json = {}
+            if self._title:
+                json["title"] = self._title.to_dict()
+            if self._text:
+                json["text"] = self._text.to_dict()
+            if self._confirm:
+                json["confirm"] = self._confirm.to_dict()
+            if self._deny:
+                json["deny"] = self._deny.to_dict()
+            if self._style:
+                json["style"] = self._style
+            return json
+
+

The base class for JSON serializable class objects

+

An object that defines a dialog that provides a confirmation step to any +interactive element. This dialog will ask the user to confirm their action by +offering a confirm and deny button. +https://docs.slack.dev/reference/block-kit/composition-objects/confirmation-dialog-object/

+

Ancestors

+ +

Class variables

+
+
var attributes : Set[str]
+
+

The type of the None singleton.

+
+
var confirm_max_length
+
+

The type of the None singleton.

+
+
var deny_max_length
+
+

The type of the None singleton.

+
+
var text_max_length
+
+

The type of the None singleton.

+
+
var title_max_length
+
+

The type of the None singleton.

+
+
+

Static methods

+
+
+def parse(confirm: ForwardRef('ConfirmObject') | Dict[str, Any]) +
+
+
+
+
+

Methods

+
+
+def confirm_length(self) ‑> bool +
+
+
+ +Expand source code + +
@JsonValidator(f"confirm attribute cannot exceed {confirm_max_length} characters")
+def confirm_length(self) -> bool:
+    return self._confirm is None or len(self._confirm.text) <= self.confirm_max_length
+
+
+
+
+def deny_length(self) ‑> bool +
+
+
+ +Expand source code + +
@JsonValidator(f"deny attribute cannot exceed {deny_max_length} characters")
+def deny_length(self) -> bool:
+    return self._deny is None or len(self._deny.text) <= self.deny_max_length
+
+
+
+
+def text_length(self) ‑> bool +
+
+
+ +Expand source code + +
@JsonValidator(f"text attribute cannot exceed {text_max_length} characters")
+def text_length(self) -> bool:
+    return self._text is None or len(self._text.text) <= self.text_max_length
+
+
+
+
+def title_length(self) ‑> bool +
+
+
+ +Expand source code + +
@JsonValidator(f"title attribute cannot exceed {title_max_length} characters")
+def title_length(self) -> bool:
+    return self._title is None or len(self._title.text) <= self.title_max_length
+
+
+
+
+

Inherited members

+ +
+
+class DispatchActionConfig +(*, trigger_actions_on: List[Any] | None = None) +
+
+
+ +Expand source code + +
class DispatchActionConfig(JsonObject):
+    attributes = {"trigger_actions_on"}
+
+    @classmethod
+    def parse(cls, config: Union["DispatchActionConfig", Dict[str, Any]]):
+        if config:
+            if isinstance(config, DispatchActionConfig):
+                return config
+            elif isinstance(config, dict):
+                return DispatchActionConfig(**config)
+            else:
+                # Not yet implemented: show some warning here
+                return None
+        return None
+
+    def __init__(
+        self,
+        *,
+        trigger_actions_on: Optional[List[Any]] = None,
+    ):
+        """
+        Determines when a plain-text input element will return a block_actions interaction payload.
+        https://docs.slack.dev/reference/block-kit/composition-objects/dispatch-action-configuration-object
+        """
+        self._trigger_actions_on = trigger_actions_on or []
+
+    def to_dict(self) -> Dict[str, Any]:
+        self.validate_json()
+        json = {}
+        if self._trigger_actions_on:
+            json["trigger_actions_on"] = self._trigger_actions_on
+        return json
+
+

The base class for JSON serializable class objects

+

Determines when a plain-text input element will return a block_actions interaction payload. +https://docs.slack.dev/reference/block-kit/composition-objects/dispatch-action-configuration-object

+

Ancestors

+ +

Class variables

+
+
var attributes
+
+

The type of the None singleton.

+
+
+

Static methods

+
+
+def parse(config: ForwardRef('DispatchActionConfig') | Dict[str, Any]) +
+
+
+
+
+

Inherited members

+ +
+
+class FeedbackButtonObject +(*,
text: str | Dict[str, Any] | PlainTextObject,
accessibility_label: str | None = None,
value: str,
**others: Dict[str, Any])
+
+
+
+ +Expand source code + +
class FeedbackButtonObject(JsonObject):
+    attributes: Set[str] = set()
+
+    text_max_length = 75
+    value_max_length = 2000
+
+    @classmethod
+    def parse(cls, feedback_button: Union["FeedbackButtonObject", Dict[str, Any]]):
+        if feedback_button:
+            if isinstance(feedback_button, FeedbackButtonObject):
+                return feedback_button
+            elif isinstance(feedback_button, dict):
+                return FeedbackButtonObject(**feedback_button)
+            else:
+                # Not yet implemented: show some warning here
+                return None
+        return None
+
+    def __init__(
+        self,
+        *,
+        text: Union[str, Dict[str, Any], PlainTextObject],
+        accessibility_label: Optional[str] = None,
+        value: str,
+        **others: Dict[str, Any],
+    ):
+        """
+        A feedback button element object for either positive or negative feedback.
+        https://docs.slack.dev/reference/block-kit/block-elements/feedback-buttons-element#button-object-fields
+
+        Args:
+            text (required): An object containing some text. Maximum length for this field is 75 characters.
+            accessibility_label: A label for longer descriptive text about a button element. This label will be read out by
+                screen readers instead of the button `text` object.
+            value (required): The button value. Maximum length for this field is 2000 characters.
+        """
+        self._text: Optional[TextObject] = PlainTextObject.parse(text, default_type=PlainTextObject.type)
+        self._accessibility_label: Optional[str] = accessibility_label
+        self._value: Optional[str] = value
+        show_unknown_key_warning(self, others)
+
+    @JsonValidator(f"text attribute cannot exceed {text_max_length} characters")
+    def text_length(self) -> bool:
+        return self._text is None or len(self._text.text) <= self.text_max_length
+
+    @JsonValidator(f"value attribute cannot exceed {value_max_length} characters")
+    def value_length(self) -> bool:
+        return self._value is None or len(self._value) <= self.value_max_length
+
+    def to_dict(self) -> Dict[str, Any]:
+        self.validate_json()
+        json: Dict[str, Union[str, dict]] = {}
+        if self._text:
+            json["text"] = self._text.to_dict()
+        if self._accessibility_label:
+            json["accessibility_label"] = self._accessibility_label
+        if self._value:
+            json["value"] = self._value
+        return json
+
+

The base class for JSON serializable class objects

+

A feedback button element object for either positive or negative feedback. +https://docs.slack.dev/reference/block-kit/block-elements/feedback-buttons-element#button-object-fields

+

Args

+
+
text : required
+
An object containing some text. Maximum length for this field is 75 characters.
+
accessibility_label
+
A label for longer descriptive text about a button element. This label will be read out by +screen readers instead of the button text object.
+
value : required
+
The button value. Maximum length for this field is 2000 characters.
+
+

Ancestors

+ +

Class variables

+
+
var attributes : Set[str]
+
+

The type of the None singleton.

+
+
var text_max_length
+
+

The type of the None singleton.

+
+
var value_max_length
+
+

The type of the None singleton.

+
+
+

Static methods

+
+
+def parse(feedback_button: ForwardRef('FeedbackButtonObject') | Dict[str, Any]) +
+
+
+
+
+

Methods

+
+
+def text_length(self) ‑> bool +
+
+
+ +Expand source code + +
@JsonValidator(f"text attribute cannot exceed {text_max_length} characters")
+def text_length(self) -> bool:
+    return self._text is None or len(self._text.text) <= self.text_max_length
+
+
+
+
+def value_length(self) ‑> bool +
+
+
+ +Expand source code + +
@JsonValidator(f"value attribute cannot exceed {value_max_length} characters")
+def value_length(self) -> bool:
+    return self._value is None or len(self._value) <= self.value_max_length
+
+
+
+
+

Inherited members

+ +
+
+class MarkdownTextObject +(*, text: str, verbatim: bool | None = None) +
+
+
+ +Expand source code + +
class MarkdownTextObject(TextObject):
+    """mrkdwn typed text object"""
+
+    type = "mrkdwn"
+
+    @property
+    def attributes(self) -> Set[str]:  # type: ignore[override]
+        return super().attributes.union({"verbatim"})
+
+    def __init__(self, *, text: str, verbatim: Optional[bool] = None):
+        """A Markdown text object, meaning markdown characters will be parsed as
+        formatting information.
+        https://docs.slack.dev/reference/block-kit/composition-objects/text-object
+
+        Args:
+            text (required): The text for the block. This field accepts any of the standard text formatting markup
+                when type is mrkdwn.
+            verbatim: When set to false (as is default) URLs will be auto-converted into links,
+                conversation names will be link-ified, and certain mentions will be automatically parsed.
+                Using a value of true will skip any preprocessing of this nature,
+                although you can still include manual parsing strings. This field is only usable when type is mrkdwn.
+        """
+        super().__init__(text=text, type=self.type)
+        self.verbatim = verbatim
+
+    @staticmethod
+    def from_str(text: str) -> "MarkdownTextObject":
+        """Transforms a string into the required object shape to act as a MarkdownTextObject"""
+        return MarkdownTextObject(text=text)
+
+    @staticmethod
+    def direct_from_string(text: str) -> Dict[str, Any]:
+        """Transforms a string into the required object shape to act as a MarkdownTextObject"""
+        return MarkdownTextObject.from_str(text).to_dict()
+
+    @staticmethod
+    def from_link(link: Link, title: str = "") -> "MarkdownTextObject":
+        """
+        Transform a Link object directly into the required object shape
+        to act as a MarkdownTextObject
+        """
+        if title:
+            title = f": {title}"
+        return MarkdownTextObject(text=f"{link}{title}")
+
+    @staticmethod
+    def direct_from_link(link: Link, title: str = "") -> Dict[str, Any]:
+        """
+        Transform a Link object directly into the required object shape
+        to act as a MarkdownTextObject
+        """
+        return MarkdownTextObject.from_link(link, title).to_dict()
+
+

mrkdwn typed text object

+

A Markdown text object, meaning markdown characters will be parsed as +formatting information. +https://docs.slack.dev/reference/block-kit/composition-objects/text-object

+

Args

+
+
text : required
+
The text for the block. This field accepts any of the standard text formatting markup +when type is mrkdwn.
+
verbatim
+
When set to false (as is default) URLs will be auto-converted into links, +conversation names will be link-ified, and certain mentions will be automatically parsed. +Using a value of true will skip any preprocessing of this nature, +although you can still include manual parsing strings. This field is only usable when type is mrkdwn.
+
+

Ancestors

+ +

Class variables

+
+
var type
+
+

The type of the None singleton.

+
+
+

Static methods

+
+ +
+
+ +Expand source code + +
@staticmethod
+def direct_from_link(link: Link, title: str = "") -> Dict[str, Any]:
+    """
+    Transform a Link object directly into the required object shape
+    to act as a MarkdownTextObject
+    """
+    return MarkdownTextObject.from_link(link, title).to_dict()
+
+

Transform a Link object directly into the required object shape +to act as a MarkdownTextObject

+
+
+def direct_from_string(text: str) ‑> Dict[str, Any] +
+
+
+ +Expand source code + +
@staticmethod
+def direct_from_string(text: str) -> Dict[str, Any]:
+    """Transforms a string into the required object shape to act as a MarkdownTextObject"""
+    return MarkdownTextObject.from_str(text).to_dict()
+
+

Transforms a string into the required object shape to act as a MarkdownTextObject

+
+ +
+
+ +Expand source code + +
@staticmethod
+def from_link(link: Link, title: str = "") -> "MarkdownTextObject":
+    """
+    Transform a Link object directly into the required object shape
+    to act as a MarkdownTextObject
+    """
+    if title:
+        title = f": {title}"
+    return MarkdownTextObject(text=f"{link}{title}")
+
+

Transform a Link object directly into the required object shape +to act as a MarkdownTextObject

+
+
+def from_str(text: str) ‑> MarkdownTextObject +
+
+
+ +Expand source code + +
@staticmethod
+def from_str(text: str) -> "MarkdownTextObject":
+    """Transforms a string into the required object shape to act as a MarkdownTextObject"""
+    return MarkdownTextObject(text=text)
+
+

Transforms a string into the required object shape to act as a MarkdownTextObject

+
+
+

Instance variables

+
+
prop attributes : Set[str]
+
+
+ +Expand source code + +
@property
+def attributes(self) -> Set[str]:  # type: ignore[override]
+    return super().attributes.union({"verbatim"})
+
+

Build an unordered collection of unique elements.

+
+
+

Inherited members

+ +
+
+class Option +(*,
value: str,
label: str | None = None,
text: str | Dict[str, Any] | TextObject | None = None,
description: str | Dict[str, Any] | TextObject | None = None,
url: str | None = None,
**others: Dict[str, Any])
+
+
+
+ +Expand source code + +
class Option(JsonObject):
+    """Option object used in dialogs, legacy message actions (interactivity in attachments),
+    and blocks. JSON must be retrieved with an explicit option_type - the Slack API has
+    different required formats in different situations
+    """
+
+    attributes: Set[str] = set()
+    logger = logging.getLogger(__name__)
+
+    label_max_length = 75
+    value_max_length = 150
+
+    def __init__(
+        self,
+        *,
+        value: str,
+        label: Optional[str] = None,
+        text: Optional[Union[str, Dict[str, Any], TextObject]] = None,  # Block Kit
+        description: Optional[Union[str, Dict[str, Any], TextObject]] = None,
+        url: Optional[str] = None,
+        **others: Dict[str, Any],
+    ):
+        """
+        An object that represents a single selectable item in a block element (
+        SelectElement, OverflowMenuElement) or dialog element
+        (StaticDialogSelectElement)
+
+        Blocks:
+        https://docs.slack.dev/reference/block-kit/composition-objects/option-object
+
+        Dialogs:
+        https://docs.slack.dev/legacy/legacy-dialogs/#select_elements
+
+        Legacy interactive attachments:
+        https://docs.slack.dev/legacy/legacy-messaging/legacy-interactive-message-field-guide/#option_fields
+
+        Args:
+            label: A short, user-facing string to label this option to users.
+                Cannot exceed 75 characters.
+            value: A short string that identifies this particular option to your
+                application. It will be part of the payload when this option is selected
+                . Cannot exceed 150 characters.
+            description: A user-facing string that provides more details about
+                this option. Only supported in legacy message actions, not in blocks or
+                dialogs.
+        """
+        if text:
+            # For better compatibility with Block Kit ("mrkdwn" does not work for it),
+            # we've changed the default text object type to plain_text since version 3.10.0
+            self._text: Optional[TextObject] = TextObject.parse(
+                text=text,  # "text" here can be either a str or a TextObject
+                default_type=PlainTextObject.type,
+            )
+            self._label: Optional[str] = None
+        else:
+            self._text = None
+            self._label = label
+
+        # for backward-compatibility with version 2.0-2.5, the following fields return str values
+        self.text: Optional[str] = self._text.text if self._text else None
+        self.label: Optional[str] = self._label
+
+        self.value: str = value
+
+        # for backward-compatibility with version 2.0-2.5, the following fields return str values
+        if isinstance(description, str):
+            self.description = description
+            self._block_description = PlainTextObject.from_str(description)
+        elif isinstance(description, dict):
+            self.description = description["text"]
+            self._block_description = TextObject.parse(description)  # type: ignore[assignment]
+        elif isinstance(description, TextObject):
+            self.description = description.text
+            self._block_description = description  # type: ignore[assignment]
+        else:
+            self.description = None  # type: ignore[assignment]
+            self._block_description = None  # type: ignore[assignment]
+
+        # A URL to load in the user's browser when the option is clicked.
+        # The url attribute is only available in overflow menus.
+        # Maximum length for this field is 3000 characters.
+        # If you're using url, you'll still receive an interaction payload
+        # and will need to send an acknowledgement response.
+        self.url: Optional[str] = url
+        show_unknown_key_warning(self, others)
+
+    @JsonValidator(f"label attribute cannot exceed {label_max_length} characters")
+    def _validate_label_length(self) -> bool:
+        return self._label is None or len(self._label) <= self.label_max_length
+
+    @JsonValidator(f"text attribute cannot exceed {label_max_length} characters")
+    def _validate_text_length(self) -> bool:
+        return self._text is None or self._text.text is None or len(self._text.text) <= self.label_max_length
+
+    @JsonValidator(f"value attribute cannot exceed {value_max_length} characters")
+    def _validate_value_length(self) -> bool:
+        return len(self.value) <= self.value_max_length
+
+    @classmethod
+    def parse_all(cls, options: Optional[Sequence[Union[Dict[str, Any], "Option"]]]) -> Optional[List["Option"]]:
+        if options is None:
+            return None
+        option_objects: List[Option] = []
+        for o in options:
+            if isinstance(o, dict):
+                d = copy.copy(o)
+                option_objects.append(Option(**d))
+            elif isinstance(o, Option):
+                option_objects.append(o)
+            else:
+                cls.logger.warning(f"Unknown option object detected and skipped ({o})")
+        return option_objects
+
+    def to_dict(self, option_type: str = "block") -> Dict[str, Any]:
+        """
+        Different parent classes must call this with a valid value from OptionTypes -
+        either "dialog", "action", or "block", so that JSON is returned in the
+        correct shape.
+        """
+        self.validate_json()
+        if option_type == "dialog":
+            return {"label": self.label, "value": self.value}
+        elif option_type == "action" or option_type == "attachment":
+            # "action" can be confusing but it means a legacy message action in attachments
+            # we don't remove the type name for backward compatibility though
+            json: Dict[str, Any] = {"text": self.label, "value": self.value}
+            if self.description is not None:
+                json["description"] = self.description
+            return json
+        else:  # if option_type == "block"; this should be the most common case
+            text: TextObject = self._text or PlainTextObject.from_str(self.label)  # type: ignore[arg-type]
+            json = {
+                "text": text.to_dict(),
+                "value": self.value,
+            }
+            if self._block_description:
+                json["description"] = self._block_description.to_dict()
+            if self.url:
+                json["url"] = self.url
+            return json
+
+    @staticmethod
+    def from_single_value(value_and_label: str):
+        """Creates a simple Option instance with the same value and label"""
+        return Option(value=value_and_label, label=value_and_label)
+
+

Option object used in dialogs, legacy message actions (interactivity in attachments), +and blocks. JSON must be retrieved with an explicit option_type - the Slack API has +different required formats in different situations

+

An object that represents a single selectable item in a block element ( +SelectElement, OverflowMenuElement) or dialog element +(StaticDialogSelectElement)

+

Blocks: +https://docs.slack.dev/reference/block-kit/composition-objects/option-object

+

Dialogs: +https://docs.slack.dev/legacy/legacy-dialogs/#select_elements

+

Legacy interactive attachments: +https://docs.slack.dev/legacy/legacy-messaging/legacy-interactive-message-field-guide/#option_fields

+

Args

+
+
label
+
A short, user-facing string to label this option to users. +Cannot exceed 75 characters.
+
value
+
A short string that identifies this particular option to your +application. It will be part of the payload when this option is selected +. Cannot exceed 150 characters.
+
description
+
A user-facing string that provides more details about +this option. Only supported in legacy message actions, not in blocks or +dialogs.
+
+

Ancestors

+ +

Class variables

+
+
var attributes : Set[str]
+
+

The type of the None singleton.

+
+
var label_max_length
+
+

The type of the None singleton.

+
+
var logger
+
+

The type of the None singleton.

+
+
var value_max_length
+
+

The type of the None singleton.

+
+
+

Static methods

+
+
+def from_single_value(value_and_label: str) +
+
+
+ +Expand source code + +
@staticmethod
+def from_single_value(value_and_label: str):
+    """Creates a simple Option instance with the same value and label"""
+    return Option(value=value_and_label, label=value_and_label)
+
+

Creates a simple Option instance with the same value and label

+
+
+def parse_all(options: Sequence[Dict[str, Any] | ForwardRef('Option')] | None) ‑> List[Option] | None +
+
+
+
+
+

Methods

+
+
+def to_dict(self, option_type: str = 'block') ‑> Dict[str, Any] +
+
+
+ +Expand source code + +
def to_dict(self, option_type: str = "block") -> Dict[str, Any]:
+    """
+    Different parent classes must call this with a valid value from OptionTypes -
+    either "dialog", "action", or "block", so that JSON is returned in the
+    correct shape.
+    """
+    self.validate_json()
+    if option_type == "dialog":
+        return {"label": self.label, "value": self.value}
+    elif option_type == "action" or option_type == "attachment":
+        # "action" can be confusing but it means a legacy message action in attachments
+        # we don't remove the type name for backward compatibility though
+        json: Dict[str, Any] = {"text": self.label, "value": self.value}
+        if self.description is not None:
+            json["description"] = self.description
+        return json
+    else:  # if option_type == "block"; this should be the most common case
+        text: TextObject = self._text or PlainTextObject.from_str(self.label)  # type: ignore[arg-type]
+        json = {
+            "text": text.to_dict(),
+            "value": self.value,
+        }
+        if self._block_description:
+            json["description"] = self._block_description.to_dict()
+        if self.url:
+            json["url"] = self.url
+        return json
+
+

Different parent classes must call this with a valid value from OptionTypes - +either "dialog", "action", or "block", so that JSON is returned in the +correct shape.

+
+
+

Inherited members

+ +
+
+class OptionGroup +(*,
label: str | Dict[str, Any] | TextObject | None = None,
options: Sequence[Dict[str, Any] | Option],
**others: Dict[str, Any])
+
+
+
+ +Expand source code + +
class OptionGroup(JsonObject):
+    """
+    JSON must be retrieved with an explicit option_type - the Slack API has
+    different required formats in different situations
+    """
+
+    attributes: Set[str] = set()
+    label_max_length = 75
+    options_max_length = 100
+    logger = logging.getLogger(__name__)
+
+    def __init__(
+        self,
+        *,
+        label: Optional[Union[str, Dict[str, Any], TextObject]] = None,
+        options: Sequence[Union[Dict[str, Any], Option]],
+        **others: Dict[str, Any],
+    ):
+        """
+        Create a group of Option objects - pass in a label (that will be part of the
+        UI) and a list of Option objects.
+
+        Blocks:
+        https://docs.slack.dev/reference/block-kit/composition-objects/option-group-object
+
+        Dialogs:
+        https://docs.slack.dev/legacy/legacy-dialogs/#select_elements
+
+        Legacy interactive attachments:
+        https://docs.slack.dev/legacy/legacy-messaging/legacy-interactive-message-field-guide/#option_groups
+
+        Args:
+            label: Text to display at the top of this group of options.
+            options: A list of no more than 100 Option objects.
+        """  # noqa prevent flake8 blowing up on the long URL
+        # default_type=PlainTextObject.type is for backward-compatibility
+        self._label: Optional[TextObject] = TextObject.parse(label, default_type=PlainTextObject.type)  # type: ignore[arg-type] # noqa: E501
+        self.label: Optional[str] = self._label.text if self._label else None
+        self.options = Option.parse_all(options)  # compatible with version 2.5
+        show_unknown_key_warning(self, others)
+
+    @JsonValidator(f"label attribute cannot exceed {label_max_length} characters")
+    def _validate_label_length(self):
+        return self.label is None or len(self.label) <= self.label_max_length
+
+    @JsonValidator(f"options attribute cannot exceed {options_max_length} elements")
+    def _validate_options_length(self):
+        return self.options is None or len(self.options) <= self.options_max_length
+
+    @classmethod
+    def parse_all(
+        cls, option_groups: Optional[Sequence[Union[Dict[str, Any], "OptionGroup"]]]
+    ) -> Optional[List["OptionGroup"]]:
+        if option_groups is None:
+            return None
+        option_group_objects = []
+        for o in option_groups:
+            if isinstance(o, dict):
+                d = copy.copy(o)
+                option_group_objects.append(OptionGroup(**d))
+            elif isinstance(o, OptionGroup):
+                option_group_objects.append(o)
+            else:
+                cls.logger.warning(f"Unknown option group object detected and skipped ({o})")
+        return option_group_objects
+
+    def to_dict(self, option_type: str = "block") -> Dict[str, Any]:
+        self.validate_json()
+        dict_options = [o.to_dict(option_type) for o in self.options]  # type: ignore[union-attr]
+        if option_type == "dialog":
+            return {
+                "label": self.label,
+                "options": dict_options,
+            }
+        elif option_type == "action":
+            return {
+                "text": self.label,
+                "options": dict_options,
+            }
+        else:  # if option_type == "block"; this should be the most common case
+            dict_label: Dict[str, Any] = self._label.to_dict()  # type: ignore[union-attr]
+            return {
+                "label": dict_label,
+                "options": dict_options,
+            }
+
+

JSON must be retrieved with an explicit option_type - the Slack API has +different required formats in different situations

+

Create a group of Option objects - pass in a label (that will be part of the +UI) and a list of Option objects.

+

Blocks: +https://docs.slack.dev/reference/block-kit/composition-objects/option-group-object

+

Dialogs: +https://docs.slack.dev/legacy/legacy-dialogs/#select_elements

+

Legacy interactive attachments: +https://docs.slack.dev/legacy/legacy-messaging/legacy-interactive-message-field-guide/#option_groups

+

Args

+
+
label
+
Text to display at the top of this group of options.
+
options
+
A list of no more than 100 Option objects.
+
+

Ancestors

+ +

Class variables

+
+
var attributes : Set[str]
+
+

The type of the None singleton.

+
+
var label_max_length
+
+

The type of the None singleton.

+
+
var logger
+
+

The type of the None singleton.

+
+
var options_max_length
+
+

The type of the None singleton.

+
+
+

Static methods

+
+
+def parse_all(option_groups: Sequence[Dict[str, Any] | ForwardRef('OptionGroup')] | None) ‑> List[OptionGroup] | None +
+
+
+
+
+

Inherited members

+ +
+
+class PlainTextObject +(*, text: str, emoji: bool | None = None) +
+
+
+ +Expand source code + +
class PlainTextObject(TextObject):
+    """plain_text typed text object"""
+
+    type = "plain_text"
+
+    @property
+    def attributes(self) -> Set[str]:  # type: ignore[override]
+        return super().attributes.union({"emoji"})
+
+    def __init__(self, *, text: str, emoji: Optional[bool] = None):
+        """A plain text object, meaning markdown characters will not be parsed as
+        formatting information.
+        https://docs.slack.dev/reference/block-kit/composition-objects/text-object
+
+        Args:
+            text (required): The text for the block. This field accepts any of the standard text formatting markup
+                when type is mrkdwn.
+            emoji: Indicates whether emojis in a text field should be escaped into the colon emoji format.
+                This field is only usable when type is plain_text.
+        """
+        super().__init__(text=text, type=self.type)
+        self.emoji = emoji
+
+    @staticmethod
+    def from_str(text: str) -> "PlainTextObject":
+        return PlainTextObject(text=text, emoji=True)
+
+    @staticmethod
+    def direct_from_string(text: str) -> Dict[str, Any]:
+        """Transforms a string into the required object shape to act as a PlainTextObject"""
+        return PlainTextObject.from_str(text).to_dict()
+
+

plain_text typed text object

+

A plain text object, meaning markdown characters will not be parsed as +formatting information. +https://docs.slack.dev/reference/block-kit/composition-objects/text-object

+

Args

+
+
text : required
+
The text for the block. This field accepts any of the standard text formatting markup +when type is mrkdwn.
+
emoji
+
Indicates whether emojis in a text field should be escaped into the colon emoji format. +This field is only usable when type is plain_text.
+
+

Ancestors

+ +

Class variables

+
+
var type
+
+

The type of the None singleton.

+
+
+

Static methods

+
+
+def direct_from_string(text: str) ‑> Dict[str, Any] +
+
+
+ +Expand source code + +
@staticmethod
+def direct_from_string(text: str) -> Dict[str, Any]:
+    """Transforms a string into the required object shape to act as a PlainTextObject"""
+    return PlainTextObject.from_str(text).to_dict()
+
+

Transforms a string into the required object shape to act as a PlainTextObject

+
+
+def from_str(text: str) ‑> PlainTextObject +
+
+
+ +Expand source code + +
@staticmethod
+def from_str(text: str) -> "PlainTextObject":
+    return PlainTextObject(text=text, emoji=True)
+
+
+
+
+

Instance variables

+
+
prop attributes : Set[str]
+
+
+ +Expand source code + +
@property
+def attributes(self) -> Set[str]:  # type: ignore[override]
+    return super().attributes.union({"emoji"})
+
+

Build an unordered collection of unique elements.

+
+
+

Inherited members

+ +
+
+class RawTextObject +(*, text: str) +
+
+
+ +Expand source code + +
class RawTextObject(TextObject):
+    """raw_text typed text object"""
+
+    type = "raw_text"
+
+    @property
+    def attributes(self) -> Set[str]:  # type: ignore[override]
+        return {"text", "type"}
+
+    def __init__(self, *, text: str):
+        """A raw text object used in table block cells.
+        https://docs.slack.dev/reference/block-kit/composition-objects/text-object/
+        https://docs.slack.dev/reference/block-kit/blocks/table-block
+
+        Args:
+            text (required): The text content for the table block cell.
+        """
+        super().__init__(text=text, type=self.type)
+
+    @staticmethod
+    def from_str(text: str) -> "RawTextObject":
+        """Transforms a string into a RawTextObject"""
+        return RawTextObject(text=text)
+
+    @staticmethod
+    def direct_from_string(text: str) -> Dict[str, Any]:
+        """Transforms a string into the required object shape to act as a RawTextObject"""
+        return RawTextObject.from_str(text).to_dict()
+
+    @JsonValidator("text attribute must have at least 1 character")
+    def _validate_text_min_length(self):
+        return len(self.text) >= 1
+
+

raw_text typed text object

+

A raw text object used in table block cells. +https://docs.slack.dev/reference/block-kit/composition-objects/text-object/ +https://docs.slack.dev/reference/block-kit/blocks/table-block

+

Args

+
+
text : required
+
The text content for the table block cell.
+
+

Ancestors

+ +

Class variables

+
+
var type
+
+

The type of the None singleton.

+
+
+

Static methods

+
+
+def direct_from_string(text: str) ‑> Dict[str, Any] +
+
+
+ +Expand source code + +
@staticmethod
+def direct_from_string(text: str) -> Dict[str, Any]:
+    """Transforms a string into the required object shape to act as a RawTextObject"""
+    return RawTextObject.from_str(text).to_dict()
+
+

Transforms a string into the required object shape to act as a RawTextObject

+
+
+def from_str(text: str) ‑> RawTextObject +
+
+
+ +Expand source code + +
@staticmethod
+def from_str(text: str) -> "RawTextObject":
+    """Transforms a string into a RawTextObject"""
+    return RawTextObject(text=text)
+
+

Transforms a string into a RawTextObject

+
+
+

Instance variables

+
+
prop attributes : Set[str]
+
+
+ +Expand source code + +
@property
+def attributes(self) -> Set[str]:  # type: ignore[override]
+    return {"text", "type"}
+
+

Build an unordered collection of unique elements.

+
+
+

Inherited members

+ +
+
+class SlackFile +(*, id: str | None = None, url: str | None = None) +
+
+
+ +Expand source code + +
class SlackFile(JsonObject):
+    attributes = {"id", "url"}
+
+    def __init__(
+        self,
+        *,
+        id: Optional[str] = None,
+        url: Optional[str] = None,
+    ):
+        """An object containing Slack file information to be used in an image block or image element.
+        https://docs.slack.dev/reference/block-kit/composition-objects/slack-file-object
+
+        Args:
+            id: Slack ID of the file.
+            url: This URL can be the url_private or the permalink of the Slack file.
+        """
+        self._id = id
+        self._url = url
+
+    def to_dict(self) -> Dict[str, Any]:
+        self.validate_json()
+        json = {}
+        if self._id is not None:
+            json["id"] = self._id
+        if self._url is not None:
+            json["url"] = self._url
+        return json
+
+

The base class for JSON serializable class objects

+

An object containing Slack file information to be used in an image block or image element. +https://docs.slack.dev/reference/block-kit/composition-objects/slack-file-object

+

Args

+
+
id
+
Slack ID of the file.
+
url
+
This URL can be the url_private or the permalink of the Slack file.
+
+

Ancestors

+ +

Class variables

+
+
var attributes
+
+

The type of the None singleton.

+
+
+

Inherited members

+ +
+
+class TextObject +(text: str,
type: str | None = None,
subtype: str | None = None,
emoji: bool | None = None,
**kwargs)
+
+
+
+ +Expand source code + +
class TextObject(JsonObject):
+    """The interface for text objects (types: plain_text, mrkdwn)"""
+
+    attributes = {"text", "type", "emoji"}
+    logger = logging.getLogger(__name__)
+
+    def _subtype_warning(self):
+        warnings.warn(
+            "subtype is deprecated since slackclient 2.6.0, use type instead",
+            DeprecationWarning,
+        )
+
+    @property
+    def subtype(self) -> Optional[str]:
+        return self.type
+
+    @classmethod
+    def parse(
+        cls,
+        text: Union[str, Dict[str, Any], "TextObject"],
+        default_type: str = "mrkdwn",
+    ) -> Optional["TextObject"]:
+        if not text:
+            return None
+        elif isinstance(text, str):
+            if default_type == PlainTextObject.type:
+                return PlainTextObject.from_str(text)
+            else:
+                return MarkdownTextObject.from_str(text)
+        elif isinstance(text, dict):
+            d = copy.copy(text)
+            t = d.pop("type")
+            if t == PlainTextObject.type:
+                return PlainTextObject(**d)
+            else:
+                return MarkdownTextObject(**d)
+        elif isinstance(text, TextObject):
+            return text
+        else:
+            cls.logger.warning(f"Unknown type ({type(text)}) detected when parsing a TextObject")
+            return None
+
+    def __init__(
+        self,
+        text: str,
+        type: Optional[str] = None,
+        subtype: Optional[str] = None,
+        emoji: Optional[bool] = None,
+        **kwargs,
+    ):
+        """Super class for new text "objects" used in Block kit"""
+        if subtype:
+            self._subtype_warning()
+
+        self.text = text
+        self.type = type if type else subtype
+        self.emoji = emoji
+
+

The interface for text objects (types: plain_text, mrkdwn)

+

Super class for new text "objects" used in Block kit

+

Ancestors

+ +

Subclasses

+ +

Class variables

+
+
var attributes
+
+

The type of the None singleton.

+
+
var logger
+
+

The type of the None singleton.

+
+
+

Static methods

+
+
+def parse(text: str | Dict[str, Any] | ForwardRef('TextObject'),
default_type: str = 'mrkdwn') ‑> TextObject | None
+
+
+
+
+
+

Instance variables

+
+
prop subtype : str | None
+
+
+ +Expand source code + +
@property
+def subtype(self) -> Optional[str]:
+    return self.type
+
+
+
+
+

Inherited members

+ +
+
+class Workflow +(*,
trigger: WorkflowTrigger | dict)
+
+
+
+ +Expand source code + +
class Workflow(JsonObject):
+    attributes = {"trigger"}
+
+    def __init__(
+        self,
+        *,
+        trigger: Union[WorkflowTrigger, dict],
+    ):
+        self._trigger = trigger
+
+    def to_dict(self) -> Dict[str, Any]:
+        self.validate_json()
+        json = {}
+        if isinstance(self._trigger, WorkflowTrigger):
+            json["trigger"] = self._trigger.to_dict()
+        else:
+            json["trigger"] = self._trigger
+        return json
+
+

The base class for JSON serializable class objects

+

Ancestors

+ +

Class variables

+
+
var attributes
+
+

The type of the None singleton.

+
+
+

Inherited members

+ +
+
+class WorkflowTrigger +(*, url: str, customizable_input_parameters: List[Dict[str, str]] | None = None) +
+
+
+ +Expand source code + +
class WorkflowTrigger(JsonObject):
+    attributes = {"trigger"}
+
+    def __init__(self, *, url: str, customizable_input_parameters: Optional[List[Dict[str, str]]] = None):
+        self._url = url
+        self._customizable_input_parameters = customizable_input_parameters
+
+    def to_dict(self) -> Dict[str, Any]:
+        self.validate_json()
+        json = {"url": self._url}
+        if self._customizable_input_parameters is not None:
+            json.update({"customizable_input_parameters": self._customizable_input_parameters})  # type: ignore[dict-item]
+        return json
+
+

The base class for JSON serializable class objects

+

Ancestors

+ +

Class variables

+
+
var attributes
+
+

The type of the None singleton.

+
+
+

Inherited members

+ +
+
+
+
+ +
+ + + diff --git a/docs/reference/models/blocks/block_elements.html b/docs/reference/models/blocks/block_elements.html new file mode 100644 index 000000000..375ee07ce --- /dev/null +++ b/docs/reference/models/blocks/block_elements.html @@ -0,0 +1,5350 @@ + + + + + + +slack_sdk.models.blocks.block_elements API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.models.blocks.block_elements

+
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class BlockElement +(*, type: str | None = None, subtype: str | None = None, **others: dict) +
+
+
+ +Expand source code + +
class BlockElement(JsonObject, metaclass=ABCMeta):
+    """Block Elements are things that exists inside of your Blocks.
+    https://docs.slack.dev/reference/block-kit/block-elements/
+    """
+
+    attributes = {"type"}
+    logger = logging.getLogger(__name__)
+
+    def _subtype_warning(self):
+        warnings.warn(
+            "subtype is deprecated since slackclient 2.6.0, use type instead",
+            DeprecationWarning,
+        )
+
+    @property
+    def subtype(self) -> Optional[str]:
+        return self.type
+
+    def __init__(
+        self,
+        *,
+        type: Optional[str] = None,
+        subtype: Optional[str] = None,
+        **others: dict,
+    ):
+        if subtype:
+            self._subtype_warning()
+        self.type = type if type else subtype
+        show_unknown_key_warning(self, others)
+
+    @classmethod
+    def parse(cls, block_element: Union[dict, "BlockElement"]) -> Optional[Union["BlockElement", TextObject]]:
+        if block_element is None:
+            return None
+        elif isinstance(block_element, dict):
+            if "type" in block_element:
+                d = copy.copy(block_element)
+                t = d.pop("type")
+                for subclass in cls._get_sub_block_elements():
+                    if t == subclass.type:
+                        return subclass(**d)
+                if t == PlainTextObject.type:
+                    return PlainTextObject(**d)
+                elif t == MarkdownTextObject.type:
+                    return MarkdownTextObject(**d)
+        elif isinstance(block_element, (TextObject, BlockElement)):
+            return block_element
+        cls.logger.warning(f"Unknown element detected and skipped ({block_element})")
+        return None
+
+    @classmethod
+    def parse_all(
+        cls, block_elements: Sequence[Union[dict, "BlockElement", TextObject]]
+    ) -> List[Union["BlockElement", TextObject]]:
+        return [cls.parse(e) for e in block_elements or []]  # type: ignore[arg-type, misc]
+
+    @classmethod
+    def _get_sub_block_elements(cls: Type["BlockElement"]) -> Iterator[Type["BlockElement"]]:
+        for subclass in cls.__subclasses__():
+            if hasattr(subclass, "type"):
+                yield subclass
+            yield from subclass._get_sub_block_elements()
+
+

Block Elements are things that exists inside of your Blocks. +https://docs.slack.dev/reference/block-kit/block-elements/

+

Ancestors

+ +

Subclasses

+ +

Class variables

+
+
var attributes
+
+

The type of the None singleton.

+
+
var logger
+
+

The type of the None singleton.

+
+
+

Static methods

+
+
+def parse(block_element: dict | ForwardRef('BlockElement')) ‑> BlockElement | TextObject | None +
+
+
+
+
+def parse_all(block_elements: Sequence[dict | ForwardRef('BlockElement') | TextObject]) ‑> List[BlockElement | TextObject] +
+
+
+
+
+

Instance variables

+
+
prop subtype : str | None
+
+
+ +Expand source code + +
@property
+def subtype(self) -> Optional[str]:
+    return self.type
+
+
+
+
+

Inherited members

+ +
+
+class ButtonElement +(*,
text: str | dict | TextObject,
action_id: str | None = None,
url: str | None = None,
value: str | None = None,
style: str | None = None,
confirm: dict | ConfirmObject | None = None,
accessibility_label: str | None = None,
**others: dict)
+
+
+
+ +Expand source code + +
class ButtonElement(InteractiveElement):
+    type = "button"
+    text_max_length = 75
+    url_max_length = 3000
+    value_max_length = 2000
+
+    @property
+    def attributes(self) -> Set[str]:  # type: ignore[override]
+        return super().attributes.union({"text", "url", "value", "style", "confirm", "accessibility_label"})
+
+    def __init__(
+        self,
+        *,
+        text: Union[str, dict, TextObject],
+        action_id: Optional[str] = None,
+        url: Optional[str] = None,
+        value: Optional[str] = None,
+        style: Optional[str] = None,  # primary, danger
+        confirm: Optional[Union[dict, ConfirmObject]] = None,
+        accessibility_label: Optional[str] = None,
+        **others: dict,
+    ):
+        """An interactive element that inserts a button. The button can be a trigger for
+        anything from opening a simple link to starting a complex workflow.
+        https://docs.slack.dev/reference/block-kit/block-elements/button-element/
+
+        Args:
+            text (required): A text object that defines the button's text.
+                Can only be of type: plain_text.
+                Maximum length for the text in this field is 75 characters.
+            action_id (required): An identifier for this action.
+                You can use this when you receive an interaction payload to identify the source of the action.
+                Should be unique among all other action_ids in the containing block.
+                Maximum length for this field is 255 characters.
+            url: A URL to load in the user's browser when the button is clicked.
+                Maximum length for this field is 3000 characters.
+                If you're using url, you'll still receive an interaction payload
+                and will need to send an acknowledgement response.
+            value: The value to send along with the interaction payload.
+                Maximum length for this field is 2000 characters.
+            style: Decorates buttons with alternative visual color schemes. Use this option with restraint.
+                "primary" gives buttons a green outline and text, ideal for affirmation or confirmation actions.
+                "primary" should only be used for one button within a set.
+                "danger" gives buttons a red outline and text, and should be used when the action is destructive.
+                Use "danger" even more sparingly than "primary".
+                If you don't include this field, the default button style will be used.
+            confirm: A confirm object that defines an optional confirmation dialog after the button is clicked.
+            accessibility_label: A label for longer descriptive text about a button element.
+                This label will be read out by screen readers instead of the button text object.
+                Maximum length for this field is 75 characters.
+        """
+        super().__init__(action_id=action_id, type=self.type)
+        show_unknown_key_warning(self, others)
+
+        # NOTE: default_type=PlainTextObject.type here is only for backward-compatibility with version 2.5.0
+        self.text = TextObject.parse(text, default_type=PlainTextObject.type)
+        self.url = url
+        self.value = value
+        self.style = style
+        self.confirm = ConfirmObject.parse(confirm)  # type: ignore[arg-type]
+        self.accessibility_label = accessibility_label
+
+    @JsonValidator(f"text attribute cannot exceed {text_max_length} characters")
+    def _validate_text_length(self) -> bool:
+        return self.text is None or self.text.text is None or len(self.text.text) <= self.text_max_length
+
+    @JsonValidator(f"url attribute cannot exceed {url_max_length} characters")
+    def _validate_url_length(self) -> bool:
+        return self.url is None or len(self.url) <= self.url_max_length
+
+    @JsonValidator(f"value attribute cannot exceed {value_max_length} characters")
+    def _validate_value_length(self) -> bool:
+        return self.value is None or len(self.value) <= self.value_max_length
+
+    @EnumValidator("style", ButtonStyles)
+    def _validate_style_valid(self):
+        return self.style is None or self.style in ButtonStyles
+
+    @JsonValidator(f"accessibility_label attribute cannot exceed {text_max_length} characters")
+    def _validate_accessibility_label_length(self) -> bool:
+        return self.accessibility_label is None or len(self.accessibility_label) <= self.text_max_length
+
+

Block Elements are things that exists inside of your Blocks. +https://docs.slack.dev/reference/block-kit/block-elements/

+

An interactive element that inserts a button. The button can be a trigger for +anything from opening a simple link to starting a complex workflow. +https://docs.slack.dev/reference/block-kit/block-elements/button-element/

+

Args

+
+
text : required
+
A text object that defines the button's text. +Can only be of type: plain_text. +Maximum length for the text in this field is 75 characters.
+
action_id : required
+
An identifier for this action. +You can use this when you receive an interaction payload to identify the source of the action. +Should be unique among all other action_ids in the containing block. +Maximum length for this field is 255 characters.
+
url
+
A URL to load in the user's browser when the button is clicked. +Maximum length for this field is 3000 characters. +If you're using url, you'll still receive an interaction payload +and will need to send an acknowledgement response.
+
value
+
The value to send along with the interaction payload. +Maximum length for this field is 2000 characters.
+
style
+
Decorates buttons with alternative visual color schemes. Use this option with restraint. +"primary" gives buttons a green outline and text, ideal for affirmation or confirmation actions. +"primary" should only be used for one button within a set. +"danger" gives buttons a red outline and text, and should be used when the action is destructive. +Use "danger" even more sparingly than "primary". +If you don't include this field, the default button style will be used.
+
confirm
+
A confirm object that defines an optional confirmation dialog after the button is clicked.
+
accessibility_label
+
A label for longer descriptive text about a button element. +This label will be read out by screen readers instead of the button text object. +Maximum length for this field is 75 characters.
+
+

Ancestors

+ +

Subclasses

+ +

Class variables

+
+
var text_max_length
+
+

The type of the None singleton.

+
+
var type
+
+

The type of the None singleton.

+
+
var url_max_length
+
+

The type of the None singleton.

+
+
var value_max_length
+
+

The type of the None singleton.

+
+
+

Inherited members

+ +
+
+class ChannelMultiSelectElement +(*,
placeholder: str | dict | TextObject | None = None,
action_id: str | None = None,
initial_channels: Sequence[str] | None = None,
confirm: dict | ConfirmObject | None = None,
max_selected_items: int | None = None,
focus_on_load: bool | None = None,
**others: dict)
+
+
+
+ +Expand source code + +
class ChannelMultiSelectElement(InputInteractiveElement):
+    type = "multi_channels_select"
+
+    @property
+    def attributes(self) -> Set[str]:  # type: ignore[override]
+        return super().attributes.union({"initial_channels", "max_selected_items"})
+
+    def __init__(
+        self,
+        *,
+        placeholder: Optional[Union[str, dict, TextObject]] = None,
+        action_id: Optional[str] = None,
+        initial_channels: Optional[Sequence[str]] = None,
+        confirm: Optional[Union[dict, ConfirmObject]] = None,
+        max_selected_items: Optional[int] = None,
+        focus_on_load: Optional[bool] = None,
+        **others: dict,
+    ):
+        """
+        This multi-select menu will populate its options with a list of public channels visible
+        to the current user in the active workspace.
+        https://docs.slack.dev/reference/block-kit/block-elements/multi-select-menu-element#channel_multi_select
+
+        Args:
+            placeholder (required): A plain_text only text object that defines the placeholder text shown on the menu.
+                Maximum length for the text in this field is 150 characters.
+            action_id (required): An identifier for the action triggered when a menu option is selected.
+                You can use this when you receive an interaction payload to identify the source of the action.
+                Should be unique among all other action_ids in the containing block.
+                Maximum length for this field is 255 characters.
+            initial_channels: An array of one or more IDs of any valid public channel
+                to be pre-selected when the menu loads.
+            confirm: A confirm object that defines an optional confirmation dialog that appears
+                before the multi-select choices are submitted.
+            max_selected_items: Specifies the maximum number of items that can be selected in the menu.
+                Minimum number is 1.
+            focus_on_load: Indicates whether the element will be set to auto focus within the view object.
+                Only one element can be set to true. Defaults to false.
+        """
+        super().__init__(
+            type=self.type,
+            action_id=action_id,
+            placeholder=TextObject.parse(placeholder, PlainTextObject.type),  # type: ignore[arg-type]
+            confirm=ConfirmObject.parse(confirm),  # type: ignore[arg-type]
+            focus_on_load=focus_on_load,
+        )
+        show_unknown_key_warning(self, others)
+
+        self.initial_channels = initial_channels
+        self.max_selected_items = max_selected_items
+
+

Block Elements are things that exists inside of your Blocks. +https://docs.slack.dev/reference/block-kit/block-elements/

+

This multi-select menu will populate its options with a list of public channels visible +to the current user in the active workspace. +https://docs.slack.dev/reference/block-kit/block-elements/multi-select-menu-element#channel_multi_select

+

Args

+
+
placeholder : required
+
A plain_text only text object that defines the placeholder text shown on the menu. +Maximum length for the text in this field is 150 characters.
+
action_id : required
+
An identifier for the action triggered when a menu option is selected. +You can use this when you receive an interaction payload to identify the source of the action. +Should be unique among all other action_ids in the containing block. +Maximum length for this field is 255 characters.
+
initial_channels
+
An array of one or more IDs of any valid public channel +to be pre-selected when the menu loads.
+
confirm
+
A confirm object that defines an optional confirmation dialog that appears +before the multi-select choices are submitted.
+
max_selected_items
+
Specifies the maximum number of items that can be selected in the menu. +Minimum number is 1.
+
focus_on_load
+
Indicates whether the element will be set to auto focus within the view object. +Only one element can be set to true. Defaults to false.
+
+

Ancestors

+ +

Class variables

+
+
var type
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
prop attributes : Set[str]
+
+
+ +Expand source code + +
@property
+def attributes(self) -> Set[str]:  # type: ignore[override]
+    return super().attributes.union({"initial_channels", "max_selected_items"})
+
+

Build an unordered collection of unique elements.

+
+
+

Inherited members

+ +
+
+class ChannelSelectElement +(*,
placeholder: str | dict | TextObject | None = None,
action_id: str | None = None,
initial_channel: str | None = None,
confirm: dict | ConfirmObject | None = None,
response_url_enabled: bool | None = None,
focus_on_load: bool | None = None,
**others: dict)
+
+
+
+ +Expand source code + +
class ChannelSelectElement(InputInteractiveElement):
+    type = "channels_select"
+
+    @property
+    def attributes(self) -> Set[str]:  # type: ignore[override]
+        return super().attributes.union({"initial_channel", "response_url_enabled"})
+
+    def __init__(
+        self,
+        *,
+        placeholder: Optional[Union[str, dict, TextObject]] = None,
+        action_id: Optional[str] = None,
+        initial_channel: Optional[str] = None,
+        confirm: Optional[Union[dict, ConfirmObject]] = None,
+        response_url_enabled: Optional[bool] = None,
+        focus_on_load: Optional[bool] = None,
+        **others: dict,
+    ):
+        """
+        This select menu will populate its options with a list of public channels
+        visible to the current user in the active workspace.
+        https://docs.slack.dev/reference/block-kit/block-elements/select-menu-element/#channels_select
+
+        Args:
+            placeholder (required): A plain_text only text object that defines the placeholder text shown on the menu.
+                Maximum length for the text in this field is 150 characters.
+            action_id (required): An identifier for the action triggered when a menu option is selected.
+                You can use this when you receive an interaction payload to identify the source of the action.
+                Should be unique among all other action_ids in the containing block.
+                Maximum length for this field is 255 characters.
+            initial_channel: The ID of any valid public channel to be pre-selected when the menu loads.
+            confirm: A confirm object that defines an optional confirmation dialog that appears
+                after a menu item is selected.
+            response_url_enabled: This field only works with menus in input blocks in modals.
+                When set to true, the view_submission payload from the menu's parent view will contain a response_url.
+                This response_url can be used for message responses.
+                The target channel for the message will be determined by the value of this select menu
+            focus_on_load: Indicates whether the element will be set to auto focus within the view object.
+                Only one element can be set to true. Defaults to false.
+        """
+        super().__init__(
+            type=self.type,
+            action_id=action_id,
+            placeholder=TextObject.parse(placeholder, PlainTextObject.type),  # type: ignore[arg-type]
+            confirm=ConfirmObject.parse(confirm),  # type: ignore[arg-type]
+            focus_on_load=focus_on_load,
+        )
+        show_unknown_key_warning(self, others)
+
+        self.initial_channel = initial_channel
+        self.response_url_enabled = response_url_enabled
+
+

Block Elements are things that exists inside of your Blocks. +https://docs.slack.dev/reference/block-kit/block-elements/

+

This select menu will populate its options with a list of public channels +visible to the current user in the active workspace. +https://docs.slack.dev/reference/block-kit/block-elements/select-menu-element/#channels_select

+

Args

+
+
placeholder : required
+
A plain_text only text object that defines the placeholder text shown on the menu. +Maximum length for the text in this field is 150 characters.
+
action_id : required
+
An identifier for the action triggered when a menu option is selected. +You can use this when you receive an interaction payload to identify the source of the action. +Should be unique among all other action_ids in the containing block. +Maximum length for this field is 255 characters.
+
initial_channel
+
The ID of any valid public channel to be pre-selected when the menu loads.
+
confirm
+
A confirm object that defines an optional confirmation dialog that appears +after a menu item is selected.
+
response_url_enabled
+
This field only works with menus in input blocks in modals. +When set to true, the view_submission payload from the menu's parent view will contain a response_url. +This response_url can be used for message responses. +The target channel for the message will be determined by the value of this select menu
+
focus_on_load
+
Indicates whether the element will be set to auto focus within the view object. +Only one element can be set to true. Defaults to false.
+
+

Ancestors

+ +

Class variables

+
+
var type
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
prop attributes : Set[str]
+
+
+ +Expand source code + +
@property
+def attributes(self) -> Set[str]:  # type: ignore[override]
+    return super().attributes.union({"initial_channel", "response_url_enabled"})
+
+

Build an unordered collection of unique elements.

+
+
+

Inherited members

+ +
+
+class CheckboxesElement +(*,
action_id: str | None = None,
options: Sequence[dict | Option] | None = None,
initial_options: Sequence[dict | Option] | None = None,
confirm: dict | ConfirmObject | None = None,
focus_on_load: bool | None = None,
**others: dict)
+
+
+
+ +Expand source code + +
class CheckboxesElement(InputInteractiveElement):
+    type = "checkboxes"
+
+    @property
+    def attributes(self) -> Set[str]:  # type: ignore[override]
+        return super().attributes.union({"options", "initial_options"})
+
+    def __init__(
+        self,
+        *,
+        action_id: Optional[str] = None,
+        options: Optional[Sequence[Union[dict, Option]]] = None,
+        initial_options: Optional[Sequence[Union[dict, Option]]] = None,
+        confirm: Optional[Union[dict, ConfirmObject]] = None,
+        focus_on_load: Optional[bool] = None,
+        **others: dict,
+    ):
+        """A checkbox group that allows a user to choose multiple items from a list of possible options.
+        https://docs.slack.dev/reference/block-kit/block-elements/checkboxes-element/
+
+        Args:
+            action_id (required): An identifier for the action triggered when the checkbox group is changed.
+                You can use this when you receive an interaction payload to identify the source of the action.
+                Should be unique among all other action_ids in the containing block.
+                Maximum length for this field is 255 characters.
+            options (required): An array of option objects. A maximum of 10 options are allowed.
+            initial_options: An array of option objects that exactly matches one or more of the options.
+                These options will be selected when the checkbox group initially loads.
+            confirm: A confirm object that defines an optional confirmation dialog that appears
+                after clicking one of the checkboxes in this element.
+            focus_on_load: Indicates whether the element will be set to auto focus within the view object.
+                Only one element can be set to true. Defaults to false.
+        """
+        super().__init__(
+            type=self.type,
+            action_id=action_id,
+            confirm=ConfirmObject.parse(confirm),  # type: ignore[arg-type]
+            focus_on_load=focus_on_load,
+        )
+        show_unknown_key_warning(self, others)
+
+        self.options = Option.parse_all(options)
+        self.initial_options = Option.parse_all(initial_options)
+
+

Block Elements are things that exists inside of your Blocks. +https://docs.slack.dev/reference/block-kit/block-elements/

+

A checkbox group that allows a user to choose multiple items from a list of possible options. +https://docs.slack.dev/reference/block-kit/block-elements/checkboxes-element/

+

Args

+
+
action_id : required
+
An identifier for the action triggered when the checkbox group is changed. +You can use this when you receive an interaction payload to identify the source of the action. +Should be unique among all other action_ids in the containing block. +Maximum length for this field is 255 characters.
+
options : required
+
An array of option objects. A maximum of 10 options are allowed.
+
initial_options
+
An array of option objects that exactly matches one or more of the options. +These options will be selected when the checkbox group initially loads.
+
confirm
+
A confirm object that defines an optional confirmation dialog that appears +after clicking one of the checkboxes in this element.
+
focus_on_load
+
Indicates whether the element will be set to auto focus within the view object. +Only one element can be set to true. Defaults to false.
+
+

Ancestors

+ +

Class variables

+
+
var type
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
prop attributes : Set[str]
+
+
+ +Expand source code + +
@property
+def attributes(self) -> Set[str]:  # type: ignore[override]
+    return super().attributes.union({"options", "initial_options"})
+
+

Build an unordered collection of unique elements.

+
+
+

Inherited members

+ +
+
+class ConversationFilter +(*,
include: Sequence[str] | None = None,
exclude_bot_users: bool | None = None,
exclude_external_shared_channels: bool | None = None)
+
+
+
+ +Expand source code + +
class ConversationFilter(JsonObject):
+    attributes = {"include", "exclude_bot_users", "exclude_external_shared_channels"}
+    logger = logging.getLogger(__name__)
+
+    def __init__(
+        self,
+        *,
+        include: Optional[Sequence[str]] = None,
+        exclude_bot_users: Optional[bool] = None,
+        exclude_external_shared_channels: Optional[bool] = None,
+    ):
+        """Provides a way to filter the list of options in a conversations select menu
+        or conversations multi-select menu.
+        https://docs.slack.dev/reference/block-kit/composition-objects/conversation-filter-object
+
+        Args:
+            include: Indicates which type of conversations should be included in the list.
+                When this field is provided, any conversations that do not match will be excluded.
+                You should provide an array of strings from the following options:
+                "im", "mpim", "private", and "public". The array cannot be empty.
+            exclude_bot_users: Indicates whether to exclude bot users from conversation lists. Defaults to false.
+            exclude_external_shared_channels: Indicates whether to exclude external shared channels
+                from conversation lists. Defaults to false.
+        """
+        self.include = include
+        self.exclude_bot_users = exclude_bot_users
+        self.exclude_external_shared_channels = exclude_external_shared_channels
+
+    @classmethod
+    def parse(cls, filter: Union[dict, "ConversationFilter"]):
+        if filter is None:
+            return None
+        elif isinstance(filter, ConversationFilter):
+            return filter
+        elif isinstance(filter, dict):
+            d = copy.copy(filter)
+            return ConversationFilter(**d)
+        else:
+            cls.logger.warning(f"Unknown conversation filter object detected and skipped ({filter})")
+            return None
+
+

The base class for JSON serializable class objects

+

Provides a way to filter the list of options in a conversations select menu +or conversations multi-select menu. +https://docs.slack.dev/reference/block-kit/composition-objects/conversation-filter-object

+

Args

+
+
include
+
Indicates which type of conversations should be included in the list. +When this field is provided, any conversations that do not match will be excluded. +You should provide an array of strings from the following options: +"im", "mpim", "private", and "public". The array cannot be empty.
+
exclude_bot_users
+
Indicates whether to exclude bot users from conversation lists. Defaults to false.
+
exclude_external_shared_channels
+
Indicates whether to exclude external shared channels +from conversation lists. Defaults to false.
+
+

Ancestors

+ +

Class variables

+
+
var attributes
+
+

The type of the None singleton.

+
+
var logger
+
+

The type of the None singleton.

+
+
+

Static methods

+
+
+def parse(filter: dict | ForwardRef('ConversationFilter')) +
+
+
+
+
+

Inherited members

+ +
+
+class ConversationMultiSelectElement +(*,
placeholder: str | dict | TextObject | None = None,
action_id: str | None = None,
initial_conversations: Sequence[str] | None = None,
confirm: dict | ConfirmObject | None = None,
max_selected_items: int | None = None,
default_to_current_conversation: bool | None = None,
filter: dict | ConversationFilter | None = None,
focus_on_load: bool | None = None,
**others: dict)
+
+
+
+ +Expand source code + +
class ConversationMultiSelectElement(InputInteractiveElement):
+    type = "multi_conversations_select"
+
+    @property
+    def attributes(self) -> Set[str]:  # type: ignore[override]
+        return super().attributes.union(
+            {
+                "initial_conversations",
+                "max_selected_items",
+                "default_to_current_conversation",
+                "filter",
+            }
+        )
+
+    def __init__(
+        self,
+        *,
+        placeholder: Optional[Union[str, dict, TextObject]] = None,
+        action_id: Optional[str] = None,
+        initial_conversations: Optional[Sequence[str]] = None,
+        confirm: Optional[Union[dict, ConfirmObject]] = None,
+        max_selected_items: Optional[int] = None,
+        default_to_current_conversation: Optional[bool] = None,
+        filter: Optional[Union[dict, ConversationFilter]] = None,
+        focus_on_load: Optional[bool] = None,
+        **others: dict,
+    ):
+        """
+        This multi-select menu will populate its options with a list of public and private channels,
+        DMs, and MPIMs visible to the current user in the active workspace.
+        https://docs.slack.dev/reference/block-kit/block-elements/multi-select-menu-element/#conversation_multi_select
+
+        Args:
+            placeholder (required): A plain_text only text object that defines the placeholder text shown on the menu.
+                Maximum length for the text in this field is 150 characters.
+            action_id (required): An identifier for the action triggered when a menu option is selected.
+                You can use this when you receive an interaction payload to identify the source of the action.
+                Should be unique among all other action_ids in the containing block.
+                Maximum length for this field is 255 characters.
+            initial_conversations: An array of one or more IDs of any valid conversations to be pre-selected
+                when the menu loads. If default_to_current_conversation is also supplied,
+                initial_conversations will be ignored.
+            confirm: A confirm object that defines an optional confirmation dialog that appears
+                before the multi-select choices are submitted.
+            max_selected_items: Specifies the maximum number of items that can be selected in the menu.
+                Minimum number is 1.
+            default_to_current_conversation: Pre-populates the select menu with the conversation that
+                the user was viewing when they opened the modal, if available. Default is false.
+            filter: A filter object that reduces the list of available conversations using the specified criteria.
+            focus_on_load: Indicates whether the element will be set to auto focus within the view object.
+                Only one element can be set to true. Defaults to false.
+        """
+        super().__init__(
+            type=self.type,
+            action_id=action_id,
+            placeholder=TextObject.parse(placeholder, PlainTextObject.type),  # type: ignore[arg-type]
+            confirm=ConfirmObject.parse(confirm),  # type: ignore[arg-type]
+            focus_on_load=focus_on_load,
+        )
+        show_unknown_key_warning(self, others)
+
+        self.initial_conversations = initial_conversations
+        self.max_selected_items = max_selected_items
+        self.default_to_current_conversation = default_to_current_conversation
+        self.filter = ConversationFilter.parse(filter)  # type: ignore[arg-type]
+
+

Block Elements are things that exists inside of your Blocks. +https://docs.slack.dev/reference/block-kit/block-elements/

+

This multi-select menu will populate its options with a list of public and private channels, +DMs, and MPIMs visible to the current user in the active workspace. +https://docs.slack.dev/reference/block-kit/block-elements/multi-select-menu-element/#conversation_multi_select

+

Args

+
+
placeholder : required
+
A plain_text only text object that defines the placeholder text shown on the menu. +Maximum length for the text in this field is 150 characters.
+
action_id : required
+
An identifier for the action triggered when a menu option is selected. +You can use this when you receive an interaction payload to identify the source of the action. +Should be unique among all other action_ids in the containing block. +Maximum length for this field is 255 characters.
+
initial_conversations
+
An array of one or more IDs of any valid conversations to be pre-selected +when the menu loads. If default_to_current_conversation is also supplied, +initial_conversations will be ignored.
+
confirm
+
A confirm object that defines an optional confirmation dialog that appears +before the multi-select choices are submitted.
+
max_selected_items
+
Specifies the maximum number of items that can be selected in the menu. +Minimum number is 1.
+
default_to_current_conversation
+
Pre-populates the select menu with the conversation that +the user was viewing when they opened the modal, if available. Default is false.
+
filter
+
A filter object that reduces the list of available conversations using the specified criteria.
+
focus_on_load
+
Indicates whether the element will be set to auto focus within the view object. +Only one element can be set to true. Defaults to false.
+
+

Ancestors

+ +

Class variables

+
+
var type
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
prop attributes : Set[str]
+
+
+ +Expand source code + +
@property
+def attributes(self) -> Set[str]:  # type: ignore[override]
+    return super().attributes.union(
+        {
+            "initial_conversations",
+            "max_selected_items",
+            "default_to_current_conversation",
+            "filter",
+        }
+    )
+
+

Build an unordered collection of unique elements.

+
+
+

Inherited members

+ +
+
+class ConversationSelectElement +(*,
placeholder: str | dict | TextObject | None = None,
action_id: str | None = None,
initial_conversation: str | None = None,
confirm: dict | ConfirmObject | None = None,
response_url_enabled: bool | None = None,
default_to_current_conversation: bool | None = None,
filter: ConversationFilter | None = None,
focus_on_load: bool | None = None,
**others: dict)
+
+
+
+ +Expand source code + +
class ConversationSelectElement(InputInteractiveElement):
+    type = "conversations_select"
+
+    @property
+    def attributes(self) -> Set[str]:  # type: ignore[override]
+        return super().attributes.union(
+            {
+                "initial_conversation",
+                "response_url_enabled",
+                "filter",
+                "default_to_current_conversation",
+            }
+        )
+
+    def __init__(
+        self,
+        *,
+        placeholder: Optional[Union[str, dict, TextObject]] = None,
+        action_id: Optional[str] = None,
+        initial_conversation: Optional[str] = None,
+        confirm: Optional[Union[dict, ConfirmObject]] = None,
+        response_url_enabled: Optional[bool] = None,
+        default_to_current_conversation: Optional[bool] = None,
+        filter: Optional[ConversationFilter] = None,
+        focus_on_load: Optional[bool] = None,
+        **others: dict,
+    ):
+        """
+        This select menu will populate its options with a list of public and private
+        channels, DMs, and MPIMs visible to the current user in the active workspace.
+        https://docs.slack.dev/reference/block-kit/block-elements/select-menu-element/#conversations_select
+
+        Args:
+            placeholder (required): A plain_text only text object that defines the placeholder text shown on the menu.
+                Maximum length for the text in this field is 150 characters.
+            action_id (required): An identifier for the action triggered when a menu option is selected.
+                You can use this when you receive an interaction payload to identify the source of the action.
+                Should be unique among all other action_ids in the containing block.
+                Maximum length for this field is 255 characters.
+            initial_conversation: The ID of any valid conversation to be pre-selected when the menu loads.
+                If default_to_current_conversation is also supplied, initial_conversation will take precedence.
+            confirm: A confirm object that defines an optional confirmation dialog
+                that appears after a menu item is selected.
+            response_url_enabled: This field only works with menus in input blocks in modals.
+                When set to true, the view_submission payload from the menu's parent view will contain a response_url.
+                This response_url can be used for message responses. The target conversation for the message
+                will be determined by the value of this select menu.
+            default_to_current_conversation: Pre-populates the select menu with the conversation
+                that the user was viewing when they opened the modal, if available. Default is false.
+            filter: A filter object that reduces the list of available conversations using the specified criteria.
+            focus_on_load: Indicates whether the element will be set to auto focus within the view object.
+                Only one element can be set to true. Defaults to false.
+        """
+        super().__init__(
+            type=self.type,
+            action_id=action_id,
+            placeholder=TextObject.parse(placeholder, PlainTextObject.type),  # type: ignore[arg-type]
+            confirm=ConfirmObject.parse(confirm),  # type: ignore[arg-type]
+            focus_on_load=focus_on_load,
+        )
+        show_unknown_key_warning(self, others)
+
+        self.initial_conversation = initial_conversation
+        self.response_url_enabled = response_url_enabled
+        self.default_to_current_conversation = default_to_current_conversation
+        self.filter = filter
+
+

Block Elements are things that exists inside of your Blocks. +https://docs.slack.dev/reference/block-kit/block-elements/

+

This select menu will populate its options with a list of public and private +channels, DMs, and MPIMs visible to the current user in the active workspace. +https://docs.slack.dev/reference/block-kit/block-elements/select-menu-element/#conversations_select

+

Args

+
+
placeholder : required
+
A plain_text only text object that defines the placeholder text shown on the menu. +Maximum length for the text in this field is 150 characters.
+
action_id : required
+
An identifier for the action triggered when a menu option is selected. +You can use this when you receive an interaction payload to identify the source of the action. +Should be unique among all other action_ids in the containing block. +Maximum length for this field is 255 characters.
+
initial_conversation
+
The ID of any valid conversation to be pre-selected when the menu loads. +If default_to_current_conversation is also supplied, initial_conversation will take precedence.
+
confirm
+
A confirm object that defines an optional confirmation dialog +that appears after a menu item is selected.
+
response_url_enabled
+
This field only works with menus in input blocks in modals. +When set to true, the view_submission payload from the menu's parent view will contain a response_url. +This response_url can be used for message responses. The target conversation for the message +will be determined by the value of this select menu.
+
default_to_current_conversation
+
Pre-populates the select menu with the conversation +that the user was viewing when they opened the modal, if available. Default is false.
+
filter
+
A filter object that reduces the list of available conversations using the specified criteria.
+
focus_on_load
+
Indicates whether the element will be set to auto focus within the view object. +Only one element can be set to true. Defaults to false.
+
+

Ancestors

+ +

Class variables

+
+
var type
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
prop attributes : Set[str]
+
+
+ +Expand source code + +
@property
+def attributes(self) -> Set[str]:  # type: ignore[override]
+    return super().attributes.union(
+        {
+            "initial_conversation",
+            "response_url_enabled",
+            "filter",
+            "default_to_current_conversation",
+        }
+    )
+
+

Build an unordered collection of unique elements.

+
+
+

Inherited members

+ +
+
+class DatePickerElement +(*,
action_id: str | None = None,
placeholder: str | dict | TextObject | None = None,
initial_date: str | None = None,
confirm: dict | ConfirmObject | None = None,
focus_on_load: bool | None = None,
**others: dict)
+
+
+
+ +Expand source code + +
class DatePickerElement(InputInteractiveElement):
+    type = "datepicker"
+
+    @property
+    def attributes(self) -> Set[str]:  # type: ignore[override]
+        return super().attributes.union({"initial_date"})
+
+    def __init__(
+        self,
+        *,
+        action_id: Optional[str] = None,
+        placeholder: Optional[Union[str, dict, TextObject]] = None,
+        initial_date: Optional[str] = None,
+        confirm: Optional[Union[dict, ConfirmObject]] = None,
+        focus_on_load: Optional[bool] = None,
+        **others: dict,
+    ):
+        """
+        An element which lets users easily select a date from a calendar style UI.
+        Date picker elements can be used inside of SectionBlocks and ActionsBlocks.
+        https://docs.slack.dev/reference/block-kit/block-elements/date-picker-element
+
+        Args:
+            action_id (required): An identifier for the action triggered when a menu option is selected.
+                You can use this when you receive an interaction payload to identify the source of the action.
+                Should be unique among all other action_ids in the containing block.
+                Maximum length for this field is 255 characters.
+            placeholder: A plain_text only text object that defines the placeholder text shown on the datepicker.
+                Maximum length for the text in this field is 150 characters.
+            initial_date: The initial date that is selected when the element is loaded.
+                This should be in the format YYYY-MM-DD.
+            confirm: A confirm object that defines an optional confirmation dialog
+                that appears after a date is selected.
+            focus_on_load: Indicates whether the element will be set to auto focus within the view object.
+                Only one element can be set to true. Defaults to false.
+        """
+        super().__init__(
+            type=self.type,
+            action_id=action_id,
+            placeholder=TextObject.parse(placeholder, PlainTextObject.type),  # type: ignore[arg-type]
+            confirm=ConfirmObject.parse(confirm),  # type: ignore[arg-type]
+            focus_on_load=focus_on_load,
+        )
+        show_unknown_key_warning(self, others)
+
+        self.initial_date = initial_date
+
+    @JsonValidator("initial_date attribute must be in format 'YYYY-MM-DD'")
+    def _validate_initial_date_valid(self) -> bool:
+        return (
+            self.initial_date is None
+            or re.match(r"\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])", self.initial_date) is not None
+        )
+
+

Block Elements are things that exists inside of your Blocks. +https://docs.slack.dev/reference/block-kit/block-elements/

+

An element which lets users easily select a date from a calendar style UI. +Date picker elements can be used inside of SectionBlocks and ActionsBlocks. +https://docs.slack.dev/reference/block-kit/block-elements/date-picker-element

+

Args

+
+
action_id : required
+
An identifier for the action triggered when a menu option is selected. +You can use this when you receive an interaction payload to identify the source of the action. +Should be unique among all other action_ids in the containing block. +Maximum length for this field is 255 characters.
+
placeholder
+
A plain_text only text object that defines the placeholder text shown on the datepicker. +Maximum length for the text in this field is 150 characters.
+
initial_date
+
The initial date that is selected when the element is loaded. +This should be in the format YYYY-MM-DD.
+
confirm
+
A confirm object that defines an optional confirmation dialog +that appears after a date is selected.
+
focus_on_load
+
Indicates whether the element will be set to auto focus within the view object. +Only one element can be set to true. Defaults to false.
+
+

Ancestors

+ +

Class variables

+
+
var type
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
prop attributes : Set[str]
+
+
+ +Expand source code + +
@property
+def attributes(self) -> Set[str]:  # type: ignore[override]
+    return super().attributes.union({"initial_date"})
+
+

Build an unordered collection of unique elements.

+
+
+

Inherited members

+ +
+
+class DateTimePickerElement +(*,
action_id: str | None = None,
initial_date_time: int | None = None,
confirm: dict | ConfirmObject | None = None,
focus_on_load: bool | None = None,
**others: dict)
+
+
+
+ +Expand source code + +
class DateTimePickerElement(InputInteractiveElement):
+    type = "datetimepicker"
+
+    @property
+    def attributes(self) -> Set[str]:  # type: ignore[override]
+        return super().attributes.union({"initial_date_time"})
+
+    def __init__(
+        self,
+        *,
+        action_id: Optional[str] = None,
+        initial_date_time: Optional[int] = None,
+        confirm: Optional[Union[dict, ConfirmObject]] = None,
+        focus_on_load: Optional[bool] = None,
+        **others: dict,
+    ):
+        """
+        An element that allows the selection of a time of day formatted as a UNIX timestamp.
+        On desktop clients, this time picker will take the form of a dropdown list and the
+        date picker will take the form of a dropdown calendar. Both options will have free-text
+        entry for precise choices. On mobile clients, the time picker and date
+        picker will use native UIs.
+        https://docs.slack.dev/reference/block-kit/block-elements/date-picker-element/
+
+        Args:
+            action_id (required): An identifier for the action triggered when a time is selected. You can use this
+                when you receive an interaction payload to identify the source of the action. Should be unique among
+                all other action_ids in the containing block. Maximum length for this field is 255 characters.
+            initial_date_time: The initial date and time that is selected when the element is loaded, represented as
+                a UNIX timestamp in seconds. This should be in the format of 10 digits, for example 1628633820
+                represents the date and time August 10th, 2021 at 03:17pm PST.
+                and mm is minutes with leading zeros (00 to 59), for example 22:25 for 10:25pm.
+            confirm: A confirm object that defines an optional confirmation dialog
+                that appears after a time is selected.
+            focus_on_load: Indicates whether the element will be set to auto focus within the view object.
+                Only one element can be set to true. Defaults to false.
+        """
+        super().__init__(
+            type=self.type,
+            action_id=action_id,
+            confirm=ConfirmObject.parse(confirm),  # type: ignore[arg-type]
+            focus_on_load=focus_on_load,
+        )
+        show_unknown_key_warning(self, others)
+
+        self.initial_date_time = initial_date_time
+
+    @JsonValidator("initial_date_time attribute must be between 0 and 99999999 seconds")
+    def _validate_initial_date_time_valid(self) -> bool:
+        return self.initial_date_time is None or (0 <= self.initial_date_time <= 9999999999)
+
+

Block Elements are things that exists inside of your Blocks. +https://docs.slack.dev/reference/block-kit/block-elements/

+

An element that allows the selection of a time of day formatted as a UNIX timestamp. +On desktop clients, this time picker will take the form of a dropdown list and the +date picker will take the form of a dropdown calendar. Both options will have free-text +entry for precise choices. On mobile clients, the time picker and date +picker will use native UIs. +https://docs.slack.dev/reference/block-kit/block-elements/date-picker-element/

+

Args

+
+
action_id : required
+
An identifier for the action triggered when a time is selected. You can use this +when you receive an interaction payload to identify the source of the action. Should be unique among +all other action_ids in the containing block. Maximum length for this field is 255 characters.
+
initial_date_time
+
The initial date and time that is selected when the element is loaded, represented as +a UNIX timestamp in seconds. This should be in the format of 10 digits, for example 1628633820 +represents the date and time August 10th, 2021 at 03:17pm PST. +and mm is minutes with leading zeros (00 to 59), for example 22:25 for 10:25pm.
+
confirm
+
A confirm object that defines an optional confirmation dialog +that appears after a time is selected.
+
focus_on_load
+
Indicates whether the element will be set to auto focus within the view object. +Only one element can be set to true. Defaults to false.
+
+

Ancestors

+ +

Class variables

+
+
var type
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
prop attributes : Set[str]
+
+
+ +Expand source code + +
@property
+def attributes(self) -> Set[str]:  # type: ignore[override]
+    return super().attributes.union({"initial_date_time"})
+
+

Build an unordered collection of unique elements.

+
+
+

Inherited members

+ +
+
+class EmailInputElement +(*,
action_id: str | None = None,
initial_value: str | None = None,
dispatch_action_config: dict | DispatchActionConfig | None = None,
focus_on_load: bool | None = None,
placeholder: str | dict | TextObject | None = None,
**others: dict)
+
+
+
+ +Expand source code + +
class EmailInputElement(InputInteractiveElement):
+    type = "email_text_input"
+
+    @property
+    def attributes(self) -> Set[str]:  # type: ignore[override]
+        return super().attributes.union(
+            {
+                "initial_value",
+                "dispatch_action_config",
+            }
+        )
+
+    def __init__(
+        self,
+        *,
+        action_id: Optional[str] = None,
+        initial_value: Optional[str] = None,
+        dispatch_action_config: Optional[Union[dict, DispatchActionConfig]] = None,
+        focus_on_load: Optional[bool] = None,
+        placeholder: Optional[Union[str, dict, TextObject]] = None,
+        **others: dict,
+    ):
+        """
+        https://docs.slack.dev/reference/block-kit/block-elements/email-input-element
+
+        Args:
+            action_id (required): An identifier for the input value when the parent modal is submitted.
+                You can use this when you receive a view_submission payload to identify the value of the input element.
+                Should be unique among all other action_ids in the containing block.
+                Maximum length for this field is 255 characters.
+            initial_value: The initial value in the email input when it is loaded.
+            dispatch_action_config:  dispatch configuration object that determines when during
+                text input the element returns a block_actions payload.
+            focus_on_load: Indicates whether the element will be set to auto focus within the view object.
+                Only one element can be set to true. Defaults to false.
+            placeholder: A plain_text only text object that defines the placeholder text shown in the
+                email input. Maximum length for the text in this field is 150 characters.
+        """
+        super().__init__(
+            type=self.type,
+            action_id=action_id,
+            placeholder=TextObject.parse(placeholder, PlainTextObject.type),  # type: ignore[arg-type]
+            focus_on_load=focus_on_load,
+        )
+        show_unknown_key_warning(self, others)
+
+        self.initial_value = initial_value
+        self.dispatch_action_config = dispatch_action_config
+
+

Block Elements are things that exists inside of your Blocks. +https://docs.slack.dev/reference/block-kit/block-elements/

+

https://docs.slack.dev/reference/block-kit/block-elements/email-input-element

+

Args

+
+
action_id : required
+
An identifier for the input value when the parent modal is submitted. +You can use this when you receive a view_submission payload to identify the value of the input element. +Should be unique among all other action_ids in the containing block. +Maximum length for this field is 255 characters.
+
initial_value
+
The initial value in the email input when it is loaded.
+
dispatch_action_config
+
dispatch configuration object that determines when during +text input the element returns a block_actions payload.
+
focus_on_load
+
Indicates whether the element will be set to auto focus within the view object. +Only one element can be set to true. Defaults to false.
+
placeholder
+
A plain_text only text object that defines the placeholder text shown in the +email input. Maximum length for the text in this field is 150 characters.
+
+

Ancestors

+ +

Class variables

+
+
var type
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
prop attributes : Set[str]
+
+
+ +Expand source code + +
@property
+def attributes(self) -> Set[str]:  # type: ignore[override]
+    return super().attributes.union(
+        {
+            "initial_value",
+            "dispatch_action_config",
+        }
+    )
+
+

Build an unordered collection of unique elements.

+
+
+

Inherited members

+ +
+
+class ExternalDataMultiSelectElement +(*,
placeholder: str | dict | TextObject | None = None,
action_id: str | None = None,
min_query_length: int | None = None,
initial_options: Sequence[dict | Option] | None = None,
confirm: dict | ConfirmObject | None = None,
max_selected_items: int | None = None,
focus_on_load: bool | None = None,
**others: dict)
+
+
+
+ +Expand source code + +
class ExternalDataMultiSelectElement(InputInteractiveElement):
+    type = "multi_external_select"
+
+    @property
+    def attributes(self) -> Set[str]:  # type: ignore[override]
+        return super().attributes.union({"min_query_length", "initial_options", "max_selected_items"})
+
+    def __init__(
+        self,
+        *,
+        placeholder: Optional[Union[str, dict, TextObject]] = None,
+        action_id: Optional[str] = None,
+        min_query_length: Optional[int] = None,
+        initial_options: Optional[Sequence[Union[dict, Option]]] = None,
+        confirm: Optional[Union[dict, ConfirmObject]] = None,
+        max_selected_items: Optional[int] = None,
+        focus_on_load: Optional[bool] = None,
+        **others: dict,
+    ):
+        """
+        This select menu will load its options from an external data source, allowing
+        for a dynamic list of options.
+        https://docs.slack.dev/reference/block-kit/block-elements/multi-select-menu-element#external_multi_select
+
+        Args:
+            placeholder (required): A plain_text only text object that defines the placeholder text shown on the menu.
+                Maximum length for the text in this field is 150 characters.
+            action_id (required): An identifier for the action triggered when a menu option is selected.
+                You can use this when you receive an interaction payload to identify the source of the action.
+                Should be unique among all other action_ids in the containing block.
+                Maximum length for this field is 255 characters.
+            min_query_length: When the typeahead field is used, a request will be sent on every character change.
+                If you prefer fewer requests or more fully ideated queries,
+                use the min_query_length attribute to tell Slack
+                the fewest number of typed characters required before dispatch.
+                The default value is 3
+            initial_options: An array of option objects that exactly match one or more of the options
+                within options or option_groups. These options will be selected when the menu initially loads.
+            confirm: A confirm object that defines an optional confirmation dialog that appears
+                before the multi-select choices are submitted.
+            max_selected_items: Specifies the maximum number of items that can be selected in the menu.
+                Minimum number is 1.
+            focus_on_load: Indicates whether the element will be set to auto focus within the view object.
+                Only one element can be set to true. Defaults to false.
+        """
+        super().__init__(
+            type=self.type,
+            action_id=action_id,
+            placeholder=TextObject.parse(placeholder, PlainTextObject.type),  # type: ignore[arg-type]
+            confirm=ConfirmObject.parse(confirm),  # type: ignore[arg-type]
+            focus_on_load=focus_on_load,
+        )
+        show_unknown_key_warning(self, others)
+
+        self.min_query_length = min_query_length
+        self.initial_options = Option.parse_all(initial_options)
+        self.max_selected_items = max_selected_items
+
+

Block Elements are things that exists inside of your Blocks. +https://docs.slack.dev/reference/block-kit/block-elements/

+

This select menu will load its options from an external data source, allowing +for a dynamic list of options. +https://docs.slack.dev/reference/block-kit/block-elements/multi-select-menu-element#external_multi_select

+

Args

+
+
placeholder : required
+
A plain_text only text object that defines the placeholder text shown on the menu. +Maximum length for the text in this field is 150 characters.
+
action_id : required
+
An identifier for the action triggered when a menu option is selected. +You can use this when you receive an interaction payload to identify the source of the action. +Should be unique among all other action_ids in the containing block. +Maximum length for this field is 255 characters.
+
min_query_length
+
When the typeahead field is used, a request will be sent on every character change. +If you prefer fewer requests or more fully ideated queries, +use the min_query_length attribute to tell Slack +the fewest number of typed characters required before dispatch. +The default value is 3
+
initial_options
+
An array of option objects that exactly match one or more of the options +within options or option_groups. These options will be selected when the menu initially loads.
+
confirm
+
A confirm object that defines an optional confirmation dialog that appears +before the multi-select choices are submitted.
+
max_selected_items
+
Specifies the maximum number of items that can be selected in the menu. +Minimum number is 1.
+
focus_on_load
+
Indicates whether the element will be set to auto focus within the view object. +Only one element can be set to true. Defaults to false.
+
+

Ancestors

+ +

Class variables

+
+
var type
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
prop attributes : Set[str]
+
+
+ +Expand source code + +
@property
+def attributes(self) -> Set[str]:  # type: ignore[override]
+    return super().attributes.union({"min_query_length", "initial_options", "max_selected_items"})
+
+

Build an unordered collection of unique elements.

+
+
+

Inherited members

+ +
+
+class ExternalDataSelectElement +(*,
action_id: str | None = None,
placeholder: str | TextObject | None = None,
initial_option: Option | OptionGroup | None = None,
min_query_length: int | None = None,
confirm: dict | ConfirmObject | None = None,
focus_on_load: bool | None = None,
**others: dict)
+
+
+
+ +Expand source code + +
class ExternalDataSelectElement(InputInteractiveElement):
+    type = "external_select"
+
+    @property
+    def attributes(self) -> Set[str]:  # type: ignore[override]
+        return super().attributes.union({"min_query_length", "initial_option"})
+
+    def __init__(
+        self,
+        *,
+        action_id: Optional[str] = None,
+        placeholder: Optional[Union[str, TextObject]] = None,
+        initial_option: Union[Optional[Option], Optional[OptionGroup]] = None,
+        min_query_length: Optional[int] = None,
+        confirm: Optional[Union[dict, ConfirmObject]] = None,
+        focus_on_load: Optional[bool] = None,
+        **others: dict,
+    ):
+        """
+        This select menu will load its options from an external data source, allowing
+        for a dynamic list of options.
+        https://docs.slack.dev/reference/block-kit/block-elements/select-menu-element#external_select
+
+        Args:
+            action_id (required): An identifier for the action triggered when a menu option is selected.
+                You can use this when you receive an interaction payload to identify the source of the action.
+                Should be unique among all other action_ids in the containing block.
+                Maximum length for this field is 255 characters.
+            placeholder (required): A plain_text only text object that defines the placeholder text shown on the menu.
+                Maximum length for the text in this field is 150 characters.
+            initial_option: A single option that exactly matches one of the options
+                within the options or option_groups loaded from the external data source.
+                This option will be selected when the menu initially loads.
+            min_query_length: When the typeahead field is used, a request will be sent on every character change.
+                If you prefer fewer requests or more fully ideated queries,
+                use the min_query_length attribute to tell Slack
+                the fewest number of typed characters required before dispatch.
+                The default value is 3.
+            confirm: A confirm object that defines an optional confirmation dialog
+                that appears after a menu item is selected.
+            focus_on_load: Indicates whether the element will be set to auto focus within the view object.
+                Only one element can be set to true. Defaults to false.
+        """
+        super().__init__(
+            type=self.type,
+            action_id=action_id,
+            placeholder=TextObject.parse(placeholder, PlainTextObject.type),  # type: ignore[arg-type]
+            confirm=ConfirmObject.parse(confirm),  # type: ignore[arg-type]
+            focus_on_load=focus_on_load,
+        )
+        show_unknown_key_warning(self, others)
+
+        self.min_query_length = min_query_length
+        self.initial_option = initial_option
+
+

Block Elements are things that exists inside of your Blocks. +https://docs.slack.dev/reference/block-kit/block-elements/

+

This select menu will load its options from an external data source, allowing +for a dynamic list of options. +https://docs.slack.dev/reference/block-kit/block-elements/select-menu-element#external_select

+

Args

+
+
action_id : required
+
An identifier for the action triggered when a menu option is selected. +You can use this when you receive an interaction payload to identify the source of the action. +Should be unique among all other action_ids in the containing block. +Maximum length for this field is 255 characters.
+
placeholder : required
+
A plain_text only text object that defines the placeholder text shown on the menu. +Maximum length for the text in this field is 150 characters.
+
initial_option
+
A single option that exactly matches one of the options +within the options or option_groups loaded from the external data source. +This option will be selected when the menu initially loads.
+
min_query_length
+
When the typeahead field is used, a request will be sent on every character change. +If you prefer fewer requests or more fully ideated queries, +use the min_query_length attribute to tell Slack +the fewest number of typed characters required before dispatch. +The default value is 3.
+
confirm
+
A confirm object that defines an optional confirmation dialog +that appears after a menu item is selected.
+
focus_on_load
+
Indicates whether the element will be set to auto focus within the view object. +Only one element can be set to true. Defaults to false.
+
+

Ancestors

+ +

Class variables

+
+
var type
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
prop attributes : Set[str]
+
+
+ +Expand source code + +
@property
+def attributes(self) -> Set[str]:  # type: ignore[override]
+    return super().attributes.union({"min_query_length", "initial_option"})
+
+

Build an unordered collection of unique elements.

+
+
+

Inherited members

+ +
+
+class FeedbackButtonsElement +(*,
action_id: str | None = None,
positive_button: dict | FeedbackButtonObject,
negative_button: dict | FeedbackButtonObject,
**others: dict)
+
+
+
+ +Expand source code + +
class FeedbackButtonsElement(InteractiveElement):
+    type = "feedback_buttons"
+
+    @property
+    def attributes(self) -> Set[str]:  # type: ignore[override]
+        return super().attributes.union({"positive_button", "negative_button"})
+
+    def __init__(
+        self,
+        *,
+        action_id: Optional[str] = None,
+        positive_button: Union[dict, FeedbackButtonObject],
+        negative_button: Union[dict, FeedbackButtonObject],
+        **others: dict,
+    ):
+        """Buttons to indicate positive or negative feedback.
+        https://docs.slack.dev/reference/block-kit/block-elements/feedback-buttons-element
+
+        Args:
+            action_id (required): An identifier for this action.
+                You can use this when you receive an interaction payload to identify the source of the action.
+                Should be unique among all other action_ids in the containing block.
+                Maximum length for this field is 255 characters.
+            positive_button (required): A button to indicate positive feedback.
+            negative_button (required): A button to indicate negative feedback.
+        """
+        super().__init__(action_id=action_id, type=self.type)
+        show_unknown_key_warning(self, others)
+
+        self.positive_button = FeedbackButtonObject.parse(positive_button)
+        self.negative_button = FeedbackButtonObject.parse(negative_button)
+
+

Block Elements are things that exists inside of your Blocks. +https://docs.slack.dev/reference/block-kit/block-elements/

+

Buttons to indicate positive or negative feedback. +https://docs.slack.dev/reference/block-kit/block-elements/feedback-buttons-element

+

Args

+
+
action_id : required
+
An identifier for this action. +You can use this when you receive an interaction payload to identify the source of the action. +Should be unique among all other action_ids in the containing block. +Maximum length for this field is 255 characters.
+
positive_button : required
+
A button to indicate positive feedback.
+
negative_button : required
+
A button to indicate negative feedback.
+
+

Ancestors

+ +

Class variables

+
+
var type
+
+

The type of the None singleton.

+
+
+

Inherited members

+ +
+
+class FileInputElement +(*,
action_id: str | None = None,
filetypes: List[str] | None = None,
max_files: int | None = None,
**others: dict)
+
+
+
+ +Expand source code + +
class FileInputElement(InputInteractiveElement):
+    type = "file_input"
+
+    @property
+    def attributes(self) -> Set[str]:  # type: ignore[override]
+        return super().attributes.union(
+            {
+                "filetypes",
+                "max_files",
+            }
+        )
+
+    def __init__(
+        self,
+        *,
+        action_id: Optional[str] = None,
+        filetypes: Optional[List[str]] = None,
+        max_files: Optional[int] = None,
+        **others: dict,
+    ):
+        """
+        https://docs.slack.dev/reference/block-kit/block-elements/file-input-element
+
+        Args:
+            action_id (required): An identifier for the input value when the parent modal is submitted.
+                You can use this when you receive a view_submission payload to identify the value of the input element.
+                Should be unique among all other action_ids in the containing block. Maximum length is 255 characters.
+            filetypes: An array of valid file extensions that will be accepted for this element.
+                All file extensions will be accepted if filetypes is not specified.
+                This validation is provided for convenience only,
+                and you should perform your own file type validation based on what you expect to receive.
+            max_files: Maximum number of files that can be uploaded for this file_input element.
+                Minimum of 1, maximum of 10. Defaults to 10 if not specified.
+        """
+        super().__init__(
+            type=self.type,
+            action_id=action_id,
+        )
+        show_unknown_key_warning(self, others)
+
+        self.filetypes = filetypes
+        self.max_files = max_files
+
+

Block Elements are things that exists inside of your Blocks. +https://docs.slack.dev/reference/block-kit/block-elements/

+

https://docs.slack.dev/reference/block-kit/block-elements/file-input-element

+

Args

+
+
action_id : required
+
An identifier for the input value when the parent modal is submitted. +You can use this when you receive a view_submission payload to identify the value of the input element. +Should be unique among all other action_ids in the containing block. Maximum length is 255 characters.
+
filetypes
+
An array of valid file extensions that will be accepted for this element. +All file extensions will be accepted if filetypes is not specified. +This validation is provided for convenience only, +and you should perform your own file type validation based on what you expect to receive.
+
max_files
+
Maximum number of files that can be uploaded for this file_input element. +Minimum of 1, maximum of 10. Defaults to 10 if not specified.
+
+

Ancestors

+ +

Class variables

+
+
var type
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
prop attributes : Set[str]
+
+
+ +Expand source code + +
@property
+def attributes(self) -> Set[str]:  # type: ignore[override]
+    return super().attributes.union(
+        {
+            "filetypes",
+            "max_files",
+        }
+    )
+
+

Build an unordered collection of unique elements.

+
+
+

Inherited members

+ +
+
+class IconButtonElement +(*,
action_id: str | None = None,
icon: str,
text: str | dict | TextObject,
accessibility_label: str | None = None,
value: str | None = None,
visible_to_user_ids: List[str] | None = None,
confirm: dict | ConfirmObject | None = None,
**others: dict)
+
+
+
+ +Expand source code + +
class IconButtonElement(InteractiveElement):
+    type = "icon_button"
+
+    @property
+    def attributes(self) -> Set[str]:  # type: ignore[override]
+        return super().attributes.union({"icon", "text", "accessibility_label", "value", "visible_to_user_ids", "confirm"})
+
+    def __init__(
+        self,
+        *,
+        action_id: Optional[str] = None,
+        icon: str,
+        text: Union[str, dict, TextObject],
+        accessibility_label: Optional[str] = None,
+        value: Optional[str] = None,
+        visible_to_user_ids: Optional[List[str]] = None,
+        confirm: Optional[Union[dict, ConfirmObject]] = None,
+        **others: dict,
+    ):
+        """An icon button to perform actions.
+        https://docs.slack.dev/reference/block-kit/block-elements/icon-button-element
+
+        Args:
+            action_id: An identifier for this action.
+                You can use this when you receive an interaction payload to identify the source of the action.
+                Should be unique among all other action_ids in the containing block.
+                Maximum length for this field is 255 characters.
+            icon (required): The icon to show (e.g., 'trash').
+            text (required): Defines an object containing some text.
+            accessibility_label: A label for longer descriptive text about a button element.
+                This label will be read out by screen readers instead of the button text object.
+                Maximum length for this field is 75 characters.
+            value: The button value.
+                Maximum length for this field is 2000 characters.
+            visible_to_user_ids: User IDs for which the icon appears.
+                Maximum length for this field is 10 user IDs.
+            confirm: A confirm object that defines an optional confirmation dialog after the button is clicked.
+        """
+        super().__init__(action_id=action_id, type=self.type)
+        show_unknown_key_warning(self, others)
+
+        self.icon = icon
+        self.text = TextObject.parse(text, PlainTextObject.type)
+        self.accessibility_label = accessibility_label
+        self.value = value
+        self.visible_to_user_ids = visible_to_user_ids
+        self.confirm = ConfirmObject.parse(confirm) if confirm else None
+
+

Block Elements are things that exists inside of your Blocks. +https://docs.slack.dev/reference/block-kit/block-elements/

+

An icon button to perform actions. +https://docs.slack.dev/reference/block-kit/block-elements/icon-button-element

+

Args

+
+
action_id
+
An identifier for this action. +You can use this when you receive an interaction payload to identify the source of the action. +Should be unique among all other action_ids in the containing block. +Maximum length for this field is 255 characters.
+
icon : required
+
The icon to show (e.g., 'trash').
+
text : required
+
Defines an object containing some text.
+
accessibility_label
+
A label for longer descriptive text about a button element. +This label will be read out by screen readers instead of the button text object. +Maximum length for this field is 75 characters.
+
value
+
The button value. +Maximum length for this field is 2000 characters.
+
visible_to_user_ids
+
User IDs for which the icon appears. +Maximum length for this field is 10 user IDs.
+
confirm
+
A confirm object that defines an optional confirmation dialog after the button is clicked.
+
+

Ancestors

+ +

Class variables

+
+
var type
+
+

The type of the None singleton.

+
+
+

Inherited members

+ +
+
+class ImageElement +(*,
alt_text: str | None = None,
image_url: str | None = None,
slack_file: Dict[str, Any] | SlackFile | None = None,
**others: dict)
+
+
+
+ +Expand source code + +
class ImageElement(BlockElement):
+    type = "image"
+    image_url_max_length = 3000
+    alt_text_max_length = 2000
+
+    @property
+    def attributes(self) -> Set[str]:  # type: ignore[override]
+        return super().attributes.union({"alt_text", "image_url", "slack_file"})
+
+    def __init__(
+        self,
+        *,
+        alt_text: Optional[str] = None,
+        image_url: Optional[str] = None,
+        slack_file: Optional[Union[Dict[str, Any], SlackFile]] = None,
+        **others: dict,
+    ):
+        """An element to insert an image - this element can be used in section and
+        context blocks only. If you want a block with only an image in it,
+        you're looking for the image block.
+        https://docs.slack.dev/reference/block-kit/block-elements/image-element
+
+        Args:
+            alt_text (required): A plain-text summary of the image. This should not contain any markup.
+            image_url: The URL of the image to be displayed.
+            slack_file: A Slack image file object that defines the source of the image.
+        """
+        super().__init__(type=self.type)
+        show_unknown_key_warning(self, others)
+
+        self.image_url = image_url
+        self.alt_text = alt_text
+        self.slack_file = slack_file if slack_file is None or isinstance(slack_file, SlackFile) else SlackFile(**slack_file)
+
+    @JsonValidator(f"image_url attribute cannot exceed {image_url_max_length} characters")
+    def _validate_image_url_length(self) -> bool:
+        return self.image_url is None or len(self.image_url) <= self.image_url_max_length
+
+    @JsonValidator(f"alt_text attribute cannot exceed {alt_text_max_length} characters")
+    def _validate_alt_text_length(self) -> bool:
+        return len(self.alt_text) <= self.alt_text_max_length  # type: ignore[arg-type]
+
+

Block Elements are things that exists inside of your Blocks. +https://docs.slack.dev/reference/block-kit/block-elements/

+

An element to insert an image - this element can be used in section and +context blocks only. If you want a block with only an image in it, +you're looking for the image block. +https://docs.slack.dev/reference/block-kit/block-elements/image-element

+

Args

+
+
alt_text : required
+
A plain-text summary of the image. This should not contain any markup.
+
image_url
+
The URL of the image to be displayed.
+
slack_file
+
A Slack image file object that defines the source of the image.
+
+

Ancestors

+ +

Class variables

+
+
var alt_text_max_length
+
+

The type of the None singleton.

+
+
var image_url_max_length
+
+

The type of the None singleton.

+
+
var type
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
prop attributes : Set[str]
+
+
+ +Expand source code + +
@property
+def attributes(self) -> Set[str]:  # type: ignore[override]
+    return super().attributes.union({"alt_text", "image_url", "slack_file"})
+
+

Build an unordered collection of unique elements.

+
+
+

Inherited members

+ +
+
+class InputInteractiveElement +(*,
action_id: str | None = None,
placeholder: str | TextObject | None = None,
type: str | None = None,
subtype: str | None = None,
confirm: dict | ConfirmObject | None = None,
focus_on_load: bool | None = None,
**others: dict)
+
+
+
+ +Expand source code + +
class InputInteractiveElement(InteractiveElement, metaclass=ABCMeta):
+    placeholder_max_length = 150
+
+    attributes = {"type", "action_id", "placeholder", "confirm", "focus_on_load"}
+
+    @property
+    def subtype(self) -> Optional[str]:
+        return self.type
+
+    def __init__(
+        self,
+        *,
+        action_id: Optional[str] = None,
+        placeholder: Optional[Union[str, TextObject]] = None,
+        type: Optional[str] = None,
+        subtype: Optional[str] = None,
+        confirm: Optional[Union[dict, ConfirmObject]] = None,
+        focus_on_load: Optional[bool] = None,
+        **others: dict,
+    ):
+        """InteractiveElement that is usable in input blocks
+
+        We generally recommend using the concrete subclasses for better supports of available properties.
+        """
+        if subtype:
+            self._subtype_warning()
+        super().__init__(action_id=action_id, type=type or subtype)
+
+        # Note that we don't intentionally have show_unknown_key_warning for the unknown key warnings here.
+        # It's fine to pass any kwargs to the held dict here although the class does not do any validation.
+        # show_unknown_key_warning(self, others)
+
+        self.placeholder = TextObject.parse(placeholder)  # type: ignore[arg-type]
+        self.confirm = ConfirmObject.parse(confirm)  # type: ignore[arg-type]
+        self.focus_on_load = focus_on_load
+
+    @JsonValidator(f"placeholder attribute cannot exceed {placeholder_max_length} characters")
+    def _validate_placeholder_length(self) -> bool:
+        return (
+            self.placeholder is None
+            or self.placeholder.text is None
+            or len(self.placeholder.text) <= self.placeholder_max_length
+        )
+
+

Block Elements are things that exists inside of your Blocks. +https://docs.slack.dev/reference/block-kit/block-elements/

+

InteractiveElement that is usable in input blocks

+

We generally recommend using the concrete subclasses for better supports of available properties.

+

Ancestors

+ +

Subclasses

+ +

Class variables

+
+
var attributes
+
+

The type of the None singleton.

+
+
var placeholder_max_length
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
prop subtype : str | None
+
+
+ +Expand source code + +
@property
+def subtype(self) -> Optional[str]:
+    return self.type
+
+
+
+
+

Inherited members

+ +
+
+class InteractiveElement +(*,
action_id: str | None = None,
type: str | None = None,
subtype: str | None = None,
**others: dict)
+
+
+
+ +Expand source code + +
class InteractiveElement(BlockElement):
+    action_id_max_length = 255
+
+    @property
+    def attributes(self) -> Set[str]:  # type: ignore[override]
+        return super().attributes.union({"alt_text", "action_id"})
+
+    def __init__(
+        self,
+        *,
+        action_id: Optional[str] = None,
+        type: Optional[str] = None,
+        subtype: Optional[str] = None,
+        **others: dict,
+    ):
+        """An interactive block element.
+
+        We generally recommend using the concrete subclasses for better supports of available properties.
+        """
+        if subtype:
+            self._subtype_warning()
+        super().__init__(type=type or subtype)
+
+        # Note that we don't intentionally have show_unknown_key_warning for the unknown key warnings here.
+        # It's fine to pass any kwargs to the held dict here although the class does not do any validation.
+        # show_unknown_key_warning(self, others)
+
+        self.action_id = action_id
+
+    @JsonValidator(f"action_id attribute cannot exceed {action_id_max_length} characters")
+    def _validate_action_id_length(self) -> bool:
+        return self.action_id is None or len(self.action_id) <= self.action_id_max_length
+
+

Block Elements are things that exists inside of your Blocks. +https://docs.slack.dev/reference/block-kit/block-elements/

+

An interactive block element.

+

We generally recommend using the concrete subclasses for better supports of available properties.

+

Ancestors

+ +

Subclasses

+ +

Class variables

+
+
var action_id_max_length
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
prop attributes : Set[str]
+
+
+ +Expand source code + +
@property
+def attributes(self) -> Set[str]:  # type: ignore[override]
+    return super().attributes.union({"alt_text", "action_id"})
+
+

Build an unordered collection of unique elements.

+
+
+

Inherited members

+ +
+
+class LinkButtonElement +(*,
text: str | dict | PlainTextObject,
url: str,
action_id: str | None = None,
style: str | None = None,
**others: dict)
+
+
+
+ +Expand source code + +
class LinkButtonElement(ButtonElement):
+    def __init__(
+        self,
+        *,
+        text: Union[str, dict, PlainTextObject],
+        url: str,
+        action_id: Optional[str] = None,
+        style: Optional[str] = None,
+        **others: dict,
+    ):
+        """A simple button that simply opens a given URL. You will still receive an
+        interaction payload and will need to send an acknowledgement response.
+        This is a helper class that makes creating links simpler.
+        https://docs.slack.dev/reference/block-kit/block-elements/button-element/
+
+        Args:
+            text (required): A text object that defines the button's text.
+                Can only be of type: plain_text.
+                Maximum length for the text in this field is 75 characters.
+            url (required): A URL to load in the user's browser when the button is clicked.
+                Maximum length for this field is 3000 characters.
+                If you're using url, you'll still receive an interaction payload
+                and will need to send an acknowledgement response.
+            action_id (required): An identifier for this action.
+                You can use this when you receive an interaction payload to identify the source of the action.
+                Should be unique among all other action_ids in the containing block.
+                Maximum length for this field is 255 characters.
+            style: Decorates buttons with alternative visual color schemes. Use this option with restraint.
+                "primary" gives buttons a green outline and text, ideal for affirmation or confirmation actions.
+                "primary" should only be used for one button within a set.
+                "danger" gives buttons a red outline and text, and should be used when the action is destructive.
+                Use "danger" even more sparingly than "primary".
+                If you don't include this field, the default button style will be used.
+        """
+        super().__init__(
+            # NOTE: value must be always absent
+            text=text,
+            url=url,
+            action_id=action_id,
+            value=None,
+            style=style,
+        )
+        show_unknown_key_warning(self, others)
+
+

Block Elements are things that exists inside of your Blocks. +https://docs.slack.dev/reference/block-kit/block-elements/

+

A simple button that simply opens a given URL. You will still receive an +interaction payload and will need to send an acknowledgement response. +This is a helper class that makes creating links simpler. +https://docs.slack.dev/reference/block-kit/block-elements/button-element/

+

Args

+
+
text : required
+
A text object that defines the button's text. +Can only be of type: plain_text. +Maximum length for the text in this field is 75 characters.
+
url : required
+
A URL to load in the user's browser when the button is clicked. +Maximum length for this field is 3000 characters. +If you're using url, you'll still receive an interaction payload +and will need to send an acknowledgement response.
+
action_id : required
+
An identifier for this action. +You can use this when you receive an interaction payload to identify the source of the action. +Should be unique among all other action_ids in the containing block. +Maximum length for this field is 255 characters.
+
style
+
Decorates buttons with alternative visual color schemes. Use this option with restraint. +"primary" gives buttons a green outline and text, ideal for affirmation or confirmation actions. +"primary" should only be used for one button within a set. +"danger" gives buttons a red outline and text, and should be used when the action is destructive. +Use "danger" even more sparingly than "primary". +If you don't include this field, the default button style will be used.
+
+

Ancestors

+ +

Inherited members

+ +
+
+class NumberInputElement +(*,
action_id: str | None = None,
is_decimal_allowed: bool | None = False,
initial_value: int | float | str | None = None,
min_value: int | float | str | None = None,
max_value: int | float | str | None = None,
dispatch_action_config: dict | DispatchActionConfig | None = None,
focus_on_load: bool | None = None,
placeholder: str | dict | TextObject | None = None,
**others: dict)
+
+
+
+ +Expand source code + +
class NumberInputElement(InputInteractiveElement):
+    type = "number_input"
+
+    @property
+    def attributes(self) -> Set[str]:  # type: ignore[override]
+        return super().attributes.union(
+            {
+                "initial_value",
+                "is_decimal_allowed",
+                "min_value",
+                "max_value",
+                "dispatch_action_config",
+            }
+        )
+
+    def __init__(
+        self,
+        *,
+        action_id: Optional[str] = None,
+        is_decimal_allowed: Optional[bool] = False,
+        initial_value: Optional[Union[int, float, str]] = None,
+        min_value: Optional[Union[int, float, str]] = None,
+        max_value: Optional[Union[int, float, str]] = None,
+        dispatch_action_config: Optional[Union[dict, DispatchActionConfig]] = None,
+        focus_on_load: Optional[bool] = None,
+        placeholder: Optional[Union[str, dict, TextObject]] = None,
+        **others: dict,
+    ):
+        """
+        https://docs.slack.dev/reference/block-kit/block-elements/number-input-element/
+
+        Args:
+            action_id (required): An identifier for the input value when the parent modal is submitted.
+                You can use this when you receive a view_submission payload to identify the value of the input element.
+                Should be unique among all other action_ids in the containing block.
+                Maximum length for this field is 255 characters.
+            is_decimal_allowed (required): Decimal numbers are allowed if is_decimal_allowed= true, set the value to
+                false otherwise.
+            initial_value: The initial value in the number input when it is loaded.
+            min_value: The minimum value, cannot be greater than max_value.
+            max_value: The maximum value, cannot be less than min_value.
+            dispatch_action_config: A dispatch configuration object that determines when
+                during text input the element returns a block_actions payload.
+            focus_on_load: Indicates whether the element will be set to auto focus within the view object.
+                Only one element can be set to true. Defaults to false.
+            placeholder: A plain_text only text object that defines the placeholder text shown
+                in the plain-text input. Maximum length for the text in this field is 150 characters.
+        """
+        super().__init__(
+            type=self.type,
+            action_id=action_id,
+            placeholder=TextObject.parse(placeholder, PlainTextObject.type),  # type: ignore[arg-type]
+            focus_on_load=focus_on_load,
+        )
+        show_unknown_key_warning(self, others)
+
+        self.initial_value = str(initial_value) if initial_value is not None else None
+        self.is_decimal_allowed = is_decimal_allowed
+        self.min_value = str(min_value) if min_value is not None else None
+        self.max_value = str(max_value) if max_value is not None else None
+        self.dispatch_action_config = dispatch_action_config
+
+

Block Elements are things that exists inside of your Blocks. +https://docs.slack.dev/reference/block-kit/block-elements/

+

https://docs.slack.dev/reference/block-kit/block-elements/number-input-element/

+

Args

+
+
action_id : required
+
An identifier for the input value when the parent modal is submitted. +You can use this when you receive a view_submission payload to identify the value of the input element. +Should be unique among all other action_ids in the containing block. +Maximum length for this field is 255 characters.
+
is_decimal_allowed : required
+
Decimal numbers are allowed if is_decimal_allowed= true, set the value to +false otherwise.
+
initial_value
+
The initial value in the number input when it is loaded.
+
min_value
+
The minimum value, cannot be greater than max_value.
+
max_value
+
The maximum value, cannot be less than min_value.
+
dispatch_action_config
+
A dispatch configuration object that determines when +during text input the element returns a block_actions payload.
+
focus_on_load
+
Indicates whether the element will be set to auto focus within the view object. +Only one element can be set to true. Defaults to false.
+
placeholder
+
A plain_text only text object that defines the placeholder text shown +in the plain-text input. Maximum length for the text in this field is 150 characters.
+
+

Ancestors

+ +

Class variables

+
+
var type
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
prop attributes : Set[str]
+
+
+ +Expand source code + +
@property
+def attributes(self) -> Set[str]:  # type: ignore[override]
+    return super().attributes.union(
+        {
+            "initial_value",
+            "is_decimal_allowed",
+            "min_value",
+            "max_value",
+            "dispatch_action_config",
+        }
+    )
+
+

Build an unordered collection of unique elements.

+
+
+

Inherited members

+ +
+
+class OverflowMenuElement +(*,
action_id: str | None = None,
options: Sequence[Option],
confirm: dict | ConfirmObject | None = None,
**others: dict)
+
+
+
+ +Expand source code + +
class OverflowMenuElement(InteractiveElement):
+    type = "overflow"
+    options_min_length = 1
+    options_max_length = 5
+
+    @property
+    def attributes(self) -> Set[str]:  # type: ignore[override]
+        return super().attributes.union({"confirm", "options"})
+
+    def __init__(
+        self,
+        *,
+        action_id: Optional[str] = None,
+        options: Sequence[Option],
+        confirm: Optional[Union[dict, ConfirmObject]] = None,
+        **others: dict,
+    ):
+        """
+        This is like a cross between a button and a select menu - when a user clicks
+        on this overflow button, they will be presented with a list of options to
+        choose from. Unlike the select menu, there is no typeahead field, and the
+        button always appears with an ellipsis ("…") rather than customisable text.
+
+        As such, it is usually used if you want a more compact layout than a select
+        menu, or to supply a list of less visually important actions after a row of
+        buttons. You can also specify simple URL links as overflow menu options,
+        instead of actions.
+
+        https://docs.slack.dev/reference/block-kit/block-elements/overflow-menu-element
+
+        Args:
+            action_id (required): An identifier for the action triggered when a menu option is selected.
+                You can use this when you receive an interaction payload to identify the source of the action.
+                Should be unique among all other action_ids in the containing block.
+                Maximum length for this field is 255 characters.
+            options (required): An array of option objects to display in the menu.
+                Maximum number of options is 5, minimum is 1.
+            confirm: A confirm object that defines an optional confirmation dialog that appears
+                after a menu item is selected.
+        """
+        super().__init__(action_id=action_id, type=self.type)
+        show_unknown_key_warning(self, others)
+
+        self.options = options
+        self.confirm = ConfirmObject.parse(confirm)  # type: ignore[arg-type]
+
+    @JsonValidator(f"options attribute must have between {options_min_length} " f"and {options_max_length} items")
+    def _validate_options_length(self) -> bool:
+        return self.options_min_length <= len(self.options) <= self.options_max_length
+
+

Block Elements are things that exists inside of your Blocks. +https://docs.slack.dev/reference/block-kit/block-elements/

+

This is like a cross between a button and a select menu - when a user clicks +on this overflow button, they will be presented with a list of options to +choose from. Unlike the select menu, there is no typeahead field, and the +button always appears with an ellipsis ("…") rather than customisable text.

+

As such, it is usually used if you want a more compact layout than a select +menu, or to supply a list of less visually important actions after a row of +buttons. You can also specify simple URL links as overflow menu options, +instead of actions.

+

https://docs.slack.dev/reference/block-kit/block-elements/overflow-menu-element

+

Args

+
+
action_id : required
+
An identifier for the action triggered when a menu option is selected. +You can use this when you receive an interaction payload to identify the source of the action. +Should be unique among all other action_ids in the containing block. +Maximum length for this field is 255 characters.
+
options : required
+
An array of option objects to display in the menu. +Maximum number of options is 5, minimum is 1.
+
confirm
+
A confirm object that defines an optional confirmation dialog that appears +after a menu item is selected.
+
+

Ancestors

+ +

Class variables

+
+
var options_max_length
+
+

The type of the None singleton.

+
+
var options_min_length
+
+

The type of the None singleton.

+
+
var type
+
+

The type of the None singleton.

+
+
+

Inherited members

+ +
+
+class PlainTextInputElement +(*,
action_id: str | None = None,
placeholder: str | dict | TextObject | None = None,
initial_value: str | None = None,
multiline: bool | None = None,
min_length: int | None = None,
max_length: int | None = None,
dispatch_action_config: dict | DispatchActionConfig | None = None,
focus_on_load: bool | None = None,
**others: dict)
+
+
+
+ +Expand source code + +
class PlainTextInputElement(InputInteractiveElement):
+    type = "plain_text_input"
+
+    @property
+    def attributes(self) -> Set[str]:  # type: ignore[override]
+        return super().attributes.union(
+            {
+                "initial_value",
+                "multiline",
+                "min_length",
+                "max_length",
+                "dispatch_action_config",
+            }
+        )
+
+    def __init__(
+        self,
+        *,
+        action_id: Optional[str] = None,
+        placeholder: Optional[Union[str, dict, TextObject]] = None,
+        initial_value: Optional[str] = None,
+        multiline: Optional[bool] = None,
+        min_length: Optional[int] = None,
+        max_length: Optional[int] = None,
+        dispatch_action_config: Optional[Union[dict, DispatchActionConfig]] = None,
+        focus_on_load: Optional[bool] = None,
+        **others: dict,
+    ):
+        """
+        A plain-text input, similar to the HTML <input> tag, creates a field
+        where a user can enter freeform data. It can appear as a single-line
+        field or a larger textarea using the multiline flag. Plain-text input
+        elements can be used inside of SectionBlocks and ActionsBlocks.
+        https://docs.slack.dev/reference/block-kit/block-elements/plain-text-input-element
+
+        Args:
+            action_id (required): An identifier for the input value when the parent modal is submitted.
+                You can use this when you receive a view_submission payload to identify the value of the input element.
+                Should be unique among all other action_ids in the containing block.
+                Maximum length for this field is 255 characters.
+            placeholder: A plain_text only text object that defines the placeholder text shown
+                in the plain-text input. Maximum length for the text in this field is 150 characters.
+            initial_value: The initial value in the plain-text input when it is loaded.
+            multiline: Indicates whether the input will be a single line (false) or a larger textarea (true).
+                Defaults to false.
+            min_length: The minimum length of input that the user must provide. If the user provides less,
+                they will receive an error. Maximum value is 3000.
+            max_length: The maximum length of input that the user can provide. If the user provides more,
+                they will receive an error.
+            dispatch_action_config: A dispatch configuration object that determines when
+                during text input the element returns a block_actions payload.
+            focus_on_load: Indicates whether the element will be set to auto focus within the view object.
+                Only one element can be set to true. Defaults to false.
+        """
+        super().__init__(
+            type=self.type,
+            action_id=action_id,
+            placeholder=TextObject.parse(placeholder, PlainTextObject.type),  # type: ignore[arg-type]
+            focus_on_load=focus_on_load,
+        )
+        show_unknown_key_warning(self, others)
+
+        self.initial_value = initial_value
+        self.multiline = multiline
+        self.min_length = min_length
+        self.max_length = max_length
+        self.dispatch_action_config = dispatch_action_config
+
+

Block Elements are things that exists inside of your Blocks. +https://docs.slack.dev/reference/block-kit/block-elements/

+

A plain-text input, similar to the HTML tag, creates a field +where a user can enter freeform data. It can appear as a single-line +field or a larger textarea using the multiline flag. Plain-text input +elements can be used inside of SectionBlocks and ActionsBlocks. +https://docs.slack.dev/reference/block-kit/block-elements/plain-text-input-element

+

Args

+
+
action_id : required
+
An identifier for the input value when the parent modal is submitted. +You can use this when you receive a view_submission payload to identify the value of the input element. +Should be unique among all other action_ids in the containing block. +Maximum length for this field is 255 characters.
+
placeholder
+
A plain_text only text object that defines the placeholder text shown +in the plain-text input. Maximum length for the text in this field is 150 characters.
+
initial_value
+
The initial value in the plain-text input when it is loaded.
+
multiline
+
Indicates whether the input will be a single line (false) or a larger textarea (true). +Defaults to false.
+
min_length
+
The minimum length of input that the user must provide. If the user provides less, +they will receive an error. Maximum value is 3000.
+
max_length
+
The maximum length of input that the user can provide. If the user provides more, +they will receive an error.
+
dispatch_action_config
+
A dispatch configuration object that determines when +during text input the element returns a block_actions payload.
+
focus_on_load
+
Indicates whether the element will be set to auto focus within the view object. +Only one element can be set to true. Defaults to false.
+
+

Ancestors

+ +

Class variables

+
+
var type
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
prop attributes : Set[str]
+
+
+ +Expand source code + +
@property
+def attributes(self) -> Set[str]:  # type: ignore[override]
+    return super().attributes.union(
+        {
+            "initial_value",
+            "multiline",
+            "min_length",
+            "max_length",
+            "dispatch_action_config",
+        }
+    )
+
+

Build an unordered collection of unique elements.

+
+
+

Inherited members

+ +
+
+class RadioButtonsElement +(*,
action_id: str | None = None,
options: Sequence[dict | Option] | None = None,
initial_option: dict | Option | None = None,
confirm: dict | ConfirmObject | None = None,
focus_on_load: bool | None = None,
**others: dict)
+
+
+
+ +Expand source code + +
class RadioButtonsElement(InputInteractiveElement):
+    type = "radio_buttons"
+
+    @property
+    def attributes(self) -> Set[str]:  # type: ignore[override]
+        return super().attributes.union({"options", "initial_option"})
+
+    def __init__(
+        self,
+        *,
+        action_id: Optional[str] = None,
+        options: Optional[Sequence[Union[dict, Option]]] = None,
+        initial_option: Optional[Union[dict, Option]] = None,
+        confirm: Optional[Union[dict, ConfirmObject]] = None,
+        focus_on_load: Optional[bool] = None,
+        **others: dict,
+    ):
+        """A radio button group that allows a user to choose one item from a list of possible options.
+        https://docs.slack.dev/reference/block-kit/block-elements/radio-button-group-element
+
+        Args:
+            action_id (required): An identifier for the action triggered when the radio button group is changed.
+                You can use this when you receive an interaction payload to identify the source of the action.
+                Should be unique among all other action_ids in the containing block.
+                Maximum length for this field is 255 characters.
+            options (required): An array of option objects. A maximum of 10 options are allowed.
+            initial_option: An option object that exactly matches one of the options.
+                This option will be selected when the radio button group initially loads.
+            confirm: A confirm object that defines an optional confirmation dialog that appears
+                after clicking one of the radio buttons in this element.
+            focus_on_load: Indicates whether the element will be set to auto focus within the view object.
+                Only one element can be set to true. Defaults to false.
+        """
+        super().__init__(
+            type=self.type,
+            action_id=action_id,
+            confirm=ConfirmObject.parse(confirm),  # type: ignore[arg-type]
+            focus_on_load=focus_on_load,
+        )
+        show_unknown_key_warning(self, others)
+
+        self.options = options
+        self.initial_option = initial_option
+
+

Block Elements are things that exists inside of your Blocks. +https://docs.slack.dev/reference/block-kit/block-elements/

+

A radio button group that allows a user to choose one item from a list of possible options. +https://docs.slack.dev/reference/block-kit/block-elements/radio-button-group-element

+

Args

+
+
action_id : required
+
An identifier for the action triggered when the radio button group is changed. +You can use this when you receive an interaction payload to identify the source of the action. +Should be unique among all other action_ids in the containing block. +Maximum length for this field is 255 characters.
+
options : required
+
An array of option objects. A maximum of 10 options are allowed.
+
initial_option
+
An option object that exactly matches one of the options. +This option will be selected when the radio button group initially loads.
+
confirm
+
A confirm object that defines an optional confirmation dialog that appears +after clicking one of the radio buttons in this element.
+
focus_on_load
+
Indicates whether the element will be set to auto focus within the view object. +Only one element can be set to true. Defaults to false.
+
+

Ancestors

+ +

Class variables

+
+
var type
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
prop attributes : Set[str]
+
+
+ +Expand source code + +
@property
+def attributes(self) -> Set[str]:  # type: ignore[override]
+    return super().attributes.union({"options", "initial_option"})
+
+

Build an unordered collection of unique elements.

+
+
+

Inherited members

+ +
+
+class RichTextElement +(*, type: str | None = None, subtype: str | None = None, **others: dict) +
+
+
+ +Expand source code + +
class RichTextElement(BlockElement):
+    pass
+
+

Block Elements are things that exists inside of your Blocks. +https://docs.slack.dev/reference/block-kit/block-elements/

+

Ancestors

+ +

Subclasses

+ +

Inherited members

+ +
+
+class RichTextElementParts +
+
+
+ +Expand source code + +
class RichTextElementParts:
+    class TextStyle:
+        def __init__(
+            self,
+            *,
+            bold: Optional[bool] = None,
+            italic: Optional[bool] = None,
+            strike: Optional[bool] = None,
+            code: Optional[bool] = None,
+            underline: Optional[bool] = None,
+        ):
+            self.bold = bold
+            self.italic = italic
+            self.strike = strike
+            self.code = code
+            self.underline = underline
+
+        def to_dict(self, *args) -> dict:
+            result = {
+                "bold": self.bold,
+                "italic": self.italic,
+                "strike": self.strike,
+                "code": self.code,
+                "underline": self.underline,
+            }
+            return {k: v for k, v in result.items() if v is not None}
+
+    class Text(RichTextElement):
+        type = "text"
+
+        @property
+        def attributes(self) -> Set[str]:  # type: ignore[override]
+            return super().attributes.union({"text", "style"})
+
+        def __init__(
+            self,
+            *,
+            text: str,
+            style: Optional[Union[dict, "RichTextElementParts.TextStyle"]] = None,
+            **others: dict,
+        ):
+            super().__init__(type=self.type)
+            show_unknown_key_warning(self, others)
+            self.text = text
+            self.style = style
+
+    class Channel(RichTextElement):
+        type = "channel"
+
+        @property
+        def attributes(self) -> Set[str]:  # type: ignore[override]
+            return super().attributes.union({"channel_id", "style"})
+
+        def __init__(
+            self,
+            *,
+            channel_id: str,
+            style: Optional[Union[dict, "RichTextElementParts.TextStyle"]] = None,
+            **others: dict,
+        ):
+            super().__init__(type=self.type)
+            show_unknown_key_warning(self, others)
+            self.channel_id = channel_id
+            self.style = style
+
+    class User(RichTextElement):
+        type = "user"
+
+        @property
+        def attributes(self) -> Set[str]:  # type: ignore[override]
+            return super().attributes.union({"user_id", "style"})
+
+        def __init__(
+            self,
+            *,
+            user_id: str,
+            style: Optional[Union[dict, "RichTextElementParts.TextStyle"]] = None,
+            **others: dict,
+        ):
+            super().__init__(type=self.type)
+            show_unknown_key_warning(self, others)
+            self.user_id = user_id
+            self.style = style
+
+    class Emoji(RichTextElement):
+        type = "emoji"
+
+        @property
+        def attributes(self) -> Set[str]:  # type: ignore[override]
+            return super().attributes.union({"name", "skin_tone", "unicode", "style"})
+
+        def __init__(
+            self,
+            *,
+            name: str,
+            skin_tone: Optional[int] = None,
+            unicode: Optional[str] = None,
+            style: Optional[Union[dict, "RichTextElementParts.TextStyle"]] = None,
+            **others: dict,
+        ):
+            super().__init__(type=self.type)
+            show_unknown_key_warning(self, others)
+            self.name = name
+            self.skin_tone = skin_tone
+            self.unicode = unicode
+            self.style = style
+
+    class Link(RichTextElement):
+        type = "link"
+
+        @property
+        def attributes(self) -> Set[str]:  # type: ignore[override]
+            return super().attributes.union({"url", "text", "style"})
+
+        def __init__(
+            self,
+            *,
+            url: str,
+            text: Optional[str] = None,
+            style: Optional[Union[dict, "RichTextElementParts.TextStyle"]] = None,
+            **others: dict,
+        ):
+            super().__init__(type=self.type)
+            show_unknown_key_warning(self, others)
+            self.url = url
+            self.text = text
+            self.style = style
+
+    class Team(RichTextElement):
+        type = "team"
+
+        @property
+        def attributes(self) -> Set[str]:  # type: ignore[override]
+            return super().attributes.union({"team_id", "style"})
+
+        def __init__(
+            self,
+            *,
+            team_id: str,
+            style: Optional[Union[dict, "RichTextElementParts.TextStyle"]] = None,
+            **others: dict,
+        ):
+            super().__init__(type=self.type)
+            show_unknown_key_warning(self, others)
+            self.team_id = team_id
+            self.style = style
+
+    class UserGroup(RichTextElement):
+        type = "usergroup"
+
+        @property
+        def attributes(self) -> Set[str]:  # type: ignore[override]
+            return super().attributes.union({"usergroup_id", "style"})
+
+        def __init__(
+            self,
+            *,
+            usergroup_id: str,
+            style: Optional[Union[dict, "RichTextElementParts.TextStyle"]] = None,
+            **others: dict,
+        ):
+            super().__init__(type=self.type)
+            show_unknown_key_warning(self, others)
+            self.usergroup_id = usergroup_id
+            self.style = style
+
+    class Date(RichTextElement):
+        type = "date"
+
+        @property
+        def attributes(self) -> Set[str]:  # type: ignore[override]
+            return super().attributes.union({"timestamp", "format", "url", "fallback"})
+
+        def __init__(
+            self,
+            *,
+            timestamp: int,
+            format: str,
+            url: Optional[str] = None,
+            fallback: Optional[str] = None,
+            **others: dict,
+        ):
+            super().__init__(type=self.type)
+            show_unknown_key_warning(self, others)
+            self.timestamp = timestamp
+            self.format = format
+            self.url = url
+            self.fallback = fallback
+
+    class Broadcast(RichTextElement):
+        type = "broadcast"
+
+        @property
+        def attributes(self) -> Set[str]:  # type: ignore[override]
+            return super().attributes.union({"range"})
+
+        def __init__(
+            self,
+            *,
+            range: str,  # channel, here, ..
+            **others: dict,
+        ):
+            super().__init__(type=self.type)
+            show_unknown_key_warning(self, others)
+            self.range = range
+
+    class Color(RichTextElement):
+        type = "color"
+
+        @property
+        def attributes(self) -> Set[str]:  # type: ignore[override]
+            return super().attributes.union({"value"})
+
+        def __init__(
+            self,
+            *,
+            value: str,
+            **others: dict,
+        ):
+            super().__init__(type=self.type)
+            show_unknown_key_warning(self, others)
+            self.value = value
+
+
+

Class variables

+
+
var Broadcast
+
+

Block Elements are things that exists inside of your Blocks. +https://docs.slack.dev/reference/block-kit/block-elements/

+
+
var Channel
+
+

Block Elements are things that exists inside of your Blocks. +https://docs.slack.dev/reference/block-kit/block-elements/

+
+
var Color
+
+

Block Elements are things that exists inside of your Blocks. +https://docs.slack.dev/reference/block-kit/block-elements/

+
+
var Date
+
+

Block Elements are things that exists inside of your Blocks. +https://docs.slack.dev/reference/block-kit/block-elements/

+
+
var Emoji
+
+

Block Elements are things that exists inside of your Blocks. +https://docs.slack.dev/reference/block-kit/block-elements/

+
+ +
+

Block Elements are things that exists inside of your Blocks. +https://docs.slack.dev/reference/block-kit/block-elements/

+
+
var Team
+
+

Block Elements are things that exists inside of your Blocks. +https://docs.slack.dev/reference/block-kit/block-elements/

+
+
var Text
+
+

Block Elements are things that exists inside of your Blocks. +https://docs.slack.dev/reference/block-kit/block-elements/

+
+
var TextStyle
+
+

The type of the None singleton.

+
+
var User
+
+

Block Elements are things that exists inside of your Blocks. +https://docs.slack.dev/reference/block-kit/block-elements/

+
+
var UserGroup
+
+

Block Elements are things that exists inside of your Blocks. +https://docs.slack.dev/reference/block-kit/block-elements/

+
+
+
+
+class RichTextInputElement +(*,
action_id: str | None = None,
placeholder: str | dict | TextObject | None = None,
initial_value: Dict[str, Any] | ForwardRef('RichTextBlock') | None = None,
dispatch_action_config: dict | DispatchActionConfig | None = None,
focus_on_load: bool | None = None,
**others: dict)
+
+
+
+ +Expand source code + +
class RichTextInputElement(InputInteractiveElement):
+    type = "rich_text_input"
+
+    @property
+    def attributes(self) -> Set[str]:  # type: ignore[override]
+        return super().attributes.union(
+            {
+                "initial_value",
+                "dispatch_action_config",
+            }
+        )
+
+    def __init__(
+        self,
+        *,
+        action_id: Optional[str] = None,
+        placeholder: Optional[Union[str, dict, TextObject]] = None,
+        # To avoid circular imports, the RichTextBlock type here is intentionally a string
+        initial_value: Optional[Union[Dict[str, Any], "RichTextBlock"]] = None,  # type: ignore[name-defined] # noqa: F821
+        dispatch_action_config: Optional[Union[dict, DispatchActionConfig]] = None,
+        focus_on_load: Optional[bool] = None,
+        **others: dict,
+    ):
+        super().__init__(
+            type=self.type,
+            action_id=action_id,
+            placeholder=TextObject.parse(placeholder, PlainTextObject.type),  # type: ignore[arg-type]
+            focus_on_load=focus_on_load,
+        )
+        show_unknown_key_warning(self, others)
+
+        self.initial_value = initial_value
+        self.dispatch_action_config = dispatch_action_config
+
+

Block Elements are things that exists inside of your Blocks. +https://docs.slack.dev/reference/block-kit/block-elements/

+

InteractiveElement that is usable in input blocks

+

We generally recommend using the concrete subclasses for better supports of available properties.

+

Ancestors

+ +

Class variables

+
+
var type
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
prop attributes : Set[str]
+
+
+ +Expand source code + +
@property
+def attributes(self) -> Set[str]:  # type: ignore[override]
+    return super().attributes.union(
+        {
+            "initial_value",
+            "dispatch_action_config",
+        }
+    )
+
+

Build an unordered collection of unique elements.

+
+
+

Inherited members

+ +
+
+class RichTextListElement +(*,
elements: Sequence[dict | RichTextElement],
style: str | None = None,
indent: int | None = None,
offset: int | None = None,
border: int | None = None,
**others: dict)
+
+
+
+ +Expand source code + +
class RichTextListElement(RichTextElement):
+    type = "rich_text_list"
+
+    @property
+    def attributes(self) -> Set[str]:  # type: ignore[override]
+        return super().attributes.union({"elements", "style", "indent", "offset", "border"})
+
+    def __init__(
+        self,
+        *,
+        elements: Sequence[Union[dict, RichTextElement]],
+        style: Optional[str] = None,  # bullet, ordered
+        indent: Optional[int] = None,
+        offset: Optional[int] = None,
+        border: Optional[int] = None,
+        **others: dict,
+    ):
+        super().__init__(type=self.type)
+        show_unknown_key_warning(self, others)
+        self.elements = BlockElement.parse_all(elements)
+        self.style = style
+        self.indent = indent
+        self.offset = offset
+        self.border = border
+
+

Block Elements are things that exists inside of your Blocks. +https://docs.slack.dev/reference/block-kit/block-elements/

+

Ancestors

+ +

Class variables

+
+
var type
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
prop attributes : Set[str]
+
+
+ +Expand source code + +
@property
+def attributes(self) -> Set[str]:  # type: ignore[override]
+    return super().attributes.union({"elements", "style", "indent", "offset", "border"})
+
+

Build an unordered collection of unique elements.

+
+
+

Inherited members

+ +
+
+class RichTextPreformattedElement +(*,
elements: Sequence[dict | RichTextElement],
border: int | None = None,
**others: dict)
+
+
+
+ +Expand source code + +
class RichTextPreformattedElement(RichTextElement):
+    type = "rich_text_preformatted"
+
+    @property
+    def attributes(self) -> Set[str]:  # type: ignore[override]
+        return super().attributes.union({"elements", "border"})
+
+    def __init__(
+        self,
+        *,
+        elements: Sequence[Union[dict, RichTextElement]],
+        border: Optional[int] = None,
+        **others: dict,
+    ):
+        super().__init__(type=self.type)
+        show_unknown_key_warning(self, others)
+        self.elements = BlockElement.parse_all(elements)
+        self.border = border
+
+

Block Elements are things that exists inside of your Blocks. +https://docs.slack.dev/reference/block-kit/block-elements/

+

Ancestors

+ +

Class variables

+
+
var type
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
prop attributes : Set[str]
+
+
+ +Expand source code + +
@property
+def attributes(self) -> Set[str]:  # type: ignore[override]
+    return super().attributes.union({"elements", "border"})
+
+

Build an unordered collection of unique elements.

+
+
+

Inherited members

+ +
+
+class RichTextQuoteElement +(*,
elements: Sequence[dict | RichTextElement],
**others: dict)
+
+
+
+ +Expand source code + +
class RichTextQuoteElement(RichTextElement):
+    type = "rich_text_quote"
+
+    @property
+    def attributes(self) -> Set[str]:  # type: ignore[override]
+        return super().attributes.union({"elements"})
+
+    def __init__(
+        self,
+        *,
+        elements: Sequence[Union[dict, RichTextElement]],
+        **others: dict,
+    ):
+        super().__init__(type=self.type)
+        show_unknown_key_warning(self, others)
+        self.elements = BlockElement.parse_all(elements)
+
+

Block Elements are things that exists inside of your Blocks. +https://docs.slack.dev/reference/block-kit/block-elements/

+

Ancestors

+ +

Class variables

+
+
var type
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
prop attributes : Set[str]
+
+
+ +Expand source code + +
@property
+def attributes(self) -> Set[str]:  # type: ignore[override]
+    return super().attributes.union({"elements"})
+
+

Build an unordered collection of unique elements.

+
+
+

Inherited members

+ +
+
+class RichTextSectionElement +(*,
elements: Sequence[dict | RichTextElement],
**others: dict)
+
+
+
+ +Expand source code + +
class RichTextSectionElement(RichTextElement):
+    type = "rich_text_section"
+
+    @property
+    def attributes(self) -> Set[str]:  # type: ignore[override]
+        return super().attributes.union({"elements"})
+
+    def __init__(
+        self,
+        *,
+        elements: Sequence[Union[dict, RichTextElement]],
+        **others: dict,
+    ):
+        super().__init__(type=self.type)
+        show_unknown_key_warning(self, others)
+        self.elements = BlockElement.parse_all(elements)
+
+

Block Elements are things that exists inside of your Blocks. +https://docs.slack.dev/reference/block-kit/block-elements/

+

Ancestors

+ +

Class variables

+
+
var type
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
prop attributes : Set[str]
+
+
+ +Expand source code + +
@property
+def attributes(self) -> Set[str]:  # type: ignore[override]
+    return super().attributes.union({"elements"})
+
+

Build an unordered collection of unique elements.

+
+
+

Inherited members

+ +
+
+class SelectElement +(*,
action_id: str | None = None,
placeholder: str | None = None,
options: Sequence[Option] | None = None,
option_groups: Sequence[OptionGroup] | None = None,
initial_option: Option | None = None,
confirm: dict | ConfirmObject | None = None,
focus_on_load: bool | None = None,
**others: dict)
+
+
+
+ +Expand source code + +
class SelectElement(InputInteractiveElement):
+    type = "static_select"
+    options_max_length = 100
+    option_groups_max_length = 100
+
+    @property
+    def attributes(self) -> Set[str]:  # type: ignore[override]
+        return super().attributes.union({"options", "option_groups", "initial_option"})
+
+    def __init__(
+        self,
+        *,
+        action_id: Optional[str] = None,
+        placeholder: Optional[str] = None,
+        options: Optional[Sequence[Option]] = None,
+        option_groups: Optional[Sequence[OptionGroup]] = None,
+        initial_option: Optional[Option] = None,
+        confirm: Optional[Union[dict, ConfirmObject]] = None,
+        focus_on_load: Optional[bool] = None,
+        **others: dict,
+    ):
+        """This is the simplest form of select menu, with a static list of options passed in when defining the element.
+        https://docs.slack.dev/reference/block-kit/block-elements/select-menu-element#static_select
+
+        Args:
+            action_id (required): An identifier for the action triggered when a menu option is selected.
+                You can use this when you receive an interaction payload to identify the source of the action.
+                Should be unique among all other action_ids in the containing block.
+                Maximum length for this field is 255 characters.
+            placeholder (required): A plain_text only text object that defines the placeholder text shown on the menu.
+                Maximum length for the text in this field is 150 characters.
+            options (either options or option_groups is required): An array of option objects.
+                Maximum number of options is 100.
+                If option_groups is specified, this field should not be.
+            option_groups (either options or option_groups is required): An array of option group objects.
+                Maximum number of option groups is 100.
+                If options is specified, this field should not be.
+            initial_option: A single option that exactly matches one of the options or option_groups.
+                This option will be selected when the menu initially loads.
+            confirm: A confirm object that defines an optional confirmation dialog
+                that appears after a menu item is selected.
+            focus_on_load: Indicates whether the element will be set to auto focus within the view object.
+                Only one element can be set to true. Defaults to false.
+        """
+        super().__init__(
+            type=self.type,
+            action_id=action_id,
+            placeholder=TextObject.parse(placeholder, PlainTextObject.type),  # type: ignore[arg-type]
+            confirm=ConfirmObject.parse(confirm),  # type: ignore[arg-type]
+            focus_on_load=focus_on_load,
+        )
+        show_unknown_key_warning(self, others)
+
+        self.options = options
+        self.option_groups = option_groups
+        self.initial_option = initial_option
+
+    @JsonValidator(f"options attribute cannot exceed {options_max_length} elements")
+    def _validate_options_length(self) -> bool:
+        return self.options is None or len(self.options) <= self.options_max_length
+
+    @JsonValidator(f"option_groups attribute cannot exceed {option_groups_max_length} elements")
+    def _validate_option_groups_length(self) -> bool:
+        return self.option_groups is None or len(self.option_groups) <= self.option_groups_max_length
+
+    @JsonValidator("options and option_groups cannot both be specified")
+    def _validate_options_and_option_groups_both_specified(self) -> bool:
+        return not (self.options is not None and self.option_groups is not None)
+
+    @JsonValidator("options or option_groups must be specified")
+    def _validate_neither_options_or_option_groups_is_specified(self) -> bool:
+        return self.options is not None or self.option_groups is not None
+
+

Block Elements are things that exists inside of your Blocks. +https://docs.slack.dev/reference/block-kit/block-elements/

+

This is the simplest form of select menu, with a static list of options passed in when defining the element. +https://docs.slack.dev/reference/block-kit/block-elements/select-menu-element#static_select

+

Args

+
+
action_id : required
+
An identifier for the action triggered when a menu option is selected. +You can use this when you receive an interaction payload to identify the source of the action. +Should be unique among all other action_ids in the containing block. +Maximum length for this field is 255 characters.
+
placeholder : required
+
A plain_text only text object that defines the placeholder text shown on the menu. +Maximum length for the text in this field is 150 characters.
+
options : either options or option_groups is required
+
An array of option objects. +Maximum number of options is 100. +If option_groups is specified, this field should not be.
+
option_groups : either options or option_groups is required
+
An array of option group objects. +Maximum number of option groups is 100. +If options is specified, this field should not be.
+
initial_option
+
A single option that exactly matches one of the options or option_groups. +This option will be selected when the menu initially loads.
+
confirm
+
A confirm object that defines an optional confirmation dialog +that appears after a menu item is selected.
+
focus_on_load
+
Indicates whether the element will be set to auto focus within the view object. +Only one element can be set to true. Defaults to false.
+
+

Ancestors

+ +

Class variables

+
+
var option_groups_max_length
+
+

The type of the None singleton.

+
+
var options_max_length
+
+

The type of the None singleton.

+
+
var type
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
prop attributes : Set[str]
+
+
+ +Expand source code + +
@property
+def attributes(self) -> Set[str]:  # type: ignore[override]
+    return super().attributes.union({"options", "option_groups", "initial_option"})
+
+

Build an unordered collection of unique elements.

+
+
+

Inherited members

+ +
+
+class StaticMultiSelectElement +(*,
placeholder: str | dict | TextObject | None = None,
action_id: str | None = None,
options: Sequence[Option] | None = None,
option_groups: Sequence[OptionGroup] | None = None,
initial_options: Sequence[Option] | None = None,
confirm: dict | ConfirmObject | None = None,
max_selected_items: int | None = None,
focus_on_load: bool | None = None,
**others: dict)
+
+
+
+ +Expand source code + +
class StaticMultiSelectElement(InputInteractiveElement):
+    type = "multi_static_select"
+    options_max_length = 100
+    option_groups_max_length = 100
+
+    @property
+    def attributes(self) -> Set[str]:  # type: ignore[override]
+        return super().attributes.union({"options", "option_groups", "initial_options", "max_selected_items"})
+
+    def __init__(
+        self,
+        *,
+        placeholder: Optional[Union[str, dict, TextObject]] = None,
+        action_id: Optional[str] = None,
+        options: Optional[Sequence[Option]] = None,
+        option_groups: Optional[Sequence[OptionGroup]] = None,
+        initial_options: Optional[Sequence[Option]] = None,
+        confirm: Optional[Union[dict, ConfirmObject]] = None,
+        max_selected_items: Optional[int] = None,
+        focus_on_load: Optional[bool] = None,
+        **others: dict,
+    ):
+        """
+        This is the simplest form of select menu, with a static list of options passed in when defining the element.
+        https://docs.slack.dev/reference/block-kit/block-elements/multi-select-menu-element#static_multi_select
+
+        Args:
+            placeholder (required): A plain_text only text object that defines the placeholder text shown on the menu.
+                Maximum length for the text in this field is 150 characters.
+            action_id (required): An identifier for the action triggered when a menu option is selected.
+                You can use this when you receive an interaction payload to identify the source of the action.
+                Should be unique among all other action_ids in the containing block.
+                Maximum length for this field is 255 characters.
+            options (either options or option_groups is required): An array of option objects.
+                Maximum number of options is 100.
+                If option_groups is specified, this field should not be.
+            option_groups (either options or option_groups is required): An array of option group objects.
+                Maximum number of option groups is 100.
+                If options is specified, this field should not be.
+            initial_options: An array of option objects that exactly match one or more of the options
+                within options or option_groups. These options will be selected when the menu initially loads.
+            confirm: A confirm object that defines an optional confirmation dialog
+                that appears before the multi-select choices are submitted.
+            max_selected_items: Specifies the maximum number of items that can be selected in the menu.
+                Minimum number is 1.
+            focus_on_load: Indicates whether the element will be set to auto focus within the view object.
+                Only one element can be set to true. Defaults to false.
+        """
+        super().__init__(
+            type=self.type,
+            action_id=action_id,
+            placeholder=TextObject.parse(placeholder, PlainTextObject.type),  # type: ignore[arg-type]
+            confirm=ConfirmObject.parse(confirm),  # type: ignore[arg-type]
+            focus_on_load=focus_on_load,
+        )
+        show_unknown_key_warning(self, others)
+
+        self.options = Option.parse_all(options)
+        self.option_groups = OptionGroup.parse_all(option_groups)
+        self.initial_options = Option.parse_all(initial_options)
+        self.max_selected_items = max_selected_items
+
+    @JsonValidator(f"options attribute cannot exceed {options_max_length} elements")
+    def _validate_options_length(self) -> bool:
+        return self.options is None or len(self.options) <= self.options_max_length
+
+    @JsonValidator(f"option_groups attribute cannot exceed {option_groups_max_length} elements")
+    def _validate_option_groups_length(self) -> bool:
+        return self.option_groups is None or len(self.option_groups) <= self.option_groups_max_length
+
+    @JsonValidator("options and option_groups cannot both be specified")
+    def _validate_options_and_option_groups_both_specified(self) -> bool:
+        return self.options is None or self.option_groups is None
+
+    @JsonValidator("options or option_groups must be specified")
+    def _validate_neither_options_or_option_groups_is_specified(self) -> bool:
+        return self.options is not None or self.option_groups is not None
+
+

Block Elements are things that exists inside of your Blocks. +https://docs.slack.dev/reference/block-kit/block-elements/

+

This is the simplest form of select menu, with a static list of options passed in when defining the element. +https://docs.slack.dev/reference/block-kit/block-elements/multi-select-menu-element#static_multi_select

+

Args

+
+
placeholder : required
+
A plain_text only text object that defines the placeholder text shown on the menu. +Maximum length for the text in this field is 150 characters.
+
action_id : required
+
An identifier for the action triggered when a menu option is selected. +You can use this when you receive an interaction payload to identify the source of the action. +Should be unique among all other action_ids in the containing block. +Maximum length for this field is 255 characters.
+
options : either options or option_groups is required
+
An array of option objects. +Maximum number of options is 100. +If option_groups is specified, this field should not be.
+
option_groups : either options or option_groups is required
+
An array of option group objects. +Maximum number of option groups is 100. +If options is specified, this field should not be.
+
initial_options
+
An array of option objects that exactly match one or more of the options +within options or option_groups. These options will be selected when the menu initially loads.
+
confirm
+
A confirm object that defines an optional confirmation dialog +that appears before the multi-select choices are submitted.
+
max_selected_items
+
Specifies the maximum number of items that can be selected in the menu. +Minimum number is 1.
+
focus_on_load
+
Indicates whether the element will be set to auto focus within the view object. +Only one element can be set to true. Defaults to false.
+
+

Ancestors

+ +

Class variables

+
+
var option_groups_max_length
+
+

The type of the None singleton.

+
+
var options_max_length
+
+

The type of the None singleton.

+
+
var type
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
prop attributes : Set[str]
+
+
+ +Expand source code + +
@property
+def attributes(self) -> Set[str]:  # type: ignore[override]
+    return super().attributes.union({"options", "option_groups", "initial_options", "max_selected_items"})
+
+

Build an unordered collection of unique elements.

+
+
+

Inherited members

+ +
+
+class StaticSelectElement +(*,
placeholder: str | dict | TextObject | None = None,
action_id: str | None = None,
options: Sequence[dict | Option] | None = None,
option_groups: Sequence[dict | OptionGroup] | None = None,
initial_option: dict | Option | None = None,
confirm: dict | ConfirmObject | None = None,
focus_on_load: bool | None = None,
**others: dict)
+
+
+
+ +Expand source code + +
class StaticSelectElement(InputInteractiveElement):
+    type = "static_select"
+    options_max_length = 100
+    option_groups_max_length = 100
+
+    @property
+    def attributes(self) -> Set[str]:  # type: ignore[override]
+        return super().attributes.union({"options", "option_groups", "initial_option"})
+
+    def __init__(
+        self,
+        *,
+        placeholder: Optional[Union[str, dict, TextObject]] = None,
+        action_id: Optional[str] = None,
+        options: Optional[Sequence[Union[dict, Option]]] = None,
+        option_groups: Optional[Sequence[Union[dict, OptionGroup]]] = None,
+        initial_option: Optional[Union[dict, Option]] = None,
+        confirm: Optional[Union[dict, ConfirmObject]] = None,
+        focus_on_load: Optional[bool] = None,
+        **others: dict,
+    ):
+        """This is the simplest form of select menu, with a static list of options passed in when defining the element.
+        https://docs.slack.dev/reference/block-kit/block-elements/select-menu-element#static_select
+
+        Args:
+            placeholder (required): A plain_text only text object that defines the placeholder text shown on the menu.
+                Maximum length for the text in this field is 150 characters.
+            action_id (required): An identifier for the action triggered when a menu option is selected.
+                You can use this when you receive an interaction payload to identify the source of the action.
+                Should be unique among all other action_ids in the containing block.
+                Maximum length for this field is 255 characters.
+            options (either options or option_groups is required): An array of option objects.
+                Maximum number of options is 100.
+                If option_groups is specified, this field should not be.
+            option_groups (either options or option_groups is required): An array of option group objects.
+                Maximum number of option groups is 100.
+                If options is specified, this field should not be.
+            initial_option: A single option that exactly matches one of the options or option_groups.
+                This option will be selected when the menu initially loads.
+            confirm: A confirm object that defines an optional confirmation dialog
+                that appears after a menu item is selected.
+            focus_on_load: Indicates whether the element will be set to auto focus within the view object.
+                Only one element can be set to true. Defaults to false.
+        """
+        super().__init__(
+            type=self.type,
+            action_id=action_id,
+            placeholder=TextObject.parse(placeholder, PlainTextObject.type),  # type: ignore[arg-type]
+            confirm=ConfirmObject.parse(confirm),  # type: ignore[arg-type]
+            focus_on_load=focus_on_load,
+        )
+        show_unknown_key_warning(self, others)
+
+        self.options = options
+        self.option_groups = option_groups
+        self.initial_option = initial_option
+
+    @JsonValidator(f"options attribute cannot exceed {options_max_length} elements")
+    def _validate_options_length(self) -> bool:
+        return self.options is None or len(self.options) <= self.options_max_length
+
+    @JsonValidator(f"option_groups attribute cannot exceed {option_groups_max_length} elements")
+    def _validate_option_groups_length(self) -> bool:
+        return self.option_groups is None or len(self.option_groups) <= self.option_groups_max_length
+
+    @JsonValidator("options and option_groups cannot both be specified")
+    def _validate_options_and_option_groups_both_specified(self) -> bool:
+        return not (self.options is not None and self.option_groups is not None)
+
+    @JsonValidator("options or option_groups must be specified")
+    def _validate_neither_options_or_option_groups_is_specified(self) -> bool:
+        return self.options is not None or self.option_groups is not None
+
+

Block Elements are things that exists inside of your Blocks. +https://docs.slack.dev/reference/block-kit/block-elements/

+

This is the simplest form of select menu, with a static list of options passed in when defining the element. +https://docs.slack.dev/reference/block-kit/block-elements/select-menu-element#static_select

+

Args

+
+
placeholder : required
+
A plain_text only text object that defines the placeholder text shown on the menu. +Maximum length for the text in this field is 150 characters.
+
action_id : required
+
An identifier for the action triggered when a menu option is selected. +You can use this when you receive an interaction payload to identify the source of the action. +Should be unique among all other action_ids in the containing block. +Maximum length for this field is 255 characters.
+
options : either options or option_groups is required
+
An array of option objects. +Maximum number of options is 100. +If option_groups is specified, this field should not be.
+
option_groups : either options or option_groups is required
+
An array of option group objects. +Maximum number of option groups is 100. +If options is specified, this field should not be.
+
initial_option
+
A single option that exactly matches one of the options or option_groups. +This option will be selected when the menu initially loads.
+
confirm
+
A confirm object that defines an optional confirmation dialog +that appears after a menu item is selected.
+
focus_on_load
+
Indicates whether the element will be set to auto focus within the view object. +Only one element can be set to true. Defaults to false.
+
+

Ancestors

+ +

Class variables

+
+
var option_groups_max_length
+
+

The type of the None singleton.

+
+
var options_max_length
+
+

The type of the None singleton.

+
+
var type
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
prop attributes : Set[str]
+
+
+ +Expand source code + +
@property
+def attributes(self) -> Set[str]:  # type: ignore[override]
+    return super().attributes.union({"options", "option_groups", "initial_option"})
+
+

Build an unordered collection of unique elements.

+
+
+

Inherited members

+ +
+
+class TimePickerElement +(*,
action_id: str | None = None,
placeholder: str | dict | TextObject | None = None,
initial_time: str | None = None,
confirm: dict | ConfirmObject | None = None,
focus_on_load: bool | None = None,
timezone: str | None = None,
**others: dict)
+
+
+
+ +Expand source code + +
class TimePickerElement(InputInteractiveElement):
+    type = "timepicker"
+
+    @property
+    def attributes(self) -> Set[str]:  # type: ignore[override]
+        return super().attributes.union({"initial_time", "timezone"})
+
+    def __init__(
+        self,
+        *,
+        action_id: Optional[str] = None,
+        placeholder: Optional[Union[str, dict, TextObject]] = None,
+        initial_time: Optional[str] = None,
+        confirm: Optional[Union[dict, ConfirmObject]] = None,
+        focus_on_load: Optional[bool] = None,
+        timezone: Optional[str] = None,
+        **others: dict,
+    ):
+        """
+        An element which allows selection of a time of day.
+        On desktop clients, this time picker will take the form of a dropdown list
+        with free-text entry for precise choices.
+        On mobile clients, the time picker will use native time picker UIs.
+        https://docs.slack.dev/reference/block-kit/block-elements/time-picker-element
+
+        Args:
+            action_id (required): An identifier for the action triggered when a time is selected.
+                You can use this when you receive an interaction payload to identify the source of the action.
+                Should be unique among all other action_ids in the containing block.
+                Maximum length for this field is 255 characters.
+            placeholder: A plain_text only text object that defines the placeholder text shown on the timepicker.
+                Maximum length for the text in this field is 150 characters.
+            initial_time: The initial time that is selected when the element is loaded.
+                This should be in the format HH:mm, where HH is the 24-hour format of an hour (00 to 23)
+                and mm is minutes with leading zeros (00 to 59), for example 22:25 for 10:25pm.
+            confirm: A confirm object that defines an optional confirmation dialog
+                that appears after a time is selected.
+            focus_on_load: Indicates whether the element will be set to auto focus within the view object.
+                Only one element can be set to true. Defaults to false.
+            timezone: The timezone to consider for this input value.
+        """
+        super().__init__(
+            type=self.type,
+            action_id=action_id,
+            placeholder=TextObject.parse(placeholder, PlainTextObject.type),  # type: ignore[arg-type]
+            confirm=ConfirmObject.parse(confirm),  # type: ignore[arg-type]
+            focus_on_load=focus_on_load,
+        )
+        show_unknown_key_warning(self, others)
+
+        self.initial_time = initial_time
+        self.timezone = timezone
+
+    @JsonValidator("initial_time attribute must be in format 'HH:mm'")
+    def _validate_initial_time_valid(self) -> bool:
+        return self.initial_time is None or re.match(r"([0-1][0-9]|2[0-3]):([0-5][0-9])", self.initial_time) is not None
+
+

Block Elements are things that exists inside of your Blocks. +https://docs.slack.dev/reference/block-kit/block-elements/

+

An element which allows selection of a time of day. +On desktop clients, this time picker will take the form of a dropdown list +with free-text entry for precise choices. +On mobile clients, the time picker will use native time picker UIs. +https://docs.slack.dev/reference/block-kit/block-elements/time-picker-element

+

Args

+
+
action_id : required
+
An identifier for the action triggered when a time is selected. +You can use this when you receive an interaction payload to identify the source of the action. +Should be unique among all other action_ids in the containing block. +Maximum length for this field is 255 characters.
+
placeholder
+
A plain_text only text object that defines the placeholder text shown on the timepicker. +Maximum length for the text in this field is 150 characters.
+
initial_time
+
The initial time that is selected when the element is loaded. +This should be in the format HH:mm, where HH is the 24-hour format of an hour (00 to 23) +and mm is minutes with leading zeros (00 to 59), for example 22:25 for 10:25pm.
+
confirm
+
A confirm object that defines an optional confirmation dialog +that appears after a time is selected.
+
focus_on_load
+
Indicates whether the element will be set to auto focus within the view object. +Only one element can be set to true. Defaults to false.
+
timezone
+
The timezone to consider for this input value.
+
+

Ancestors

+ +

Class variables

+
+
var type
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
prop attributes : Set[str]
+
+
+ +Expand source code + +
@property
+def attributes(self) -> Set[str]:  # type: ignore[override]
+    return super().attributes.union({"initial_time", "timezone"})
+
+

Build an unordered collection of unique elements.

+
+
+

Inherited members

+ +
+
+class UrlInputElement +(*,
action_id: str | None = None,
initial_value: str | None = None,
dispatch_action_config: dict | DispatchActionConfig | None = None,
focus_on_load: bool | None = None,
placeholder: str | dict | TextObject | None = None,
**others: dict)
+
+
+
+ +Expand source code + +
class UrlInputElement(InputInteractiveElement):
+    type = "url_text_input"
+
+    @property
+    def attributes(self) -> Set[str]:  # type: ignore[override]
+        return super().attributes.union(
+            {
+                "initial_value",
+                "dispatch_action_config",
+            }
+        )
+
+    def __init__(
+        self,
+        *,
+        action_id: Optional[str] = None,
+        initial_value: Optional[str] = None,
+        dispatch_action_config: Optional[Union[dict, DispatchActionConfig]] = None,
+        focus_on_load: Optional[bool] = None,
+        placeholder: Optional[Union[str, dict, TextObject]] = None,
+        **others: dict,
+    ):
+        """
+        A URL input element, similar to the Plain-text input element,
+        creates a single line field where a user can enter URL-encoded data.
+        https://docs.slack.dev/reference/block-kit/block-elements/url-input-element
+
+        Args:
+            action_id (required): An identifier for the input value when the parent modal is submitted.
+                You can use this when you receive a view_submission payload to identify the value of the input element.
+                Should be unique among all other action_ids in the containing block.
+                Maximum length for this field is 255 characters.
+            initial_value: The initial value in the URL input when it is loaded.
+            dispatch_action_config: A dispatch configuration object that determines when during text input
+                the element returns a block_actions payload.
+            focus_on_load: Indicates whether the element will be set to auto focus within the view object.
+                Only one element can be set to true. Defaults to false.
+            placeholder: A plain_text only text object that defines the placeholder text shown in the URL input.
+                Maximum length for the text in this field is 150 characters.
+        """
+        super().__init__(
+            type=self.type,
+            action_id=action_id,
+            placeholder=TextObject.parse(placeholder, PlainTextObject.type),  # type: ignore[arg-type]
+            focus_on_load=focus_on_load,
+        )
+        show_unknown_key_warning(self, others)
+
+        self.initial_value = initial_value
+        self.dispatch_action_config = dispatch_action_config
+
+

Block Elements are things that exists inside of your Blocks. +https://docs.slack.dev/reference/block-kit/block-elements/

+

A URL input element, similar to the Plain-text input element, +creates a single line field where a user can enter URL-encoded data. +https://docs.slack.dev/reference/block-kit/block-elements/url-input-element

+

Args

+
+
action_id : required
+
An identifier for the input value when the parent modal is submitted. +You can use this when you receive a view_submission payload to identify the value of the input element. +Should be unique among all other action_ids in the containing block. +Maximum length for this field is 255 characters.
+
initial_value
+
The initial value in the URL input when it is loaded.
+
dispatch_action_config
+
A dispatch configuration object that determines when during text input +the element returns a block_actions payload.
+
focus_on_load
+
Indicates whether the element will be set to auto focus within the view object. +Only one element can be set to true. Defaults to false.
+
placeholder
+
A plain_text only text object that defines the placeholder text shown in the URL input. +Maximum length for the text in this field is 150 characters.
+
+

Ancestors

+ +

Class variables

+
+
var type
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
prop attributes : Set[str]
+
+
+ +Expand source code + +
@property
+def attributes(self) -> Set[str]:  # type: ignore[override]
+    return super().attributes.union(
+        {
+            "initial_value",
+            "dispatch_action_config",
+        }
+    )
+
+

Build an unordered collection of unique elements.

+
+
+

Inherited members

+ +
+
+class UserMultiSelectElement +(*,
action_id: str | None = None,
placeholder: str | dict | TextObject | None = None,
initial_users: Sequence[str] | None = None,
confirm: dict | ConfirmObject | None = None,
max_selected_items: int | None = None,
focus_on_load: bool | None = None,
**others: dict)
+
+
+
+ +Expand source code + +
class UserMultiSelectElement(InputInteractiveElement):
+    type = "multi_users_select"
+
+    @property
+    def attributes(self) -> Set[str]:  # type: ignore[override]
+        return super().attributes.union({"initial_users", "max_selected_items"})
+
+    def __init__(
+        self,
+        *,
+        action_id: Optional[str] = None,
+        placeholder: Optional[Union[str, dict, TextObject]] = None,
+        initial_users: Optional[Sequence[str]] = None,
+        confirm: Optional[Union[dict, ConfirmObject]] = None,
+        max_selected_items: Optional[int] = None,
+        focus_on_load: Optional[bool] = None,
+        **others: dict,
+    ):
+        """
+        This select menu will populate its options with a list of Slack users visible to
+        the current user in the active workspace.
+        https://docs.slack.dev/reference/block-kit/block-elements/multi-select-menu-element#users_multi_select
+
+        Args:
+            action_id (required): An identifier for the action triggered when a menu option is selected.
+                You can use this when you receive an interaction payload to identify the source of the action.
+                Should be unique among all other action_ids in the containing block.
+                Maximum length for this field is 255 characters.
+            placeholder (required): A plain_text only text object that defines the placeholder text shown on the menu.
+                Maximum length for the text in this field is 150 characters.
+            initial_users: An array of user IDs of any valid users to be pre-selected when the menu loads.
+            confirm: A confirm object that defines an optional confirmation dialog that appears
+                before the multi-select choices are submitted.
+            max_selected_items: Specifies the maximum number of items that can be selected in the menu.
+                Minimum number is 1.
+            focus_on_load: Indicates whether the element will be set to auto focus within the view object.
+                Only one element can be set to true. Defaults to false.
+        """
+        super().__init__(
+            type=self.type,
+            action_id=action_id,
+            placeholder=TextObject.parse(placeholder, PlainTextObject.type),  # type: ignore[arg-type]
+            confirm=ConfirmObject.parse(confirm),  # type: ignore[arg-type]
+            focus_on_load=focus_on_load,
+        )
+        show_unknown_key_warning(self, others)
+
+        self.initial_users = initial_users
+        self.max_selected_items = max_selected_items
+
+

Block Elements are things that exists inside of your Blocks. +https://docs.slack.dev/reference/block-kit/block-elements/

+

This select menu will populate its options with a list of Slack users visible to +the current user in the active workspace. +https://docs.slack.dev/reference/block-kit/block-elements/multi-select-menu-element#users_multi_select

+

Args

+
+
action_id : required
+
An identifier for the action triggered when a menu option is selected. +You can use this when you receive an interaction payload to identify the source of the action. +Should be unique among all other action_ids in the containing block. +Maximum length for this field is 255 characters.
+
placeholder : required
+
A plain_text only text object that defines the placeholder text shown on the menu. +Maximum length for the text in this field is 150 characters.
+
initial_users
+
An array of user IDs of any valid users to be pre-selected when the menu loads.
+
confirm
+
A confirm object that defines an optional confirmation dialog that appears +before the multi-select choices are submitted.
+
max_selected_items
+
Specifies the maximum number of items that can be selected in the menu. +Minimum number is 1.
+
focus_on_load
+
Indicates whether the element will be set to auto focus within the view object. +Only one element can be set to true. Defaults to false.
+
+

Ancestors

+ +

Class variables

+
+
var type
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
prop attributes : Set[str]
+
+
+ +Expand source code + +
@property
+def attributes(self) -> Set[str]:  # type: ignore[override]
+    return super().attributes.union({"initial_users", "max_selected_items"})
+
+

Build an unordered collection of unique elements.

+
+
+

Inherited members

+ +
+
+class UserSelectElement +(*,
placeholder: str | dict | TextObject | None = None,
action_id: str | None = None,
initial_user: str | None = None,
confirm: dict | ConfirmObject | None = None,
focus_on_load: bool | None = None,
**others: dict)
+
+
+
+ +Expand source code + +
class UserSelectElement(InputInteractiveElement):
+    type = "users_select"
+
+    @property
+    def attributes(self) -> Set[str]:  # type: ignore[override]
+        return super().attributes.union({"initial_user"})
+
+    def __init__(
+        self,
+        *,
+        placeholder: Optional[Union[str, dict, TextObject]] = None,
+        action_id: Optional[str] = None,
+        initial_user: Optional[str] = None,
+        confirm: Optional[Union[dict, ConfirmObject]] = None,
+        focus_on_load: Optional[bool] = None,
+        **others: dict,
+    ):
+        """
+        This select menu will populate its options with a list of Slack users visible to
+        the current user in the active workspace.
+        https://docs.slack.dev/reference/block-kit/block-elements/select-menu-element#users_select
+
+        Args:
+            placeholder (required): A plain_text only text object that defines the placeholder text shown on the menu.
+                Maximum length for the text in this field is 150 characters.
+            action_id (required): An identifier for the action triggered when a menu option is selected.
+                You can use this when you receive an interaction payload to identify the source of the action.
+                Should be unique among all other action_ids in the containing block.
+                Maximum length for this field is 255 characters.
+            initial_user: The user ID of any valid user to be pre-selected when the menu loads.
+            confirm: A confirm object that defines an optional confirmation dialog
+                that appears after a menu item is selected.
+            focus_on_load: Indicates whether the element will be set to auto focus within the view object.
+                Only one element can be set to true. Defaults to false.
+        """
+        super().__init__(
+            type=self.type,
+            action_id=action_id,
+            placeholder=TextObject.parse(placeholder, PlainTextObject.type),  # type: ignore[arg-type]
+            confirm=ConfirmObject.parse(confirm),  # type: ignore[arg-type]
+            focus_on_load=focus_on_load,
+        )
+        show_unknown_key_warning(self, others)
+
+        self.initial_user = initial_user
+
+

Block Elements are things that exists inside of your Blocks. +https://docs.slack.dev/reference/block-kit/block-elements/

+

This select menu will populate its options with a list of Slack users visible to +the current user in the active workspace. +https://docs.slack.dev/reference/block-kit/block-elements/select-menu-element#users_select

+

Args

+
+
placeholder : required
+
A plain_text only text object that defines the placeholder text shown on the menu. +Maximum length for the text in this field is 150 characters.
+
action_id : required
+
An identifier for the action triggered when a menu option is selected. +You can use this when you receive an interaction payload to identify the source of the action. +Should be unique among all other action_ids in the containing block. +Maximum length for this field is 255 characters.
+
initial_user
+
The user ID of any valid user to be pre-selected when the menu loads.
+
confirm
+
A confirm object that defines an optional confirmation dialog +that appears after a menu item is selected.
+
focus_on_load
+
Indicates whether the element will be set to auto focus within the view object. +Only one element can be set to true. Defaults to false.
+
+

Ancestors

+ +

Class variables

+
+
var type
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
prop attributes : Set[str]
+
+
+ +Expand source code + +
@property
+def attributes(self) -> Set[str]:  # type: ignore[override]
+    return super().attributes.union({"initial_user"})
+
+

Build an unordered collection of unique elements.

+
+
+

Inherited members

+ +
+
+class WorkflowButtonElement +(*,
text: str | dict | TextObject,
action_id: str | None = None,
workflow: dict | Workflow | None = None,
style: str | None = None,
accessibility_label: str | None = None,
**others: dict)
+
+
+
+ +Expand source code + +
class WorkflowButtonElement(InteractiveElement):
+    type = "workflow_button"
+
+    @property
+    def attributes(self) -> Set[str]:  # type: ignore[override]
+        return super().attributes.union({"text", "workflow", "style", "accessibility_label"})
+
+    def __init__(
+        self,
+        *,
+        text: Union[str, dict, TextObject],
+        action_id: Optional[str] = None,
+        workflow: Optional[Union[dict, Workflow]] = None,
+        style: Optional[str] = None,  # primary, danger
+        accessibility_label: Optional[str] = None,
+        **others: dict,
+    ):
+        """Allows users to run a link trigger with customizable inputs
+        Interactive component - but interactions with workflow button elements will not send block_actions events,
+        since these are used to start new workflow runs.
+        https://docs.slack.dev/reference/block-kit/block-elements/workflow-button-element
+
+        Args:
+            text (required): A text object that defines the button's text.
+                Can only be of type: plain_text. text may truncate with ~30 characters.
+                Maximum length for the text in this field is 75 characters.
+            action_id (required): An identifier for this action.
+                You can use this when you receive an interaction payload to identify the source of the action.
+                Should be unique among all other action_ids in the containing block.
+                Maximum length for this field is 255 characters.
+            workflow: A workflow object that contains details about the workflow
+                that will run when the button is clicked.
+            style: Decorates buttons with alternative visual color schemes. Use this option with restraint.
+                "primary" gives buttons a green outline and text, ideal for affirmation or confirmation actions.
+                "primary" should only be used for one button within a set.
+                "danger" gives buttons a red outline and text, and should be used when the action is destructive.
+                Use "danger" even more sparingly than "primary".
+                If you don't include this field, the default button style will be used.
+            accessibility_label: A label for longer descriptive text about a button element.
+                This label will be read out by screen readers instead of the button text object.
+                Maximum length for this field is 75 characters.
+        """
+        super().__init__(action_id=action_id, type=self.type)
+        show_unknown_key_warning(self, others)
+
+        # NOTE: default_type=PlainTextObject.type here is only for backward-compatibility with version 2.5.0
+        self.text = TextObject.parse(text, default_type=PlainTextObject.type)
+        self.workflow = workflow
+        self.style = style
+        self.accessibility_label = accessibility_label
+
+

Block Elements are things that exists inside of your Blocks. +https://docs.slack.dev/reference/block-kit/block-elements/

+

Allows users to run a link trigger with customizable inputs +Interactive component - but interactions with workflow button elements will not send block_actions events, +since these are used to start new workflow runs. +https://docs.slack.dev/reference/block-kit/block-elements/workflow-button-element

+

Args

+
+
text : required
+
A text object that defines the button's text. +Can only be of type: plain_text. text may truncate with ~30 characters. +Maximum length for the text in this field is 75 characters.
+
action_id : required
+
An identifier for this action. +You can use this when you receive an interaction payload to identify the source of the action. +Should be unique among all other action_ids in the containing block. +Maximum length for this field is 255 characters.
+
workflow
+
A workflow object that contains details about the workflow +that will run when the button is clicked.
+
style
+
Decorates buttons with alternative visual color schemes. Use this option with restraint. +"primary" gives buttons a green outline and text, ideal for affirmation or confirmation actions. +"primary" should only be used for one button within a set. +"danger" gives buttons a red outline and text, and should be used when the action is destructive. +Use "danger" even more sparingly than "primary". +If you don't include this field, the default button style will be used.
+
accessibility_label
+
A label for longer descriptive text about a button element. +This label will be read out by screen readers instead of the button text object. +Maximum length for this field is 75 characters.
+
+

Ancestors

+ +

Class variables

+
+
var type
+
+

The type of the None singleton.

+
+
+

Inherited members

+ +
+
+
+
+ +
+ + + diff --git a/docs/reference/models/blocks/blocks.html b/docs/reference/models/blocks/blocks.html new file mode 100644 index 000000000..722d12164 --- /dev/null +++ b/docs/reference/models/blocks/blocks.html @@ -0,0 +1,2051 @@ + + + + + + +slack_sdk.models.blocks.blocks API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.models.blocks.blocks

+
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class ActionsBlock +(*,
elements: Sequence[dict | InteractiveElement],
block_id: str | None = None,
**others: dict)
+
+
+
+ +Expand source code + +
class ActionsBlock(Block):
+    type = "actions"
+    elements_max_length = 25
+
+    @property
+    def attributes(self) -> Set[str]:  # type: ignore[override]
+        return super().attributes.union({"elements"})
+
+    def __init__(
+        self,
+        *,
+        elements: Sequence[Union[dict, InteractiveElement]],
+        block_id: Optional[str] = None,
+        **others: dict,
+    ):
+        """A block that is used to hold interactive elements.
+        https://docs.slack.dev/reference/block-kit/blocks/actions-block
+
+        Args:
+            elements (required): An array of interactive element objects - buttons, select menus, overflow menus,
+                or date pickers. There is a maximum of 25 elements in each action block.
+            block_id: A string acting as a unique identifier for a block.
+                If not specified, a block_id will be generated.
+                You can use this block_id when you receive an interaction payload to identify the source of the action.
+                Maximum length for this field is 255 characters.
+                block_id should be unique for each message and each iteration of a message.
+                If a message is updated, use a new block_id.
+        """
+        super().__init__(type=self.type, block_id=block_id)
+        show_unknown_key_warning(self, others)
+
+        self.elements = BlockElement.parse_all(elements)
+
+    @JsonValidator(f"elements attribute cannot exceed {elements_max_length} elements")
+    def _validate_elements_length(self):
+        return self.elements is None or len(self.elements) <= self.elements_max_length
+
+

Blocks are a series of components that can be combined +to create visually rich and compellingly interactive messages. +https://docs.slack.dev/reference/block-kit/blocks

+

A block that is used to hold interactive elements. +https://docs.slack.dev/reference/block-kit/blocks/actions-block

+

Args

+
+
elements : required
+
An array of interactive element objects - buttons, select menus, overflow menus, +or date pickers. There is a maximum of 25 elements in each action block.
+
block_id
+
A string acting as a unique identifier for a block. +If not specified, a block_id will be generated. +You can use this block_id when you receive an interaction payload to identify the source of the action. +Maximum length for this field is 255 characters. +block_id should be unique for each message and each iteration of a message. +If a message is updated, use a new block_id.
+
+

Ancestors

+ +

Class variables

+
+
var elements_max_length
+
+

The type of the None singleton.

+
+
var type
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
prop attributes : Set[str]
+
+
+ +Expand source code + +
@property
+def attributes(self) -> Set[str]:  # type: ignore[override]
+    return super().attributes.union({"elements"})
+
+

Build an unordered collection of unique elements.

+
+
+

Inherited members

+ +
+
+class Block +(*,
type: str | None = None,
subtype: str | None = None,
block_id: str | None = None)
+
+
+
+ +Expand source code + +
class Block(JsonObject):
+    """Blocks are a series of components that can be combined
+    to create visually rich and compellingly interactive messages.
+    https://docs.slack.dev/reference/block-kit/blocks
+    """
+
+    attributes = {"block_id", "type"}
+    block_id_max_length = 255
+    logger = logging.getLogger(__name__)
+
+    def _subtype_warning(self):
+        warnings.warn(
+            "subtype is deprecated since slackclient 2.6.0, use type instead",
+            DeprecationWarning,
+        )
+
+    @property
+    def subtype(self) -> Optional[str]:
+        return self.type
+
+    def __init__(
+        self,
+        *,
+        type: Optional[str] = None,
+        subtype: Optional[str] = None,  # deprecated
+        block_id: Optional[str] = None,
+    ):
+        if subtype:
+            self._subtype_warning()
+        self.type = type if type else subtype
+        self.block_id = block_id
+        self.color = None
+
+    @JsonValidator(f"block_id cannot exceed {block_id_max_length} characters")
+    def _validate_block_id_length(self):
+        return self.block_id is None or len(self.block_id) <= self.block_id_max_length
+
+    @classmethod
+    def parse(cls, block: Union[dict, "Block"]) -> Optional["Block"]:
+        if block is None:
+            return None
+        elif isinstance(block, Block):
+            return block
+        else:
+            if "type" in block:
+                type = block["type"]
+                if type == SectionBlock.type:
+                    return SectionBlock(**block)
+                elif type == DividerBlock.type:
+                    return DividerBlock(**block)
+                elif type == ImageBlock.type:
+                    return ImageBlock(**block)
+                elif type == ActionsBlock.type:
+                    return ActionsBlock(**block)
+                elif type == ContextBlock.type:
+                    return ContextBlock(**block)
+                elif type == ContextActionsBlock.type:
+                    return ContextActionsBlock(**block)
+                elif type == InputBlock.type:
+                    return InputBlock(**block)
+                elif type == FileBlock.type:
+                    return FileBlock(**block)
+                elif type == CallBlock.type:
+                    return CallBlock(**block)
+                elif type == HeaderBlock.type:
+                    return HeaderBlock(**block)
+                elif type == MarkdownBlock.type:
+                    return MarkdownBlock(**block)
+                elif type == VideoBlock.type:
+                    return VideoBlock(**block)
+                elif type == RichTextBlock.type:
+                    return RichTextBlock(**block)
+                elif type == TableBlock.type:
+                    return TableBlock(**block)
+                else:
+                    cls.logger.warning(f"Unknown block detected and skipped ({block})")
+                    return None
+            else:
+                cls.logger.warning(f"Unknown block detected and skipped ({block})")
+                return None
+
+    @classmethod
+    def parse_all(cls, blocks: Optional[Sequence[Union[dict, "Block"]]]) -> List["Block"]:
+        return [cls.parse(b) for b in blocks or []]  # type: ignore[misc]
+
+

Blocks are a series of components that can be combined +to create visually rich and compellingly interactive messages. +https://docs.slack.dev/reference/block-kit/blocks

+

Ancestors

+ +

Subclasses

+ +

Class variables

+
+
var attributes
+
+

The type of the None singleton.

+
+
var block_id_max_length
+
+

The type of the None singleton.

+
+
var logger
+
+

The type of the None singleton.

+
+
+

Static methods

+
+
+def parse(block: dict | ForwardRef('Block')) ‑> Block | None +
+
+
+
+
+def parse_all(blocks: Sequence[dict | ForwardRef('Block')] | None) ‑> List[Block] +
+
+
+
+
+

Instance variables

+
+
prop subtype : str | None
+
+
+ +Expand source code + +
@property
+def subtype(self) -> Optional[str]:
+    return self.type
+
+
+
+
+

Inherited members

+ +
+
+class CallBlock +(*,
call_id: str,
api_decoration_available: bool | None = None,
call: Dict[str, Dict[str, Any]] | None = None,
block_id: str | None = None,
**others: dict)
+
+
+
+ +Expand source code + +
class CallBlock(Block):
+    type = "call"
+
+    @property
+    def attributes(self) -> Set[str]:  # type: ignore[override]
+        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://docs.slack.dev/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
+
+

Blocks are a series of components that can be combined +to create visually rich and compellingly interactive messages. +https://docs.slack.dev/reference/block-kit/blocks

+

Displays a call information +https://docs.slack.dev/reference/block-kit/blocks#call

+

Ancestors

+ +

Class variables

+
+
var type
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
prop attributes : Set[str]
+
+
+ +Expand source code + +
@property
+def attributes(self) -> Set[str]:  # type: ignore[override]
+    return super().attributes.union({"call_id", "api_decoration_available", "call"})
+
+

Build an unordered collection of unique elements.

+
+
+

Inherited members

+ +
+
+class ContextActionsBlock +(*,
elements: Sequence[dict | FeedbackButtonsElement | IconButtonElement],
block_id: str | None = None,
**others: dict)
+
+
+
+ +Expand source code + +
class ContextActionsBlock(Block):
+    type = "context_actions"
+    elements_max_length = 5
+
+    @property
+    def attributes(self) -> Set[str]:  # type: ignore[override]
+        return super().attributes.union({"elements"})
+
+    def __init__(
+        self,
+        *,
+        elements: Sequence[Union[dict, FeedbackButtonsElement, IconButtonElement]],
+        block_id: Optional[str] = None,
+        **others: dict,
+    ):
+        """Displays actions as contextual info, which can include both feedback buttons and icon buttons.
+        https://docs.slack.dev/reference/block-kit/blocks/context-actions-block
+
+        Args:
+            elements (required): An array of feedback_buttons or icon_button block elements. Maximum number of items is 5.
+            block_id: A string acting as a unique identifier for a block. If not specified, one will be generated.
+                Maximum length for this field is 255 characters.
+                block_id should be unique for each message and each iteration of a message.
+                If a message is updated, use a new block_id.
+        """
+        super().__init__(type=self.type, block_id=block_id)
+        show_unknown_key_warning(self, others)
+
+        self.elements = BlockElement.parse_all(elements)
+
+    @JsonValidator("elements attribute must be specified")
+    def _validate_elements(self):
+        return self.elements is None or len(self.elements) > 0
+
+    @JsonValidator(f"elements attribute cannot exceed {elements_max_length} elements")
+    def _validate_elements_length(self):
+        return self.elements is None or len(self.elements) <= self.elements_max_length
+
+

Blocks are a series of components that can be combined +to create visually rich and compellingly interactive messages. +https://docs.slack.dev/reference/block-kit/blocks

+

Displays actions as contextual info, which can include both feedback buttons and icon buttons. +https://docs.slack.dev/reference/block-kit/blocks/context-actions-block

+

Args

+
+
elements : required
+
An array of feedback_buttons or icon_button block elements. Maximum number of items is 5.
+
block_id
+
A string acting as a unique identifier for a block. If not specified, one will be generated. +Maximum length for this field is 255 characters. +block_id should be unique for each message and each iteration of a message. +If a message is updated, use a new block_id.
+
+

Ancestors

+ +

Class variables

+
+
var elements_max_length
+
+

The type of the None singleton.

+
+
var type
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
prop attributes : Set[str]
+
+
+ +Expand source code + +
@property
+def attributes(self) -> Set[str]:  # type: ignore[override]
+    return super().attributes.union({"elements"})
+
+

Build an unordered collection of unique elements.

+
+
+

Inherited members

+ +
+
+class ContextBlock +(*,
elements: Sequence[dict | ImageElement | TextObject],
block_id: str | None = None,
**others: dict)
+
+
+
+ +Expand source code + +
class ContextBlock(Block):
+    type = "context"
+    elements_max_length = 10
+
+    @property
+    def attributes(self) -> Set[str]:  # type: ignore[override]
+        return super().attributes.union({"elements"})
+
+    def __init__(
+        self,
+        *,
+        elements: Sequence[Union[dict, ImageElement, TextObject]],
+        block_id: Optional[str] = None,
+        **others: dict,
+    ):
+        """Displays message context, which can include both images and text.
+        https://docs.slack.dev/reference/block-kit/blocks/context-block
+
+        Args:
+            elements (required): An array of image elements and text objects. Maximum number of items is 10.
+            block_id: A string acting as a unique identifier for a block. If not specified, one will be generated.
+                Maximum length for this field is 255 characters.
+                block_id should be unique for each message and each iteration of a message.
+                If a message is updated, use a new block_id.
+        """
+        super().__init__(type=self.type, block_id=block_id)
+        show_unknown_key_warning(self, others)
+
+        self.elements = BlockElement.parse_all(elements)
+
+    @JsonValidator(f"elements attribute cannot exceed {elements_max_length} elements")
+    def _validate_elements_length(self):
+        return self.elements is None or len(self.elements) <= self.elements_max_length
+
+

Blocks are a series of components that can be combined +to create visually rich and compellingly interactive messages. +https://docs.slack.dev/reference/block-kit/blocks

+

Displays message context, which can include both images and text. +https://docs.slack.dev/reference/block-kit/blocks/context-block

+

Args

+
+
elements : required
+
An array of image elements and text objects. Maximum number of items is 10.
+
block_id
+
A string acting as a unique identifier for a block. If not specified, one will be generated. +Maximum length for this field is 255 characters. +block_id should be unique for each message and each iteration of a message. +If a message is updated, use a new block_id.
+
+

Ancestors

+ +

Class variables

+
+
var elements_max_length
+
+

The type of the None singleton.

+
+
var type
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
prop attributes : Set[str]
+
+
+ +Expand source code + +
@property
+def attributes(self) -> Set[str]:  # type: ignore[override]
+    return super().attributes.union({"elements"})
+
+

Build an unordered collection of unique elements.

+
+
+

Inherited members

+ +
+
+class DividerBlock +(*, block_id: str | None = None, **others: dict) +
+
+
+ +Expand source code + +
class DividerBlock(Block):
+    type = "divider"
+
+    def __init__(
+        self,
+        *,
+        block_id: Optional[str] = None,
+        **others: dict,
+    ):
+        """A content divider, like an <hr>, to split up different blocks inside of a message.
+        https://docs.slack.dev/reference/block-kit/blocks/divider-block
+
+        Args:
+            block_id: A string acting as a unique identifier for a block. If not specified, one will be generated.
+                You can use this block_id when you receive an interaction payload to identify the source of the action.
+                Maximum length for this field is 255 characters.
+                block_id should be unique for each message and each iteration of a message.
+                If a message is updated, use a new block_id.
+        """
+        super().__init__(type=self.type, block_id=block_id)
+        show_unknown_key_warning(self, others)
+
+

Blocks are a series of components that can be combined +to create visually rich and compellingly interactive messages. +https://docs.slack.dev/reference/block-kit/blocks

+

A content divider, like an


, to split up different blocks inside of a message. +https://docs.slack.dev/reference/block-kit/blocks/divider-block

+

Args

+
+
block_id
+
A string acting as a unique identifier for a block. If not specified, one will be generated. +You can use this block_id when you receive an interaction payload to identify the source of the action. +Maximum length for this field is 255 characters. +block_id should be unique for each message and each iteration of a message. +If a message is updated, use a new block_id.
+
+

Ancestors

+ +

Class variables

+
+
var type
+
+

The type of the None singleton.

+
+
+

Inherited members

+ +
+
+class FileBlock +(*,
external_id: str,
source: str = 'remote',
block_id: str | None = None,
**others: dict)
+
+
+
+ +Expand source code + +
class FileBlock(Block):
+    type = "file"
+
+    @property
+    def attributes(self) -> Set[str]:  # type: ignore[override]
+        return super().attributes.union({"external_id", "source"})
+
+    def __init__(
+        self,
+        *,
+        external_id: str,
+        source: str = "remote",
+        block_id: Optional[str] = None,
+        **others: dict,
+    ):
+        """Displays a remote file.
+        https://docs.slack.dev/reference/block-kit/blocks/file-block
+
+        Args:
+            external_id (required): The external unique ID for this file.
+            source (required): At the moment, source will always be remote for a remote file.
+            block_id: A string acting as a unique identifier for a block. If not specified, one will be generated.
+                Maximum length for this field is 255 characters.
+                block_id should be unique for each message and each iteration of a message.
+                If a message is updated, use a new block_id.
+        """
+        super().__init__(type=self.type, block_id=block_id)
+        show_unknown_key_warning(self, others)
+
+        self.external_id = external_id
+        self.source = source
+
+

Blocks are a series of components that can be combined +to create visually rich and compellingly interactive messages. +https://docs.slack.dev/reference/block-kit/blocks

+

Displays a remote file. +https://docs.slack.dev/reference/block-kit/blocks/file-block

+

Args

+
+
external_id : required
+
The external unique ID for this file.
+
source : required
+
At the moment, source will always be remote for a remote file.
+
block_id
+
A string acting as a unique identifier for a block. If not specified, one will be generated. +Maximum length for this field is 255 characters. +block_id should be unique for each message and each iteration of a message. +If a message is updated, use a new block_id.
+
+

Ancestors

+ +

Class variables

+
+
var type
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
prop attributes : Set[str]
+
+
+ +Expand source code + +
@property
+def attributes(self) -> Set[str]:  # type: ignore[override]
+    return super().attributes.union({"external_id", "source"})
+
+

Build an unordered collection of unique elements.

+
+
+

Inherited members

+ +
+
+class HeaderBlock +(*,
block_id: str | None = None,
text: str | dict | TextObject | None = None,
**others: dict)
+
+
+
+ +Expand source code + +
class HeaderBlock(Block):
+    type = "header"
+    text_max_length = 150
+
+    @property
+    def attributes(self) -> Set[str]:  # type: ignore[override]
+        return super().attributes.union({"text"})
+
+    def __init__(
+        self,
+        *,
+        block_id: Optional[str] = None,
+        text: Optional[Union[str, dict, TextObject]] = None,
+        **others: dict,
+    ):
+        """A header is a plain-text block that displays in a larger, bold font.
+        https://docs.slack.dev/reference/block-kit/blocks/header-block
+
+        Args:
+            block_id: A string acting as a unique identifier for a block. If not specified, one will be generated.
+                Maximum length for this field is 255 characters.
+                block_id should be unique for each message and each iteration of a message.
+                If a message is updated, use a new block_id.
+            text (required): The text for the block, in the form of a plain_text text object.
+                Maximum length for the text in this field is 150 characters.
+        """
+        super().__init__(type=self.type, block_id=block_id)
+        show_unknown_key_warning(self, others)
+
+        self.text = TextObject.parse(text, default_type=PlainTextObject.type)  # type: ignore[arg-type]
+
+    @JsonValidator("text attribute must be specified")
+    def _validate_text(self):
+        return self.text is not None
+
+    @JsonValidator(f"text attribute cannot exceed {text_max_length} characters")
+    def _validate_alt_text_length(self):
+        return self.text is None or len(self.text.text) <= self.text_max_length
+
+

Blocks are a series of components that can be combined +to create visually rich and compellingly interactive messages. +https://docs.slack.dev/reference/block-kit/blocks

+

A header is a plain-text block that displays in a larger, bold font. +https://docs.slack.dev/reference/block-kit/blocks/header-block

+

Args

+
+
block_id
+
A string acting as a unique identifier for a block. If not specified, one will be generated. +Maximum length for this field is 255 characters. +block_id should be unique for each message and each iteration of a message. +If a message is updated, use a new block_id.
+
text : required
+
The text for the block, in the form of a plain_text text object. +Maximum length for the text in this field is 150 characters.
+
+

Ancestors

+ +

Class variables

+
+
var text_max_length
+
+

The type of the None singleton.

+
+
var type
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
prop attributes : Set[str]
+
+
+ +Expand source code + +
@property
+def attributes(self) -> Set[str]:  # type: ignore[override]
+    return super().attributes.union({"text"})
+
+

Build an unordered collection of unique elements.

+
+
+

Inherited members

+ +
+
+class ImageBlock +(*,
alt_text: str,
image_url: str | None = None,
slack_file: Dict[str, Any] | SlackFile | None = None,
title: str | dict | PlainTextObject | None = None,
block_id: str | None = None,
**others: dict)
+
+
+
+ +Expand source code + +
class ImageBlock(Block):
+    type = "image"
+
+    @property
+    def attributes(self) -> Set[str]:  # type: ignore[override]
+        return super().attributes.union({"alt_text", "image_url", "title", "slack_file"})
+
+    image_url_max_length = 3000
+    alt_text_max_length = 2000
+    title_max_length = 2000
+
+    def __init__(
+        self,
+        *,
+        alt_text: str,
+        image_url: Optional[str] = None,
+        slack_file: Optional[Union[Dict[str, Any], SlackFile]] = None,
+        title: Optional[Union[str, dict, PlainTextObject]] = None,
+        block_id: Optional[str] = None,
+        **others: dict,
+    ):
+        """A simple image block, designed to make those cat photos really pop.
+        https://docs.slack.dev/reference/block-kit/blocks/image-block
+
+        Args:
+            alt_text (required): A plain-text summary of the image. This should not contain any markup.
+                Maximum length for this field is 2000 characters.
+            image_url: The URL of the image to be displayed.
+                Maximum length for this field is 3000 characters.
+            slack_file: A Slack image file object that defines the source of the image.
+            title: An optional title for the image in the form of a text object that can only be of type: plain_text.
+                Maximum length for the text in this field is 2000 characters.
+            block_id: A string acting as a unique identifier for a block. If not specified, one will be generated.
+                Maximum length for this field is 255 characters.
+                block_id should be unique for each message and each iteration of a message.
+                If a message is updated, use a new block_id.
+        """
+        super().__init__(type=self.type, block_id=block_id)
+        show_unknown_key_warning(self, others)
+
+        self.image_url = image_url
+        self.alt_text = alt_text
+        parsed_title = None
+        if title is not None:
+            if isinstance(title, str):
+                parsed_title = PlainTextObject(text=title)
+            elif isinstance(title, dict):
+                if title.get("type") != PlainTextObject.type:
+                    raise SlackObjectFormationError(f"Unsupported type for title in an image block: {title.get('type')}")
+                parsed_title = PlainTextObject(text=title.get("text"), emoji=title.get("emoji"))  # type: ignore[arg-type]
+            elif isinstance(title, PlainTextObject):
+                parsed_title = title
+            else:
+                raise SlackObjectFormationError(f"Unsupported type for title in an image block: {type(title)}")
+        if slack_file is not None:
+            self.slack_file = (
+                slack_file if slack_file is None or isinstance(slack_file, SlackFile) else SlackFile(**slack_file)
+            )
+        self.title = parsed_title
+
+    @JsonValidator(f"image_url attribute cannot exceed {image_url_max_length} characters")
+    def _validate_image_url_length(self):
+        return self.image_url is None or len(self.image_url) <= self.image_url_max_length
+
+    @JsonValidator(f"alt_text attribute cannot exceed {alt_text_max_length} characters")
+    def _validate_alt_text_length(self):
+        return len(self.alt_text) <= self.alt_text_max_length
+
+    @JsonValidator(f"title attribute cannot exceed {title_max_length} characters")
+    def _validate_title_length(self):
+        return self.title is None or self.title.text is None or len(self.title.text) <= self.title_max_length
+
+

Blocks are a series of components that can be combined +to create visually rich and compellingly interactive messages. +https://docs.slack.dev/reference/block-kit/blocks

+

A simple image block, designed to make those cat photos really pop. +https://docs.slack.dev/reference/block-kit/blocks/image-block

+

Args

+
+
alt_text : required
+
A plain-text summary of the image. This should not contain any markup. +Maximum length for this field is 2000 characters.
+
image_url
+
The URL of the image to be displayed. +Maximum length for this field is 3000 characters.
+
slack_file
+
A Slack image file object that defines the source of the image.
+
title
+
An optional title for the image in the form of a text object that can only be of type: plain_text. +Maximum length for the text in this field is 2000 characters.
+
block_id
+
A string acting as a unique identifier for a block. If not specified, one will be generated. +Maximum length for this field is 255 characters. +block_id should be unique for each message and each iteration of a message. +If a message is updated, use a new block_id.
+
+

Ancestors

+ +

Class variables

+
+
var alt_text_max_length
+
+

The type of the None singleton.

+
+
var image_url_max_length
+
+

The type of the None singleton.

+
+
var title_max_length
+
+

The type of the None singleton.

+
+
var type
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
prop attributes : Set[str]
+
+
+ +Expand source code + +
@property
+def attributes(self) -> Set[str]:  # type: ignore[override]
+    return super().attributes.union({"alt_text", "image_url", "title", "slack_file"})
+
+

Build an unordered collection of unique elements.

+
+
+

Inherited members

+ +
+
+class InputBlock +(*,
label: str | dict | PlainTextObject,
element: str | dict | InputInteractiveElement,
block_id: str | None = None,
hint: str | dict | PlainTextObject | None = None,
dispatch_action: bool | None = None,
optional: bool | None = None,
**others: dict)
+
+
+
+ +Expand source code + +
class InputBlock(Block):
+    type = "input"
+    label_max_length = 2000
+    hint_max_length = 2000
+
+    @property
+    def attributes(self) -> Set[str]:  # type: ignore[override]
+        return super().attributes.union({"label", "hint", "element", "optional", "dispatch_action"})
+
+    def __init__(
+        self,
+        *,
+        label: Union[str, dict, PlainTextObject],
+        element: Union[str, dict, InputInteractiveElement],
+        block_id: Optional[str] = None,
+        hint: Optional[Union[str, dict, PlainTextObject]] = None,
+        dispatch_action: Optional[bool] = None,
+        optional: Optional[bool] = None,
+        **others: dict,
+    ):
+        """A block that collects information from users - it can hold a plain-text input element,
+        a select menu element, a multi-select menu element, or a datepicker.
+        https://docs.slack.dev/reference/block-kit/blocks/input-block
+
+        Args:
+            label (required): A label that appears above an input element in the form of a text object
+                that must have type of plain_text. Maximum length for the text in this field is 2000 characters.
+            element (required): An plain-text input element, a checkbox element, a radio button element,
+                a select menu element, a multi-select menu element, or a datepicker.
+            block_id: A string acting as a unique identifier for a block. If not specified, one will be generated.
+                Maximum length for this field is 255 characters.
+                block_id should be unique for each message or view and each iteration of a message or view.
+                If a message or view is updated, use a new block_id.
+            hint: An optional hint that appears below an input element in a lighter grey.
+                It must be a text object with a type of plain_text.
+                Maximum length for the text in this field is 2000 characters.
+            dispatch_action: A boolean that indicates whether or not the use of elements in this block
+                should dispatch a block_actions payload. Defaults to false.
+            optional: A boolean that indicates whether the input element may be empty when a user submits the modal.
+                Defaults to false.
+        """
+        super().__init__(type=self.type, block_id=block_id)
+        show_unknown_key_warning(self, others)
+
+        self.label = TextObject.parse(label, default_type=PlainTextObject.type)
+        self.element = BlockElement.parse(element)  # type: ignore[arg-type]
+        self.hint = TextObject.parse(hint, default_type=PlainTextObject.type)  # type: ignore[arg-type]
+        self.dispatch_action = dispatch_action
+        self.optional = optional
+
+    @JsonValidator(f"label attribute cannot exceed {label_max_length} characters")
+    def _validate_label_length(self):
+        return self.label is None or self.label.text is None or len(self.label.text) <= self.label_max_length
+
+    @JsonValidator(f"hint attribute cannot exceed {hint_max_length} characters")
+    def _validate_hint_length(self):
+        return self.hint is None or self.hint.text is None or len(self.hint.text) <= self.label_max_length
+
+    @JsonValidator(
+        (
+            "element attribute must be a string, select element, multi-select element, "
+            "or a datepicker. (Sub-classes of InputInteractiveElement)"
+        )
+    )
+    def _validate_element_type(self):
+        return self.element is None or isinstance(self.element, (str, InputInteractiveElement))
+
+

Blocks are a series of components that can be combined +to create visually rich and compellingly interactive messages. +https://docs.slack.dev/reference/block-kit/blocks

+

A block that collects information from users - it can hold a plain-text input element, +a select menu element, a multi-select menu element, or a datepicker. +https://docs.slack.dev/reference/block-kit/blocks/input-block

+

Args

+
+
label : required
+
A label that appears above an input element in the form of a text object +that must have type of plain_text. Maximum length for the text in this field is 2000 characters.
+
element : required
+
An plain-text input element, a checkbox element, a radio button element, +a select menu element, a multi-select menu element, or a datepicker.
+
block_id
+
A string acting as a unique identifier for a block. If not specified, one will be generated. +Maximum length for this field is 255 characters. +block_id should be unique for each message or view and each iteration of a message or view. +If a message or view is updated, use a new block_id.
+
hint
+
An optional hint that appears below an input element in a lighter grey. +It must be a text object with a type of plain_text. +Maximum length for the text in this field is 2000 characters.
+
dispatch_action
+
A boolean that indicates whether or not the use of elements in this block +should dispatch a block_actions payload. Defaults to false.
+
optional
+
A boolean that indicates whether the input element may be empty when a user submits the modal. +Defaults to false.
+
+

Ancestors

+ +

Class variables

+
+
var hint_max_length
+
+

The type of the None singleton.

+
+
var label_max_length
+
+

The type of the None singleton.

+
+
var type
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
prop attributes : Set[str]
+
+
+ +Expand source code + +
@property
+def attributes(self) -> Set[str]:  # type: ignore[override]
+    return super().attributes.union({"label", "hint", "element", "optional", "dispatch_action"})
+
+

Build an unordered collection of unique elements.

+
+
+

Inherited members

+ +
+
+class MarkdownBlock +(*, text: str, block_id: str | None = None, **others: dict) +
+
+
+ +Expand source code + +
class MarkdownBlock(Block):
+    type = "markdown"
+    text_max_length = 12000
+
+    @property
+    def attributes(self) -> Set[str]:  # type: ignore[override]
+        return super().attributes.union({"text"})
+
+    def __init__(
+        self,
+        *,
+        text: str,
+        block_id: Optional[str] = None,
+        **others: dict,
+    ):
+        """Displays formatted markdown.
+        https://docs.slack.dev/reference/block-kit/blocks/markdown-block/
+
+        Args:
+            block_id: A string acting as a unique identifier for a block. If not specified, one will be generated.
+                Maximum length for this field is 255 characters.
+                block_id should be unique for each message and each iteration of a message.
+                If a message is updated, use a new block_id.
+            text (required): The standard markdown-formatted text. Limit 12,000 characters max.
+        """
+        super().__init__(type=self.type, block_id=block_id)
+        show_unknown_key_warning(self, others)
+
+        self.text = text
+
+    @JsonValidator("text attribute must be specified")
+    def _validate_text(self):
+        return self.text != ""
+
+    @JsonValidator(f"text attribute cannot exceed {text_max_length} characters")
+    def _validate_alt_text_length(self):
+        return len(self.text) <= self.text_max_length
+
+

Blocks are a series of components that can be combined +to create visually rich and compellingly interactive messages. +https://docs.slack.dev/reference/block-kit/blocks

+

Displays formatted markdown. +https://docs.slack.dev/reference/block-kit/blocks/markdown-block/

+

Args

+
+
block_id
+
A string acting as a unique identifier for a block. If not specified, one will be generated. +Maximum length for this field is 255 characters. +block_id should be unique for each message and each iteration of a message. +If a message is updated, use a new block_id.
+
text : required
+
The standard markdown-formatted text. Limit 12,000 characters max.
+
+

Ancestors

+ +

Class variables

+
+
var text_max_length
+
+

The type of the None singleton.

+
+
var type
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
prop attributes : Set[str]
+
+
+ +Expand source code + +
@property
+def attributes(self) -> Set[str]:  # type: ignore[override]
+    return super().attributes.union({"text"})
+
+

Build an unordered collection of unique elements.

+
+
+

Inherited members

+ +
+
+class RichTextBlock +(*,
elements: Sequence[dict | RichTextElement],
block_id: str | None = None,
**others: dict)
+
+
+
+ +Expand source code + +
class RichTextBlock(Block):
+    type = "rich_text"
+
+    @property
+    def attributes(self) -> Set[str]:  # type: ignore[override]
+        return super().attributes.union({"elements"})
+
+    def __init__(
+        self,
+        *,
+        elements: Sequence[Union[dict, RichTextElement]],
+        block_id: Optional[str] = None,
+        **others: dict,
+    ):
+        """A block that is used to hold interactive elements.
+        https://docs.slack.dev/reference/block-kit/blocks/rich-text-block
+
+        Args:
+            elements (required): An array of rich text objects -
+                rich_text_section, rich_text_list, rich_text_quote, rich_text_preformatted
+            block_id: A unique identifier for a block. If not specified, one will be generated.
+                Maximum length for this field is 255 characters.
+                block_id should be unique for each message or view and each iteration of a message or view.
+                If a message or view is updated, use a new block_id.
+        """
+        super().__init__(type=self.type, block_id=block_id)
+        show_unknown_key_warning(self, others)
+
+        self.elements = BlockElement.parse_all(elements)
+
+

Blocks are a series of components that can be combined +to create visually rich and compellingly interactive messages. +https://docs.slack.dev/reference/block-kit/blocks

+

A block that is used to hold interactive elements. +https://docs.slack.dev/reference/block-kit/blocks/rich-text-block

+

Args

+
+
elements : required
+
An array of rich text objects - +rich_text_section, rich_text_list, rich_text_quote, rich_text_preformatted
+
block_id
+
A unique identifier for a block. If not specified, one will be generated. +Maximum length for this field is 255 characters. +block_id should be unique for each message or view and each iteration of a message or view. +If a message or view is updated, use a new block_id.
+
+

Ancestors

+ +

Class variables

+
+
var type
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
prop attributes : Set[str]
+
+
+ +Expand source code + +
@property
+def attributes(self) -> Set[str]:  # type: ignore[override]
+    return super().attributes.union({"elements"})
+
+

Build an unordered collection of unique elements.

+
+
+

Inherited members

+ +
+
+class SectionBlock +(*,
block_id: str | None = None,
text: str | dict | TextObject | None = None,
fields: Sequence[str | dict | TextObject] | None = None,
accessory: dict | BlockElement | None = None,
expand: bool | None = None,
**others: dict)
+
+
+
+ +Expand source code + +
class SectionBlock(Block):
+    type = "section"
+    fields_max_length = 10
+    text_max_length = 3000
+
+    @property
+    def attributes(self) -> Set[str]:  # type: ignore[override]
+        return super().attributes.union({"text", "fields", "accessory", "expand"})
+
+    def __init__(
+        self,
+        *,
+        block_id: Optional[str] = None,
+        text: Optional[Union[str, dict, TextObject]] = None,
+        fields: Optional[Sequence[Union[str, dict, TextObject]]] = None,
+        accessory: Optional[Union[dict, BlockElement]] = None,
+        expand: Optional[bool] = None,
+        **others: dict,
+    ):
+        """A section is one of the most flexible blocks available.
+        https://docs.slack.dev/reference/block-kit/blocks/section-block
+
+        Args:
+            block_id (required): A string acting as a unique identifier for a block.
+                If not specified, one will be generated.
+                You can use this block_id when you receive an interaction payload to identify the source of the action.
+                Maximum length for this field is 255 characters.
+                block_id should be unique for each message and each iteration of a message.
+                If a message is updated, use a new block_id.
+            text (preferred): The text for the block, in the form of a text object.
+                Maximum length for the text in this field is 3000 characters.
+                This field is not required if a valid array of fields objects is provided instead.
+            fields (required if no text is provided): Required if no text is provided.
+                An array of text objects. Any text objects included with fields will be rendered
+                in a compact format that allows for 2 columns of side-by-side text.
+                Maximum number of items is 10. Maximum length for the text in each item is 2000 characters.
+            accessory: One of the available element objects.
+            expand: Whether or not this section block's text should always expand when rendered.
+                If false or not provided, it may be rendered with a 'see more' option to expand and show the full text.
+                For AI Assistant apps, this allows the app to post long messages without users needing
+                to click 'see more' to expand the message.
+        """
+        super().__init__(type=self.type, block_id=block_id)
+        show_unknown_key_warning(self, others)
+
+        self.text = TextObject.parse(text)  # type: ignore[arg-type]
+        field_objects = []
+        for f in fields or []:
+            if isinstance(f, str):
+                field_objects.append(MarkdownTextObject.from_str(f))
+            elif isinstance(f, TextObject):
+                field_objects.append(f)  # type: ignore[arg-type]
+            elif isinstance(f, dict) and "type" in f:
+                d = copy.copy(f)
+                t = d.pop("type")
+                if t == MarkdownTextObject.type:
+                    field_objects.append(MarkdownTextObject(**d))
+                else:
+                    field_objects.append(PlainTextObject(**d))  # type: ignore[arg-type]
+            else:
+                self.logger.warning(f"Unsupported filed detected and skipped {f}")
+        self.fields = field_objects
+        self.accessory = BlockElement.parse(accessory)  # type: ignore[arg-type]
+        self.expand = expand
+
+    @JsonValidator("text or fields attribute must be specified")
+    def _validate_text_or_fields_populated(self):
+        return self.text is not None or self.fields
+
+    @JsonValidator(f"fields attribute cannot exceed {fields_max_length} items")
+    def _validate_fields_length(self):
+        return self.fields is None or len(self.fields) <= self.fields_max_length
+
+    @JsonValidator(f"text attribute cannot exceed {text_max_length} characters")
+    def _validate_alt_text_length(self):
+        return self.text is None or len(self.text.text) <= self.text_max_length
+
+

Blocks are a series of components that can be combined +to create visually rich and compellingly interactive messages. +https://docs.slack.dev/reference/block-kit/blocks

+

A section is one of the most flexible blocks available. +https://docs.slack.dev/reference/block-kit/blocks/section-block

+

Args

+
+
block_id : required
+
A string acting as a unique identifier for a block. +If not specified, one will be generated. +You can use this block_id when you receive an interaction payload to identify the source of the action. +Maximum length for this field is 255 characters. +block_id should be unique for each message and each iteration of a message. +If a message is updated, use a new block_id.
+
text : preferred
+
The text for the block, in the form of a text object. +Maximum length for the text in this field is 3000 characters. +This field is not required if a valid array of fields objects is provided instead.
+
fields : required if no text is provided
+
Required if no text is provided. +An array of text objects. Any text objects included with fields will be rendered +in a compact format that allows for 2 columns of side-by-side text. +Maximum number of items is 10. Maximum length for the text in each item is 2000 characters.
+
accessory
+
One of the available element objects.
+
expand
+
Whether or not this section block's text should always expand when rendered. +If false or not provided, it may be rendered with a 'see more' option to expand and show the full text. +For AI Assistant apps, this allows the app to post long messages without users needing +to click 'see more' to expand the message.
+
+

Ancestors

+ +

Class variables

+
+
var fields_max_length
+
+

The type of the None singleton.

+
+
var text_max_length
+
+

The type of the None singleton.

+
+
var type
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
prop attributes : Set[str]
+
+
+ +Expand source code + +
@property
+def attributes(self) -> Set[str]:  # type: ignore[override]
+    return super().attributes.union({"text", "fields", "accessory", "expand"})
+
+

Build an unordered collection of unique elements.

+
+
+

Inherited members

+ +
+
+class TableBlock +(*,
rows: Sequence[Sequence[Dict[str, Any]]],
column_settings: Sequence[Dict[str, Any] | None] | None = None,
block_id: str | None = None,
**others: dict)
+
+
+
+ +Expand source code + +
class TableBlock(Block):
+    type = "table"
+
+    @property
+    def attributes(self) -> Set[str]:  # type: ignore[override]
+        return super().attributes.union({"rows", "column_settings"})
+
+    def __init__(
+        self,
+        *,
+        rows: Sequence[Sequence[Dict[str, Any]]],
+        column_settings: Optional[Sequence[Optional[Dict[str, Any]]]] = None,
+        block_id: Optional[str] = None,
+        **others: dict,
+    ):
+        """Displays structured information in a table.
+        https://docs.slack.dev/reference/block-kit/blocks/table-block
+
+        Args:
+            rows (required): An array consisting of table rows. Maximum 100 rows.
+                Each row object is an array with a max of 20 table cells.
+                Table cells can have a type of raw_text or rich_text.
+            column_settings: An array describing column behavior. If there are fewer items in the column_settings array
+                than there are columns in the table, then the items in the the column_settings array will describe
+                the same number of columns in the table as there are in the array itself.
+                Any additional columns will have the default behavior. Maximum 20 items.
+                See below for column settings schema.
+            block_id: A unique identifier for a block. If not specified, a block_id will be generated.
+                You can use this block_id when you receive an interaction payload to identify the source of the action.
+                Maximum length for this field is 255 characters.
+                block_id should be unique for each message and each iteration of a message.
+                If a message is updated, use a new block_id.
+        """
+        super().__init__(type=self.type, block_id=block_id)
+        show_unknown_key_warning(self, others)
+
+        self.rows = rows
+        self.column_settings = column_settings
+
+    @JsonValidator("rows attribute must be specified")
+    def _validate_rows(self):
+        return self.rows is not None and len(self.rows) > 0
+
+

Blocks are a series of components that can be combined +to create visually rich and compellingly interactive messages. +https://docs.slack.dev/reference/block-kit/blocks

+

Displays structured information in a table. +https://docs.slack.dev/reference/block-kit/blocks/table-block

+

Args

+
+
rows : required
+
An array consisting of table rows. Maximum 100 rows. +Each row object is an array with a max of 20 table cells. +Table cells can have a type of raw_text or rich_text.
+
column_settings
+
An array describing column behavior. If there are fewer items in the column_settings array +than there are columns in the table, then the items in the the column_settings array will describe +the same number of columns in the table as there are in the array itself. +Any additional columns will have the default behavior. Maximum 20 items. +See below for column settings schema.
+
block_id
+
A unique identifier for a block. If not specified, a block_id will be generated. +You can use this block_id when you receive an interaction payload to identify the source of the action. +Maximum length for this field is 255 characters. +block_id should be unique for each message and each iteration of a message. +If a message is updated, use a new block_id.
+
+

Ancestors

+ +

Class variables

+
+
var type
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
prop attributes : Set[str]
+
+
+ +Expand source code + +
@property
+def attributes(self) -> Set[str]:  # type: ignore[override]
+    return super().attributes.union({"rows", "column_settings"})
+
+

Build an unordered collection of unique elements.

+
+
+

Inherited members

+ +
+
+class VideoBlock +(*,
block_id: str | None = None,
alt_text: str | None = None,
video_url: str | None = None,
thumbnail_url: str | None = None,
title: str | dict | PlainTextObject | None = None,
title_url: str | None = None,
description: str | dict | PlainTextObject | None = None,
provider_icon_url: str | None = None,
provider_name: str | None = None,
author_name: str | None = None,
**others: dict)
+
+
+
+ +Expand source code + +
class VideoBlock(Block):
+    type = "video"
+    title_max_length = 200
+    author_name_max_length = 50
+
+    @property
+    def attributes(self) -> Set[str]:  # type: ignore[override]
+        return super().attributes.union(
+            {
+                "alt_text",
+                "video_url",
+                "thumbnail_url",
+                "title",
+                "title_url",
+                "description",
+                "provider_icon_url",
+                "provider_name",
+                "author_name",
+            }
+        )
+
+    def __init__(
+        self,
+        *,
+        block_id: Optional[str] = None,
+        alt_text: Optional[str] = None,
+        video_url: Optional[str] = None,
+        thumbnail_url: Optional[str] = None,
+        title: Optional[Union[str, dict, PlainTextObject]] = None,
+        title_url: Optional[str] = None,
+        description: Optional[Union[str, dict, PlainTextObject]] = None,
+        provider_icon_url: Optional[str] = None,
+        provider_name: Optional[str] = None,
+        author_name: Optional[str] = None,
+        **others: dict,
+    ):
+        """A video block is designed to embed videos in all app surfaces
+        (e.g. link unfurls, messages, modals, App Home) —
+        anywhere you can put blocks! To use the video block within your app,
+        you must have the links.embed:write scope.
+        https://docs.slack.dev/reference/block-kit/blocks/video-block
+
+        Args:
+            block_id: A string acting as a unique identifier for a block. If not specified, one will be generated.
+                Maximum length for this field is 255 characters.
+                block_id should be unique for each message and each iteration of a message.
+                If a message is updated, use a new block_id.
+            alt_text (required): A tooltip for the video. Required for accessibility
+            video_url (required): The URL to be embedded. Must match any existing unfurl domains within the app
+                and point to a HTTPS URL.
+            thumbnail_url (required): The thumbnail image URL
+            title (required): Video title in plain text format. Must be less than 200 characters.
+            title_url: Hyperlink for the title text. Must correspond to the non-embeddable URL for the video.
+                Must go to an HTTPS URL.
+            description: Description for video in plain text format.
+            provider_icon_url: Icon for the video provider - ex. Youtube icon
+            provider_name: The originating application or domain of the video ex. Youtube
+            author_name: Author name to be displayed. Must be less than 50 characters.
+        """
+        super().__init__(type=self.type, block_id=block_id)
+        show_unknown_key_warning(self, others)
+
+        self.alt_text = alt_text
+        self.video_url = video_url
+        self.thumbnail_url = thumbnail_url
+        self.title = TextObject.parse(title, default_type=PlainTextObject.type)  # type: ignore[arg-type]
+        self.title_url = title_url
+        self.description = TextObject.parse(description, default_type=PlainTextObject.type)  # type: ignore[arg-type]
+        self.provider_icon_url = provider_icon_url
+        self.provider_name = provider_name
+        self.author_name = author_name
+
+    @JsonValidator("alt_text attribute must be specified")
+    def _validate_alt_text(self):
+        return self.alt_text is not None
+
+    @JsonValidator("video_url attribute must be specified")
+    def _validate_video_url(self):
+        return self.video_url is not None
+
+    @JsonValidator("thumbnail_url attribute must be specified")
+    def _validate_thumbnail_url(self):
+        return self.thumbnail_url is not None
+
+    @JsonValidator("title attribute must be specified")
+    def _validate_title(self):
+        return self.title is not None
+
+    @JsonValidator(f"title attribute cannot exceed {title_max_length} characters")
+    def _validate_title_length(self):
+        return self.title is None or len(self.title.text) < self.title_max_length
+
+    @JsonValidator(f"author_name attribute cannot exceed {author_name_max_length} characters")
+    def _validate_author_name_length(self):
+        return self.author_name is None or len(self.author_name) < self.author_name_max_length
+
+

Blocks are a series of components that can be combined +to create visually rich and compellingly interactive messages. +https://docs.slack.dev/reference/block-kit/blocks

+

A video block is designed to embed videos in all app surfaces +(e.g. link unfurls, messages, modals, App Home) — +anywhere you can put blocks! To use the video block within your app, +you must have the links.embed:write scope. +https://docs.slack.dev/reference/block-kit/blocks/video-block

+

Args

+
+
block_id
+
A string acting as a unique identifier for a block. If not specified, one will be generated. +Maximum length for this field is 255 characters. +block_id should be unique for each message and each iteration of a message. +If a message is updated, use a new block_id.
+
alt_text : required
+
A tooltip for the video. Required for accessibility
+
video_url : required
+
The URL to be embedded. Must match any existing unfurl domains within the app +and point to a HTTPS URL.
+
thumbnail_url : required
+
The thumbnail image URL
+
title : required
+
Video title in plain text format. Must be less than 200 characters.
+
title_url
+
Hyperlink for the title text. Must correspond to the non-embeddable URL for the video. +Must go to an HTTPS URL.
+
description
+
Description for video in plain text format.
+
provider_icon_url
+
Icon for the video provider - ex. Youtube icon
+
provider_name
+
The originating application or domain of the video ex. Youtube
+
author_name
+
Author name to be displayed. Must be less than 50 characters.
+
+

Ancestors

+ +

Class variables

+
+
var author_name_max_length
+
+

The type of the None singleton.

+
+
var title_max_length
+
+

The type of the None singleton.

+
+
var type
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
prop attributes : Set[str]
+
+
+ +Expand source code + +
@property
+def attributes(self) -> Set[str]:  # type: ignore[override]
+    return super().attributes.union(
+        {
+            "alt_text",
+            "video_url",
+            "thumbnail_url",
+            "title",
+            "title_url",
+            "description",
+            "provider_icon_url",
+            "provider_name",
+            "author_name",
+        }
+    )
+
+

Build an unordered collection of unique elements.

+
+
+

Inherited members

+ +
+
+
+
+ +
+ + + diff --git a/docs/reference/models/blocks/index.html b/docs/reference/models/blocks/index.html new file mode 100644 index 000000000..0d2047c12 --- /dev/null +++ b/docs/reference/models/blocks/index.html @@ -0,0 +1,8566 @@ + + + + + + +slack_sdk.models.blocks API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.models.blocks

+
+
+

Block Kit data model objects

+

To learn more about Block Kit, please check the following resources and tools:

+ +
+
+

Sub-modules

+
+
slack_sdk.models.blocks.basic_components
+
+
+
+
slack_sdk.models.blocks.block_elements
+
+
+
+
slack_sdk.models.blocks.blocks
+
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class ActionsBlock +(*,
elements: Sequence[dict | InteractiveElement],
block_id: str | None = None,
**others: dict)
+
+
+
+ +Expand source code + +
class ActionsBlock(Block):
+    type = "actions"
+    elements_max_length = 25
+
+    @property
+    def attributes(self) -> Set[str]:  # type: ignore[override]
+        return super().attributes.union({"elements"})
+
+    def __init__(
+        self,
+        *,
+        elements: Sequence[Union[dict, InteractiveElement]],
+        block_id: Optional[str] = None,
+        **others: dict,
+    ):
+        """A block that is used to hold interactive elements.
+        https://docs.slack.dev/reference/block-kit/blocks/actions-block
+
+        Args:
+            elements (required): An array of interactive element objects - buttons, select menus, overflow menus,
+                or date pickers. There is a maximum of 25 elements in each action block.
+            block_id: A string acting as a unique identifier for a block.
+                If not specified, a block_id will be generated.
+                You can use this block_id when you receive an interaction payload to identify the source of the action.
+                Maximum length for this field is 255 characters.
+                block_id should be unique for each message and each iteration of a message.
+                If a message is updated, use a new block_id.
+        """
+        super().__init__(type=self.type, block_id=block_id)
+        show_unknown_key_warning(self, others)
+
+        self.elements = BlockElement.parse_all(elements)
+
+    @JsonValidator(f"elements attribute cannot exceed {elements_max_length} elements")
+    def _validate_elements_length(self):
+        return self.elements is None or len(self.elements) <= self.elements_max_length
+
+

Blocks are a series of components that can be combined +to create visually rich and compellingly interactive messages. +https://docs.slack.dev/reference/block-kit/blocks

+

A block that is used to hold interactive elements. +https://docs.slack.dev/reference/block-kit/blocks/actions-block

+

Args

+
+
elements : required
+
An array of interactive element objects - buttons, select menus, overflow menus, +or date pickers. There is a maximum of 25 elements in each action block.
+
block_id
+
A string acting as a unique identifier for a block. +If not specified, a block_id will be generated. +You can use this block_id when you receive an interaction payload to identify the source of the action. +Maximum length for this field is 255 characters. +block_id should be unique for each message and each iteration of a message. +If a message is updated, use a new block_id.
+
+

Ancestors

+ +

Class variables

+
+
var elements_max_length
+
+

The type of the None singleton.

+
+
var type
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
prop attributes : Set[str]
+
+
+ +Expand source code + +
@property
+def attributes(self) -> Set[str]:  # type: ignore[override]
+    return super().attributes.union({"elements"})
+
+

Build an unordered collection of unique elements.

+
+
+

Inherited members

+ +
+
+class Block +(*,
type: str | None = None,
subtype: str | None = None,
block_id: str | None = None)
+
+
+
+ +Expand source code + +
class Block(JsonObject):
+    """Blocks are a series of components that can be combined
+    to create visually rich and compellingly interactive messages.
+    https://docs.slack.dev/reference/block-kit/blocks
+    """
+
+    attributes = {"block_id", "type"}
+    block_id_max_length = 255
+    logger = logging.getLogger(__name__)
+
+    def _subtype_warning(self):
+        warnings.warn(
+            "subtype is deprecated since slackclient 2.6.0, use type instead",
+            DeprecationWarning,
+        )
+
+    @property
+    def subtype(self) -> Optional[str]:
+        return self.type
+
+    def __init__(
+        self,
+        *,
+        type: Optional[str] = None,
+        subtype: Optional[str] = None,  # deprecated
+        block_id: Optional[str] = None,
+    ):
+        if subtype:
+            self._subtype_warning()
+        self.type = type if type else subtype
+        self.block_id = block_id
+        self.color = None
+
+    @JsonValidator(f"block_id cannot exceed {block_id_max_length} characters")
+    def _validate_block_id_length(self):
+        return self.block_id is None or len(self.block_id) <= self.block_id_max_length
+
+    @classmethod
+    def parse(cls, block: Union[dict, "Block"]) -> Optional["Block"]:
+        if block is None:
+            return None
+        elif isinstance(block, Block):
+            return block
+        else:
+            if "type" in block:
+                type = block["type"]
+                if type == SectionBlock.type:
+                    return SectionBlock(**block)
+                elif type == DividerBlock.type:
+                    return DividerBlock(**block)
+                elif type == ImageBlock.type:
+                    return ImageBlock(**block)
+                elif type == ActionsBlock.type:
+                    return ActionsBlock(**block)
+                elif type == ContextBlock.type:
+                    return ContextBlock(**block)
+                elif type == ContextActionsBlock.type:
+                    return ContextActionsBlock(**block)
+                elif type == InputBlock.type:
+                    return InputBlock(**block)
+                elif type == FileBlock.type:
+                    return FileBlock(**block)
+                elif type == CallBlock.type:
+                    return CallBlock(**block)
+                elif type == HeaderBlock.type:
+                    return HeaderBlock(**block)
+                elif type == MarkdownBlock.type:
+                    return MarkdownBlock(**block)
+                elif type == VideoBlock.type:
+                    return VideoBlock(**block)
+                elif type == RichTextBlock.type:
+                    return RichTextBlock(**block)
+                elif type == TableBlock.type:
+                    return TableBlock(**block)
+                else:
+                    cls.logger.warning(f"Unknown block detected and skipped ({block})")
+                    return None
+            else:
+                cls.logger.warning(f"Unknown block detected and skipped ({block})")
+                return None
+
+    @classmethod
+    def parse_all(cls, blocks: Optional[Sequence[Union[dict, "Block"]]]) -> List["Block"]:
+        return [cls.parse(b) for b in blocks or []]  # type: ignore[misc]
+
+

Blocks are a series of components that can be combined +to create visually rich and compellingly interactive messages. +https://docs.slack.dev/reference/block-kit/blocks

+

Ancestors

+ +

Subclasses

+ +

Class variables

+
+
var attributes
+
+

The type of the None singleton.

+
+
var block_id_max_length
+
+

The type of the None singleton.

+
+
var logger
+
+

The type of the None singleton.

+
+
+

Static methods

+
+
+def parse(block: dict | ForwardRef('Block')) ‑> Block | None +
+
+
+
+
+def parse_all(blocks: Sequence[dict | ForwardRef('Block')] | None) ‑> List[Block] +
+
+
+
+
+

Instance variables

+
+
prop subtype : str | None
+
+
+ +Expand source code + +
@property
+def subtype(self) -> Optional[str]:
+    return self.type
+
+
+
+
+

Inherited members

+ +
+
+class BlockElement +(*, type: str | None = None, subtype: str | None = None, **others: dict) +
+
+
+ +Expand source code + +
class BlockElement(JsonObject, metaclass=ABCMeta):
+    """Block Elements are things that exists inside of your Blocks.
+    https://docs.slack.dev/reference/block-kit/block-elements/
+    """
+
+    attributes = {"type"}
+    logger = logging.getLogger(__name__)
+
+    def _subtype_warning(self):
+        warnings.warn(
+            "subtype is deprecated since slackclient 2.6.0, use type instead",
+            DeprecationWarning,
+        )
+
+    @property
+    def subtype(self) -> Optional[str]:
+        return self.type
+
+    def __init__(
+        self,
+        *,
+        type: Optional[str] = None,
+        subtype: Optional[str] = None,
+        **others: dict,
+    ):
+        if subtype:
+            self._subtype_warning()
+        self.type = type if type else subtype
+        show_unknown_key_warning(self, others)
+
+    @classmethod
+    def parse(cls, block_element: Union[dict, "BlockElement"]) -> Optional[Union["BlockElement", TextObject]]:
+        if block_element is None:
+            return None
+        elif isinstance(block_element, dict):
+            if "type" in block_element:
+                d = copy.copy(block_element)
+                t = d.pop("type")
+                for subclass in cls._get_sub_block_elements():
+                    if t == subclass.type:
+                        return subclass(**d)
+                if t == PlainTextObject.type:
+                    return PlainTextObject(**d)
+                elif t == MarkdownTextObject.type:
+                    return MarkdownTextObject(**d)
+        elif isinstance(block_element, (TextObject, BlockElement)):
+            return block_element
+        cls.logger.warning(f"Unknown element detected and skipped ({block_element})")
+        return None
+
+    @classmethod
+    def parse_all(
+        cls, block_elements: Sequence[Union[dict, "BlockElement", TextObject]]
+    ) -> List[Union["BlockElement", TextObject]]:
+        return [cls.parse(e) for e in block_elements or []]  # type: ignore[arg-type, misc]
+
+    @classmethod
+    def _get_sub_block_elements(cls: Type["BlockElement"]) -> Iterator[Type["BlockElement"]]:
+        for subclass in cls.__subclasses__():
+            if hasattr(subclass, "type"):
+                yield subclass
+            yield from subclass._get_sub_block_elements()
+
+

Block Elements are things that exists inside of your Blocks. +https://docs.slack.dev/reference/block-kit/block-elements/

+

Ancestors

+ +

Subclasses

+ +

Class variables

+
+
var attributes
+
+

The type of the None singleton.

+
+
var logger
+
+

The type of the None singleton.

+
+
+

Static methods

+
+
+def parse(block_element: dict | ForwardRef('BlockElement')) ‑> BlockElement | TextObject | None +
+
+
+
+
+def parse_all(block_elements: Sequence[dict | ForwardRef('BlockElement') | TextObject]) ‑> List[BlockElement | TextObject] +
+
+
+
+
+

Instance variables

+
+
prop subtype : str | None
+
+
+ +Expand source code + +
@property
+def subtype(self) -> Optional[str]:
+    return self.type
+
+
+
+
+

Inherited members

+ +
+
+class ButtonElement +(*,
text: str | dict | TextObject,
action_id: str | None = None,
url: str | None = None,
value: str | None = None,
style: str | None = None,
confirm: dict | ConfirmObject | None = None,
accessibility_label: str | None = None,
**others: dict)
+
+
+
+ +Expand source code + +
class ButtonElement(InteractiveElement):
+    type = "button"
+    text_max_length = 75
+    url_max_length = 3000
+    value_max_length = 2000
+
+    @property
+    def attributes(self) -> Set[str]:  # type: ignore[override]
+        return super().attributes.union({"text", "url", "value", "style", "confirm", "accessibility_label"})
+
+    def __init__(
+        self,
+        *,
+        text: Union[str, dict, TextObject],
+        action_id: Optional[str] = None,
+        url: Optional[str] = None,
+        value: Optional[str] = None,
+        style: Optional[str] = None,  # primary, danger
+        confirm: Optional[Union[dict, ConfirmObject]] = None,
+        accessibility_label: Optional[str] = None,
+        **others: dict,
+    ):
+        """An interactive element that inserts a button. The button can be a trigger for
+        anything from opening a simple link to starting a complex workflow.
+        https://docs.slack.dev/reference/block-kit/block-elements/button-element/
+
+        Args:
+            text (required): A text object that defines the button's text.
+                Can only be of type: plain_text.
+                Maximum length for the text in this field is 75 characters.
+            action_id (required): An identifier for this action.
+                You can use this when you receive an interaction payload to identify the source of the action.
+                Should be unique among all other action_ids in the containing block.
+                Maximum length for this field is 255 characters.
+            url: A URL to load in the user's browser when the button is clicked.
+                Maximum length for this field is 3000 characters.
+                If you're using url, you'll still receive an interaction payload
+                and will need to send an acknowledgement response.
+            value: The value to send along with the interaction payload.
+                Maximum length for this field is 2000 characters.
+            style: Decorates buttons with alternative visual color schemes. Use this option with restraint.
+                "primary" gives buttons a green outline and text, ideal for affirmation or confirmation actions.
+                "primary" should only be used for one button within a set.
+                "danger" gives buttons a red outline and text, and should be used when the action is destructive.
+                Use "danger" even more sparingly than "primary".
+                If you don't include this field, the default button style will be used.
+            confirm: A confirm object that defines an optional confirmation dialog after the button is clicked.
+            accessibility_label: A label for longer descriptive text about a button element.
+                This label will be read out by screen readers instead of the button text object.
+                Maximum length for this field is 75 characters.
+        """
+        super().__init__(action_id=action_id, type=self.type)
+        show_unknown_key_warning(self, others)
+
+        # NOTE: default_type=PlainTextObject.type here is only for backward-compatibility with version 2.5.0
+        self.text = TextObject.parse(text, default_type=PlainTextObject.type)
+        self.url = url
+        self.value = value
+        self.style = style
+        self.confirm = ConfirmObject.parse(confirm)  # type: ignore[arg-type]
+        self.accessibility_label = accessibility_label
+
+    @JsonValidator(f"text attribute cannot exceed {text_max_length} characters")
+    def _validate_text_length(self) -> bool:
+        return self.text is None or self.text.text is None or len(self.text.text) <= self.text_max_length
+
+    @JsonValidator(f"url attribute cannot exceed {url_max_length} characters")
+    def _validate_url_length(self) -> bool:
+        return self.url is None or len(self.url) <= self.url_max_length
+
+    @JsonValidator(f"value attribute cannot exceed {value_max_length} characters")
+    def _validate_value_length(self) -> bool:
+        return self.value is None or len(self.value) <= self.value_max_length
+
+    @EnumValidator("style", ButtonStyles)
+    def _validate_style_valid(self):
+        return self.style is None or self.style in ButtonStyles
+
+    @JsonValidator(f"accessibility_label attribute cannot exceed {text_max_length} characters")
+    def _validate_accessibility_label_length(self) -> bool:
+        return self.accessibility_label is None or len(self.accessibility_label) <= self.text_max_length
+
+

Block Elements are things that exists inside of your Blocks. +https://docs.slack.dev/reference/block-kit/block-elements/

+

An interactive element that inserts a button. The button can be a trigger for +anything from opening a simple link to starting a complex workflow. +https://docs.slack.dev/reference/block-kit/block-elements/button-element/

+

Args

+
+
text : required
+
A text object that defines the button's text. +Can only be of type: plain_text. +Maximum length for the text in this field is 75 characters.
+
action_id : required
+
An identifier for this action. +You can use this when you receive an interaction payload to identify the source of the action. +Should be unique among all other action_ids in the containing block. +Maximum length for this field is 255 characters.
+
url
+
A URL to load in the user's browser when the button is clicked. +Maximum length for this field is 3000 characters. +If you're using url, you'll still receive an interaction payload +and will need to send an acknowledgement response.
+
value
+
The value to send along with the interaction payload. +Maximum length for this field is 2000 characters.
+
style
+
Decorates buttons with alternative visual color schemes. Use this option with restraint. +"primary" gives buttons a green outline and text, ideal for affirmation or confirmation actions. +"primary" should only be used for one button within a set. +"danger" gives buttons a red outline and text, and should be used when the action is destructive. +Use "danger" even more sparingly than "primary". +If you don't include this field, the default button style will be used.
+
confirm
+
A confirm object that defines an optional confirmation dialog after the button is clicked.
+
accessibility_label
+
A label for longer descriptive text about a button element. +This label will be read out by screen readers instead of the button text object. +Maximum length for this field is 75 characters.
+
+

Ancestors

+ +

Subclasses

+ +

Class variables

+
+
var text_max_length
+
+

The type of the None singleton.

+
+
var type
+
+

The type of the None singleton.

+
+
var url_max_length
+
+

The type of the None singleton.

+
+
var value_max_length
+
+

The type of the None singleton.

+
+
+

Inherited members

+ +
+
+class CallBlock +(*,
call_id: str,
api_decoration_available: bool | None = None,
call: Dict[str, Dict[str, Any]] | None = None,
block_id: str | None = None,
**others: dict)
+
+
+
+ +Expand source code + +
class CallBlock(Block):
+    type = "call"
+
+    @property
+    def attributes(self) -> Set[str]:  # type: ignore[override]
+        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://docs.slack.dev/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
+
+

Blocks are a series of components that can be combined +to create visually rich and compellingly interactive messages. +https://docs.slack.dev/reference/block-kit/blocks

+

Displays a call information +https://docs.slack.dev/reference/block-kit/blocks#call

+

Ancestors

+ +

Class variables

+
+
var type
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
prop attributes : Set[str]
+
+
+ +Expand source code + +
@property
+def attributes(self) -> Set[str]:  # type: ignore[override]
+    return super().attributes.union({"call_id", "api_decoration_available", "call"})
+
+

Build an unordered collection of unique elements.

+
+
+

Inherited members

+ +
+
+class ChannelMultiSelectElement +(*,
placeholder: str | dict | TextObject | None = None,
action_id: str | None = None,
initial_channels: Sequence[str] | None = None,
confirm: dict | ConfirmObject | None = None,
max_selected_items: int | None = None,
focus_on_load: bool | None = None,
**others: dict)
+
+
+
+ +Expand source code + +
class ChannelMultiSelectElement(InputInteractiveElement):
+    type = "multi_channels_select"
+
+    @property
+    def attributes(self) -> Set[str]:  # type: ignore[override]
+        return super().attributes.union({"initial_channels", "max_selected_items"})
+
+    def __init__(
+        self,
+        *,
+        placeholder: Optional[Union[str, dict, TextObject]] = None,
+        action_id: Optional[str] = None,
+        initial_channels: Optional[Sequence[str]] = None,
+        confirm: Optional[Union[dict, ConfirmObject]] = None,
+        max_selected_items: Optional[int] = None,
+        focus_on_load: Optional[bool] = None,
+        **others: dict,
+    ):
+        """
+        This multi-select menu will populate its options with a list of public channels visible
+        to the current user in the active workspace.
+        https://docs.slack.dev/reference/block-kit/block-elements/multi-select-menu-element#channel_multi_select
+
+        Args:
+            placeholder (required): A plain_text only text object that defines the placeholder text shown on the menu.
+                Maximum length for the text in this field is 150 characters.
+            action_id (required): An identifier for the action triggered when a menu option is selected.
+                You can use this when you receive an interaction payload to identify the source of the action.
+                Should be unique among all other action_ids in the containing block.
+                Maximum length for this field is 255 characters.
+            initial_channels: An array of one or more IDs of any valid public channel
+                to be pre-selected when the menu loads.
+            confirm: A confirm object that defines an optional confirmation dialog that appears
+                before the multi-select choices are submitted.
+            max_selected_items: Specifies the maximum number of items that can be selected in the menu.
+                Minimum number is 1.
+            focus_on_load: Indicates whether the element will be set to auto focus within the view object.
+                Only one element can be set to true. Defaults to false.
+        """
+        super().__init__(
+            type=self.type,
+            action_id=action_id,
+            placeholder=TextObject.parse(placeholder, PlainTextObject.type),  # type: ignore[arg-type]
+            confirm=ConfirmObject.parse(confirm),  # type: ignore[arg-type]
+            focus_on_load=focus_on_load,
+        )
+        show_unknown_key_warning(self, others)
+
+        self.initial_channels = initial_channels
+        self.max_selected_items = max_selected_items
+
+

Block Elements are things that exists inside of your Blocks. +https://docs.slack.dev/reference/block-kit/block-elements/

+

This multi-select menu will populate its options with a list of public channels visible +to the current user in the active workspace. +https://docs.slack.dev/reference/block-kit/block-elements/multi-select-menu-element#channel_multi_select

+

Args

+
+
placeholder : required
+
A plain_text only text object that defines the placeholder text shown on the menu. +Maximum length for the text in this field is 150 characters.
+
action_id : required
+
An identifier for the action triggered when a menu option is selected. +You can use this when you receive an interaction payload to identify the source of the action. +Should be unique among all other action_ids in the containing block. +Maximum length for this field is 255 characters.
+
initial_channels
+
An array of one or more IDs of any valid public channel +to be pre-selected when the menu loads.
+
confirm
+
A confirm object that defines an optional confirmation dialog that appears +before the multi-select choices are submitted.
+
max_selected_items
+
Specifies the maximum number of items that can be selected in the menu. +Minimum number is 1.
+
focus_on_load
+
Indicates whether the element will be set to auto focus within the view object. +Only one element can be set to true. Defaults to false.
+
+

Ancestors

+ +

Class variables

+
+
var type
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
prop attributes : Set[str]
+
+
+ +Expand source code + +
@property
+def attributes(self) -> Set[str]:  # type: ignore[override]
+    return super().attributes.union({"initial_channels", "max_selected_items"})
+
+

Build an unordered collection of unique elements.

+
+
+

Inherited members

+ +
+
+class ChannelSelectElement +(*,
placeholder: str | dict | TextObject | None = None,
action_id: str | None = None,
initial_channel: str | None = None,
confirm: dict | ConfirmObject | None = None,
response_url_enabled: bool | None = None,
focus_on_load: bool | None = None,
**others: dict)
+
+
+
+ +Expand source code + +
class ChannelSelectElement(InputInteractiveElement):
+    type = "channels_select"
+
+    @property
+    def attributes(self) -> Set[str]:  # type: ignore[override]
+        return super().attributes.union({"initial_channel", "response_url_enabled"})
+
+    def __init__(
+        self,
+        *,
+        placeholder: Optional[Union[str, dict, TextObject]] = None,
+        action_id: Optional[str] = None,
+        initial_channel: Optional[str] = None,
+        confirm: Optional[Union[dict, ConfirmObject]] = None,
+        response_url_enabled: Optional[bool] = None,
+        focus_on_load: Optional[bool] = None,
+        **others: dict,
+    ):
+        """
+        This select menu will populate its options with a list of public channels
+        visible to the current user in the active workspace.
+        https://docs.slack.dev/reference/block-kit/block-elements/select-menu-element/#channels_select
+
+        Args:
+            placeholder (required): A plain_text only text object that defines the placeholder text shown on the menu.
+                Maximum length for the text in this field is 150 characters.
+            action_id (required): An identifier for the action triggered when a menu option is selected.
+                You can use this when you receive an interaction payload to identify the source of the action.
+                Should be unique among all other action_ids in the containing block.
+                Maximum length for this field is 255 characters.
+            initial_channel: The ID of any valid public channel to be pre-selected when the menu loads.
+            confirm: A confirm object that defines an optional confirmation dialog that appears
+                after a menu item is selected.
+            response_url_enabled: This field only works with menus in input blocks in modals.
+                When set to true, the view_submission payload from the menu's parent view will contain a response_url.
+                This response_url can be used for message responses.
+                The target channel for the message will be determined by the value of this select menu
+            focus_on_load: Indicates whether the element will be set to auto focus within the view object.
+                Only one element can be set to true. Defaults to false.
+        """
+        super().__init__(
+            type=self.type,
+            action_id=action_id,
+            placeholder=TextObject.parse(placeholder, PlainTextObject.type),  # type: ignore[arg-type]
+            confirm=ConfirmObject.parse(confirm),  # type: ignore[arg-type]
+            focus_on_load=focus_on_load,
+        )
+        show_unknown_key_warning(self, others)
+
+        self.initial_channel = initial_channel
+        self.response_url_enabled = response_url_enabled
+
+

Block Elements are things that exists inside of your Blocks. +https://docs.slack.dev/reference/block-kit/block-elements/

+

This select menu will populate its options with a list of public channels +visible to the current user in the active workspace. +https://docs.slack.dev/reference/block-kit/block-elements/select-menu-element/#channels_select

+

Args

+
+
placeholder : required
+
A plain_text only text object that defines the placeholder text shown on the menu. +Maximum length for the text in this field is 150 characters.
+
action_id : required
+
An identifier for the action triggered when a menu option is selected. +You can use this when you receive an interaction payload to identify the source of the action. +Should be unique among all other action_ids in the containing block. +Maximum length for this field is 255 characters.
+
initial_channel
+
The ID of any valid public channel to be pre-selected when the menu loads.
+
confirm
+
A confirm object that defines an optional confirmation dialog that appears +after a menu item is selected.
+
response_url_enabled
+
This field only works with menus in input blocks in modals. +When set to true, the view_submission payload from the menu's parent view will contain a response_url. +This response_url can be used for message responses. +The target channel for the message will be determined by the value of this select menu
+
focus_on_load
+
Indicates whether the element will be set to auto focus within the view object. +Only one element can be set to true. Defaults to false.
+
+

Ancestors

+ +

Class variables

+
+
var type
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
prop attributes : Set[str]
+
+
+ +Expand source code + +
@property
+def attributes(self) -> Set[str]:  # type: ignore[override]
+    return super().attributes.union({"initial_channel", "response_url_enabled"})
+
+

Build an unordered collection of unique elements.

+
+
+

Inherited members

+ +
+
+class CheckboxesElement +(*,
action_id: str | None = None,
options: Sequence[dict | Option] | None = None,
initial_options: Sequence[dict | Option] | None = None,
confirm: dict | ConfirmObject | None = None,
focus_on_load: bool | None = None,
**others: dict)
+
+
+
+ +Expand source code + +
class CheckboxesElement(InputInteractiveElement):
+    type = "checkboxes"
+
+    @property
+    def attributes(self) -> Set[str]:  # type: ignore[override]
+        return super().attributes.union({"options", "initial_options"})
+
+    def __init__(
+        self,
+        *,
+        action_id: Optional[str] = None,
+        options: Optional[Sequence[Union[dict, Option]]] = None,
+        initial_options: Optional[Sequence[Union[dict, Option]]] = None,
+        confirm: Optional[Union[dict, ConfirmObject]] = None,
+        focus_on_load: Optional[bool] = None,
+        **others: dict,
+    ):
+        """A checkbox group that allows a user to choose multiple items from a list of possible options.
+        https://docs.slack.dev/reference/block-kit/block-elements/checkboxes-element/
+
+        Args:
+            action_id (required): An identifier for the action triggered when the checkbox group is changed.
+                You can use this when you receive an interaction payload to identify the source of the action.
+                Should be unique among all other action_ids in the containing block.
+                Maximum length for this field is 255 characters.
+            options (required): An array of option objects. A maximum of 10 options are allowed.
+            initial_options: An array of option objects that exactly matches one or more of the options.
+                These options will be selected when the checkbox group initially loads.
+            confirm: A confirm object that defines an optional confirmation dialog that appears
+                after clicking one of the checkboxes in this element.
+            focus_on_load: Indicates whether the element will be set to auto focus within the view object.
+                Only one element can be set to true. Defaults to false.
+        """
+        super().__init__(
+            type=self.type,
+            action_id=action_id,
+            confirm=ConfirmObject.parse(confirm),  # type: ignore[arg-type]
+            focus_on_load=focus_on_load,
+        )
+        show_unknown_key_warning(self, others)
+
+        self.options = Option.parse_all(options)
+        self.initial_options = Option.parse_all(initial_options)
+
+

Block Elements are things that exists inside of your Blocks. +https://docs.slack.dev/reference/block-kit/block-elements/

+

A checkbox group that allows a user to choose multiple items from a list of possible options. +https://docs.slack.dev/reference/block-kit/block-elements/checkboxes-element/

+

Args

+
+
action_id : required
+
An identifier for the action triggered when the checkbox group is changed. +You can use this when you receive an interaction payload to identify the source of the action. +Should be unique among all other action_ids in the containing block. +Maximum length for this field is 255 characters.
+
options : required
+
An array of option objects. A maximum of 10 options are allowed.
+
initial_options
+
An array of option objects that exactly matches one or more of the options. +These options will be selected when the checkbox group initially loads.
+
confirm
+
A confirm object that defines an optional confirmation dialog that appears +after clicking one of the checkboxes in this element.
+
focus_on_load
+
Indicates whether the element will be set to auto focus within the view object. +Only one element can be set to true. Defaults to false.
+
+

Ancestors

+ +

Class variables

+
+
var type
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
prop attributes : Set[str]
+
+
+ +Expand source code + +
@property
+def attributes(self) -> Set[str]:  # type: ignore[override]
+    return super().attributes.union({"options", "initial_options"})
+
+

Build an unordered collection of unique elements.

+
+
+

Inherited members

+ +
+
+class ConfirmObject +(*,
title: str | Dict[str, Any] | PlainTextObject,
text: str | Dict[str, Any] | TextObject,
confirm: str | Dict[str, Any] | PlainTextObject = 'Yes',
deny: str | Dict[str, Any] | PlainTextObject = 'No',
style: str | None = None)
+
+
+
+ +Expand source code + +
class ConfirmObject(JsonObject):
+    attributes: Set[str] = set()
+
+    title_max_length = 100
+    text_max_length = 300
+    confirm_max_length = 30
+    deny_max_length = 30
+
+    @classmethod
+    def parse(cls, confirm: Union["ConfirmObject", Dict[str, Any]]):
+        if confirm:
+            if isinstance(confirm, ConfirmObject):
+                return confirm
+            elif isinstance(confirm, dict):
+                return ConfirmObject(**confirm)
+            else:
+                # Not yet implemented: show some warning here
+                return None
+        return None
+
+    def __init__(
+        self,
+        *,
+        title: Union[str, Dict[str, Any], PlainTextObject],
+        text: Union[str, Dict[str, Any], TextObject],
+        confirm: Union[str, Dict[str, Any], PlainTextObject] = "Yes",
+        deny: Union[str, Dict[str, Any], PlainTextObject] = "No",
+        style: Optional[str] = None,
+    ):
+        """
+        An object that defines a dialog that provides a confirmation step to any
+        interactive element. This dialog will ask the user to confirm their action by
+        offering a confirm and deny button.
+        https://docs.slack.dev/reference/block-kit/composition-objects/confirmation-dialog-object/
+        """
+        self._title = TextObject.parse(title, default_type=PlainTextObject.type)
+        self._text = TextObject.parse(text, default_type=MarkdownTextObject.type)
+        self._confirm = TextObject.parse(confirm, default_type=PlainTextObject.type)
+        self._deny = TextObject.parse(deny, default_type=PlainTextObject.type)
+        self._style = style
+
+        # for backward-compatibility with version 2.0-2.5, the following fields return str values
+        self.title = self._title.text if self._title else None
+        self.text = self._text.text if self._text else None
+        self.confirm = self._confirm.text if self._confirm else None
+        self.deny = self._deny.text if self._deny else None
+        self.style = self._style
+
+    @JsonValidator(f"title attribute cannot exceed {title_max_length} characters")
+    def title_length(self) -> bool:
+        return self._title is None or len(self._title.text) <= self.title_max_length
+
+    @JsonValidator(f"text attribute cannot exceed {text_max_length} characters")
+    def text_length(self) -> bool:
+        return self._text is None or len(self._text.text) <= self.text_max_length
+
+    @JsonValidator(f"confirm attribute cannot exceed {confirm_max_length} characters")
+    def confirm_length(self) -> bool:
+        return self._confirm is None or len(self._confirm.text) <= self.confirm_max_length
+
+    @JsonValidator(f"deny attribute cannot exceed {deny_max_length} characters")
+    def deny_length(self) -> bool:
+        return self._deny is None or len(self._deny.text) <= self.deny_max_length
+
+    @JsonValidator('style for confirm must be either "primary" or "danger"')
+    def _validate_confirm_style(self) -> bool:
+        return self._style is None or self._style in ["primary", "danger"]
+
+    def to_dict(self, option_type: str = "block") -> Dict[str, Any]:
+        if option_type == "action":
+            # deliberately skipping JSON validators here - can't find documentation
+            # on actual limits here
+            json: Dict[str, Union[str, dict]] = {
+                "ok_text": self._confirm.text if self._confirm and self._confirm.text != "Yes" else "Okay",
+                "dismiss_text": self._deny.text if self._deny and self._deny.text != "No" else "Cancel",
+            }
+            if self._title:
+                json["title"] = self._title.text
+            if self._text:
+                json["text"] = self._text.text
+            return json
+
+        else:
+            self.validate_json()
+            json = {}
+            if self._title:
+                json["title"] = self._title.to_dict()
+            if self._text:
+                json["text"] = self._text.to_dict()
+            if self._confirm:
+                json["confirm"] = self._confirm.to_dict()
+            if self._deny:
+                json["deny"] = self._deny.to_dict()
+            if self._style:
+                json["style"] = self._style
+            return json
+
+

The base class for JSON serializable class objects

+

An object that defines a dialog that provides a confirmation step to any +interactive element. This dialog will ask the user to confirm their action by +offering a confirm and deny button. +https://docs.slack.dev/reference/block-kit/composition-objects/confirmation-dialog-object/

+

Ancestors

+ +

Class variables

+
+
var attributes : Set[str]
+
+

The type of the None singleton.

+
+
var confirm_max_length
+
+

The type of the None singleton.

+
+
var deny_max_length
+
+

The type of the None singleton.

+
+
var text_max_length
+
+

The type of the None singleton.

+
+
var title_max_length
+
+

The type of the None singleton.

+
+
+

Static methods

+
+
+def parse(confirm: ForwardRef('ConfirmObject') | Dict[str, Any]) +
+
+
+
+
+

Methods

+
+
+def confirm_length(self) ‑> bool +
+
+
+ +Expand source code + +
@JsonValidator(f"confirm attribute cannot exceed {confirm_max_length} characters")
+def confirm_length(self) -> bool:
+    return self._confirm is None or len(self._confirm.text) <= self.confirm_max_length
+
+
+
+
+def deny_length(self) ‑> bool +
+
+
+ +Expand source code + +
@JsonValidator(f"deny attribute cannot exceed {deny_max_length} characters")
+def deny_length(self) -> bool:
+    return self._deny is None or len(self._deny.text) <= self.deny_max_length
+
+
+
+
+def text_length(self) ‑> bool +
+
+
+ +Expand source code + +
@JsonValidator(f"text attribute cannot exceed {text_max_length} characters")
+def text_length(self) -> bool:
+    return self._text is None or len(self._text.text) <= self.text_max_length
+
+
+
+
+def title_length(self) ‑> bool +
+
+
+ +Expand source code + +
@JsonValidator(f"title attribute cannot exceed {title_max_length} characters")
+def title_length(self) -> bool:
+    return self._title is None or len(self._title.text) <= self.title_max_length
+
+
+
+
+

Inherited members

+ +
+
+class ContextActionsBlock +(*,
elements: Sequence[dict | FeedbackButtonsElement | IconButtonElement],
block_id: str | None = None,
**others: dict)
+
+
+
+ +Expand source code + +
class ContextActionsBlock(Block):
+    type = "context_actions"
+    elements_max_length = 5
+
+    @property
+    def attributes(self) -> Set[str]:  # type: ignore[override]
+        return super().attributes.union({"elements"})
+
+    def __init__(
+        self,
+        *,
+        elements: Sequence[Union[dict, FeedbackButtonsElement, IconButtonElement]],
+        block_id: Optional[str] = None,
+        **others: dict,
+    ):
+        """Displays actions as contextual info, which can include both feedback buttons and icon buttons.
+        https://docs.slack.dev/reference/block-kit/blocks/context-actions-block
+
+        Args:
+            elements (required): An array of feedback_buttons or icon_button block elements. Maximum number of items is 5.
+            block_id: A string acting as a unique identifier for a block. If not specified, one will be generated.
+                Maximum length for this field is 255 characters.
+                block_id should be unique for each message and each iteration of a message.
+                If a message is updated, use a new block_id.
+        """
+        super().__init__(type=self.type, block_id=block_id)
+        show_unknown_key_warning(self, others)
+
+        self.elements = BlockElement.parse_all(elements)
+
+    @JsonValidator("elements attribute must be specified")
+    def _validate_elements(self):
+        return self.elements is None or len(self.elements) > 0
+
+    @JsonValidator(f"elements attribute cannot exceed {elements_max_length} elements")
+    def _validate_elements_length(self):
+        return self.elements is None or len(self.elements) <= self.elements_max_length
+
+

Blocks are a series of components that can be combined +to create visually rich and compellingly interactive messages. +https://docs.slack.dev/reference/block-kit/blocks

+

Displays actions as contextual info, which can include both feedback buttons and icon buttons. +https://docs.slack.dev/reference/block-kit/blocks/context-actions-block

+

Args

+
+
elements : required
+
An array of feedback_buttons or icon_button block elements. Maximum number of items is 5.
+
block_id
+
A string acting as a unique identifier for a block. If not specified, one will be generated. +Maximum length for this field is 255 characters. +block_id should be unique for each message and each iteration of a message. +If a message is updated, use a new block_id.
+
+

Ancestors

+ +

Class variables

+
+
var elements_max_length
+
+

The type of the None singleton.

+
+
var type
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
prop attributes : Set[str]
+
+
+ +Expand source code + +
@property
+def attributes(self) -> Set[str]:  # type: ignore[override]
+    return super().attributes.union({"elements"})
+
+

Build an unordered collection of unique elements.

+
+
+

Inherited members

+ +
+
+class ContextBlock +(*,
elements: Sequence[dict | ImageElement | TextObject],
block_id: str | None = None,
**others: dict)
+
+
+
+ +Expand source code + +
class ContextBlock(Block):
+    type = "context"
+    elements_max_length = 10
+
+    @property
+    def attributes(self) -> Set[str]:  # type: ignore[override]
+        return super().attributes.union({"elements"})
+
+    def __init__(
+        self,
+        *,
+        elements: Sequence[Union[dict, ImageElement, TextObject]],
+        block_id: Optional[str] = None,
+        **others: dict,
+    ):
+        """Displays message context, which can include both images and text.
+        https://docs.slack.dev/reference/block-kit/blocks/context-block
+
+        Args:
+            elements (required): An array of image elements and text objects. Maximum number of items is 10.
+            block_id: A string acting as a unique identifier for a block. If not specified, one will be generated.
+                Maximum length for this field is 255 characters.
+                block_id should be unique for each message and each iteration of a message.
+                If a message is updated, use a new block_id.
+        """
+        super().__init__(type=self.type, block_id=block_id)
+        show_unknown_key_warning(self, others)
+
+        self.elements = BlockElement.parse_all(elements)
+
+    @JsonValidator(f"elements attribute cannot exceed {elements_max_length} elements")
+    def _validate_elements_length(self):
+        return self.elements is None or len(self.elements) <= self.elements_max_length
+
+

Blocks are a series of components that can be combined +to create visually rich and compellingly interactive messages. +https://docs.slack.dev/reference/block-kit/blocks

+

Displays message context, which can include both images and text. +https://docs.slack.dev/reference/block-kit/blocks/context-block

+

Args

+
+
elements : required
+
An array of image elements and text objects. Maximum number of items is 10.
+
block_id
+
A string acting as a unique identifier for a block. If not specified, one will be generated. +Maximum length for this field is 255 characters. +block_id should be unique for each message and each iteration of a message. +If a message is updated, use a new block_id.
+
+

Ancestors

+ +

Class variables

+
+
var elements_max_length
+
+

The type of the None singleton.

+
+
var type
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
prop attributes : Set[str]
+
+
+ +Expand source code + +
@property
+def attributes(self) -> Set[str]:  # type: ignore[override]
+    return super().attributes.union({"elements"})
+
+

Build an unordered collection of unique elements.

+
+
+

Inherited members

+ +
+
+class ConversationFilter +(*,
include: Sequence[str] | None = None,
exclude_bot_users: bool | None = None,
exclude_external_shared_channels: bool | None = None)
+
+
+
+ +Expand source code + +
class ConversationFilter(JsonObject):
+    attributes = {"include", "exclude_bot_users", "exclude_external_shared_channels"}
+    logger = logging.getLogger(__name__)
+
+    def __init__(
+        self,
+        *,
+        include: Optional[Sequence[str]] = None,
+        exclude_bot_users: Optional[bool] = None,
+        exclude_external_shared_channels: Optional[bool] = None,
+    ):
+        """Provides a way to filter the list of options in a conversations select menu
+        or conversations multi-select menu.
+        https://docs.slack.dev/reference/block-kit/composition-objects/conversation-filter-object
+
+        Args:
+            include: Indicates which type of conversations should be included in the list.
+                When this field is provided, any conversations that do not match will be excluded.
+                You should provide an array of strings from the following options:
+                "im", "mpim", "private", and "public". The array cannot be empty.
+            exclude_bot_users: Indicates whether to exclude bot users from conversation lists. Defaults to false.
+            exclude_external_shared_channels: Indicates whether to exclude external shared channels
+                from conversation lists. Defaults to false.
+        """
+        self.include = include
+        self.exclude_bot_users = exclude_bot_users
+        self.exclude_external_shared_channels = exclude_external_shared_channels
+
+    @classmethod
+    def parse(cls, filter: Union[dict, "ConversationFilter"]):
+        if filter is None:
+            return None
+        elif isinstance(filter, ConversationFilter):
+            return filter
+        elif isinstance(filter, dict):
+            d = copy.copy(filter)
+            return ConversationFilter(**d)
+        else:
+            cls.logger.warning(f"Unknown conversation filter object detected and skipped ({filter})")
+            return None
+
+

The base class for JSON serializable class objects

+

Provides a way to filter the list of options in a conversations select menu +or conversations multi-select menu. +https://docs.slack.dev/reference/block-kit/composition-objects/conversation-filter-object

+

Args

+
+
include
+
Indicates which type of conversations should be included in the list. +When this field is provided, any conversations that do not match will be excluded. +You should provide an array of strings from the following options: +"im", "mpim", "private", and "public". The array cannot be empty.
+
exclude_bot_users
+
Indicates whether to exclude bot users from conversation lists. Defaults to false.
+
exclude_external_shared_channels
+
Indicates whether to exclude external shared channels +from conversation lists. Defaults to false.
+
+

Ancestors

+ +

Class variables

+
+
var attributes
+
+

The type of the None singleton.

+
+
var logger
+
+

The type of the None singleton.

+
+
+

Static methods

+
+
+def parse(filter: dict | ForwardRef('ConversationFilter')) +
+
+
+
+
+

Inherited members

+ +
+
+class ConversationMultiSelectElement +(*,
placeholder: str | dict | TextObject | None = None,
action_id: str | None = None,
initial_conversations: Sequence[str] | None = None,
confirm: dict | ConfirmObject | None = None,
max_selected_items: int | None = None,
default_to_current_conversation: bool | None = None,
filter: dict | ConversationFilter | None = None,
focus_on_load: bool | None = None,
**others: dict)
+
+
+
+ +Expand source code + +
class ConversationMultiSelectElement(InputInteractiveElement):
+    type = "multi_conversations_select"
+
+    @property
+    def attributes(self) -> Set[str]:  # type: ignore[override]
+        return super().attributes.union(
+            {
+                "initial_conversations",
+                "max_selected_items",
+                "default_to_current_conversation",
+                "filter",
+            }
+        )
+
+    def __init__(
+        self,
+        *,
+        placeholder: Optional[Union[str, dict, TextObject]] = None,
+        action_id: Optional[str] = None,
+        initial_conversations: Optional[Sequence[str]] = None,
+        confirm: Optional[Union[dict, ConfirmObject]] = None,
+        max_selected_items: Optional[int] = None,
+        default_to_current_conversation: Optional[bool] = None,
+        filter: Optional[Union[dict, ConversationFilter]] = None,
+        focus_on_load: Optional[bool] = None,
+        **others: dict,
+    ):
+        """
+        This multi-select menu will populate its options with a list of public and private channels,
+        DMs, and MPIMs visible to the current user in the active workspace.
+        https://docs.slack.dev/reference/block-kit/block-elements/multi-select-menu-element/#conversation_multi_select
+
+        Args:
+            placeholder (required): A plain_text only text object that defines the placeholder text shown on the menu.
+                Maximum length for the text in this field is 150 characters.
+            action_id (required): An identifier for the action triggered when a menu option is selected.
+                You can use this when you receive an interaction payload to identify the source of the action.
+                Should be unique among all other action_ids in the containing block.
+                Maximum length for this field is 255 characters.
+            initial_conversations: An array of one or more IDs of any valid conversations to be pre-selected
+                when the menu loads. If default_to_current_conversation is also supplied,
+                initial_conversations will be ignored.
+            confirm: A confirm object that defines an optional confirmation dialog that appears
+                before the multi-select choices are submitted.
+            max_selected_items: Specifies the maximum number of items that can be selected in the menu.
+                Minimum number is 1.
+            default_to_current_conversation: Pre-populates the select menu with the conversation that
+                the user was viewing when they opened the modal, if available. Default is false.
+            filter: A filter object that reduces the list of available conversations using the specified criteria.
+            focus_on_load: Indicates whether the element will be set to auto focus within the view object.
+                Only one element can be set to true. Defaults to false.
+        """
+        super().__init__(
+            type=self.type,
+            action_id=action_id,
+            placeholder=TextObject.parse(placeholder, PlainTextObject.type),  # type: ignore[arg-type]
+            confirm=ConfirmObject.parse(confirm),  # type: ignore[arg-type]
+            focus_on_load=focus_on_load,
+        )
+        show_unknown_key_warning(self, others)
+
+        self.initial_conversations = initial_conversations
+        self.max_selected_items = max_selected_items
+        self.default_to_current_conversation = default_to_current_conversation
+        self.filter = ConversationFilter.parse(filter)  # type: ignore[arg-type]
+
+

Block Elements are things that exists inside of your Blocks. +https://docs.slack.dev/reference/block-kit/block-elements/

+

This multi-select menu will populate its options with a list of public and private channels, +DMs, and MPIMs visible to the current user in the active workspace. +https://docs.slack.dev/reference/block-kit/block-elements/multi-select-menu-element/#conversation_multi_select

+

Args

+
+
placeholder : required
+
A plain_text only text object that defines the placeholder text shown on the menu. +Maximum length for the text in this field is 150 characters.
+
action_id : required
+
An identifier for the action triggered when a menu option is selected. +You can use this when you receive an interaction payload to identify the source of the action. +Should be unique among all other action_ids in the containing block. +Maximum length for this field is 255 characters.
+
initial_conversations
+
An array of one or more IDs of any valid conversations to be pre-selected +when the menu loads. If default_to_current_conversation is also supplied, +initial_conversations will be ignored.
+
confirm
+
A confirm object that defines an optional confirmation dialog that appears +before the multi-select choices are submitted.
+
max_selected_items
+
Specifies the maximum number of items that can be selected in the menu. +Minimum number is 1.
+
default_to_current_conversation
+
Pre-populates the select menu with the conversation that +the user was viewing when they opened the modal, if available. Default is false.
+
filter
+
A filter object that reduces the list of available conversations using the specified criteria.
+
focus_on_load
+
Indicates whether the element will be set to auto focus within the view object. +Only one element can be set to true. Defaults to false.
+
+

Ancestors

+ +

Class variables

+
+
var type
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
prop attributes : Set[str]
+
+
+ +Expand source code + +
@property
+def attributes(self) -> Set[str]:  # type: ignore[override]
+    return super().attributes.union(
+        {
+            "initial_conversations",
+            "max_selected_items",
+            "default_to_current_conversation",
+            "filter",
+        }
+    )
+
+

Build an unordered collection of unique elements.

+
+
+

Inherited members

+ +
+
+class ConversationSelectElement +(*,
placeholder: str | dict | TextObject | None = None,
action_id: str | None = None,
initial_conversation: str | None = None,
confirm: dict | ConfirmObject | None = None,
response_url_enabled: bool | None = None,
default_to_current_conversation: bool | None = None,
filter: ConversationFilter | None = None,
focus_on_load: bool | None = None,
**others: dict)
+
+
+
+ +Expand source code + +
class ConversationSelectElement(InputInteractiveElement):
+    type = "conversations_select"
+
+    @property
+    def attributes(self) -> Set[str]:  # type: ignore[override]
+        return super().attributes.union(
+            {
+                "initial_conversation",
+                "response_url_enabled",
+                "filter",
+                "default_to_current_conversation",
+            }
+        )
+
+    def __init__(
+        self,
+        *,
+        placeholder: Optional[Union[str, dict, TextObject]] = None,
+        action_id: Optional[str] = None,
+        initial_conversation: Optional[str] = None,
+        confirm: Optional[Union[dict, ConfirmObject]] = None,
+        response_url_enabled: Optional[bool] = None,
+        default_to_current_conversation: Optional[bool] = None,
+        filter: Optional[ConversationFilter] = None,
+        focus_on_load: Optional[bool] = None,
+        **others: dict,
+    ):
+        """
+        This select menu will populate its options with a list of public and private
+        channels, DMs, and MPIMs visible to the current user in the active workspace.
+        https://docs.slack.dev/reference/block-kit/block-elements/select-menu-element/#conversations_select
+
+        Args:
+            placeholder (required): A plain_text only text object that defines the placeholder text shown on the menu.
+                Maximum length for the text in this field is 150 characters.
+            action_id (required): An identifier for the action triggered when a menu option is selected.
+                You can use this when you receive an interaction payload to identify the source of the action.
+                Should be unique among all other action_ids in the containing block.
+                Maximum length for this field is 255 characters.
+            initial_conversation: The ID of any valid conversation to be pre-selected when the menu loads.
+                If default_to_current_conversation is also supplied, initial_conversation will take precedence.
+            confirm: A confirm object that defines an optional confirmation dialog
+                that appears after a menu item is selected.
+            response_url_enabled: This field only works with menus in input blocks in modals.
+                When set to true, the view_submission payload from the menu's parent view will contain a response_url.
+                This response_url can be used for message responses. The target conversation for the message
+                will be determined by the value of this select menu.
+            default_to_current_conversation: Pre-populates the select menu with the conversation
+                that the user was viewing when they opened the modal, if available. Default is false.
+            filter: A filter object that reduces the list of available conversations using the specified criteria.
+            focus_on_load: Indicates whether the element will be set to auto focus within the view object.
+                Only one element can be set to true. Defaults to false.
+        """
+        super().__init__(
+            type=self.type,
+            action_id=action_id,
+            placeholder=TextObject.parse(placeholder, PlainTextObject.type),  # type: ignore[arg-type]
+            confirm=ConfirmObject.parse(confirm),  # type: ignore[arg-type]
+            focus_on_load=focus_on_load,
+        )
+        show_unknown_key_warning(self, others)
+
+        self.initial_conversation = initial_conversation
+        self.response_url_enabled = response_url_enabled
+        self.default_to_current_conversation = default_to_current_conversation
+        self.filter = filter
+
+

Block Elements are things that exists inside of your Blocks. +https://docs.slack.dev/reference/block-kit/block-elements/

+

This select menu will populate its options with a list of public and private +channels, DMs, and MPIMs visible to the current user in the active workspace. +https://docs.slack.dev/reference/block-kit/block-elements/select-menu-element/#conversations_select

+

Args

+
+
placeholder : required
+
A plain_text only text object that defines the placeholder text shown on the menu. +Maximum length for the text in this field is 150 characters.
+
action_id : required
+
An identifier for the action triggered when a menu option is selected. +You can use this when you receive an interaction payload to identify the source of the action. +Should be unique among all other action_ids in the containing block. +Maximum length for this field is 255 characters.
+
initial_conversation
+
The ID of any valid conversation to be pre-selected when the menu loads. +If default_to_current_conversation is also supplied, initial_conversation will take precedence.
+
confirm
+
A confirm object that defines an optional confirmation dialog +that appears after a menu item is selected.
+
response_url_enabled
+
This field only works with menus in input blocks in modals. +When set to true, the view_submission payload from the menu's parent view will contain a response_url. +This response_url can be used for message responses. The target conversation for the message +will be determined by the value of this select menu.
+
default_to_current_conversation
+
Pre-populates the select menu with the conversation +that the user was viewing when they opened the modal, if available. Default is false.
+
filter
+
A filter object that reduces the list of available conversations using the specified criteria.
+
focus_on_load
+
Indicates whether the element will be set to auto focus within the view object. +Only one element can be set to true. Defaults to false.
+
+

Ancestors

+ +

Class variables

+
+
var type
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
prop attributes : Set[str]
+
+
+ +Expand source code + +
@property
+def attributes(self) -> Set[str]:  # type: ignore[override]
+    return super().attributes.union(
+        {
+            "initial_conversation",
+            "response_url_enabled",
+            "filter",
+            "default_to_current_conversation",
+        }
+    )
+
+

Build an unordered collection of unique elements.

+
+
+

Inherited members

+ +
+
+class DatePickerElement +(*,
action_id: str | None = None,
placeholder: str | dict | TextObject | None = None,
initial_date: str | None = None,
confirm: dict | ConfirmObject | None = None,
focus_on_load: bool | None = None,
**others: dict)
+
+
+
+ +Expand source code + +
class DatePickerElement(InputInteractiveElement):
+    type = "datepicker"
+
+    @property
+    def attributes(self) -> Set[str]:  # type: ignore[override]
+        return super().attributes.union({"initial_date"})
+
+    def __init__(
+        self,
+        *,
+        action_id: Optional[str] = None,
+        placeholder: Optional[Union[str, dict, TextObject]] = None,
+        initial_date: Optional[str] = None,
+        confirm: Optional[Union[dict, ConfirmObject]] = None,
+        focus_on_load: Optional[bool] = None,
+        **others: dict,
+    ):
+        """
+        An element which lets users easily select a date from a calendar style UI.
+        Date picker elements can be used inside of SectionBlocks and ActionsBlocks.
+        https://docs.slack.dev/reference/block-kit/block-elements/date-picker-element
+
+        Args:
+            action_id (required): An identifier for the action triggered when a menu option is selected.
+                You can use this when you receive an interaction payload to identify the source of the action.
+                Should be unique among all other action_ids in the containing block.
+                Maximum length for this field is 255 characters.
+            placeholder: A plain_text only text object that defines the placeholder text shown on the datepicker.
+                Maximum length for the text in this field is 150 characters.
+            initial_date: The initial date that is selected when the element is loaded.
+                This should be in the format YYYY-MM-DD.
+            confirm: A confirm object that defines an optional confirmation dialog
+                that appears after a date is selected.
+            focus_on_load: Indicates whether the element will be set to auto focus within the view object.
+                Only one element can be set to true. Defaults to false.
+        """
+        super().__init__(
+            type=self.type,
+            action_id=action_id,
+            placeholder=TextObject.parse(placeholder, PlainTextObject.type),  # type: ignore[arg-type]
+            confirm=ConfirmObject.parse(confirm),  # type: ignore[arg-type]
+            focus_on_load=focus_on_load,
+        )
+        show_unknown_key_warning(self, others)
+
+        self.initial_date = initial_date
+
+    @JsonValidator("initial_date attribute must be in format 'YYYY-MM-DD'")
+    def _validate_initial_date_valid(self) -> bool:
+        return (
+            self.initial_date is None
+            or re.match(r"\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])", self.initial_date) is not None
+        )
+
+

Block Elements are things that exists inside of your Blocks. +https://docs.slack.dev/reference/block-kit/block-elements/

+

An element which lets users easily select a date from a calendar style UI. +Date picker elements can be used inside of SectionBlocks and ActionsBlocks. +https://docs.slack.dev/reference/block-kit/block-elements/date-picker-element

+

Args

+
+
action_id : required
+
An identifier for the action triggered when a menu option is selected. +You can use this when you receive an interaction payload to identify the source of the action. +Should be unique among all other action_ids in the containing block. +Maximum length for this field is 255 characters.
+
placeholder
+
A plain_text only text object that defines the placeholder text shown on the datepicker. +Maximum length for the text in this field is 150 characters.
+
initial_date
+
The initial date that is selected when the element is loaded. +This should be in the format YYYY-MM-DD.
+
confirm
+
A confirm object that defines an optional confirmation dialog +that appears after a date is selected.
+
focus_on_load
+
Indicates whether the element will be set to auto focus within the view object. +Only one element can be set to true. Defaults to false.
+
+

Ancestors

+ +

Class variables

+
+
var type
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
prop attributes : Set[str]
+
+
+ +Expand source code + +
@property
+def attributes(self) -> Set[str]:  # type: ignore[override]
+    return super().attributes.union({"initial_date"})
+
+

Build an unordered collection of unique elements.

+
+
+

Inherited members

+ +
+
+class DateTimePickerElement +(*,
action_id: str | None = None,
initial_date_time: int | None = None,
confirm: dict | ConfirmObject | None = None,
focus_on_load: bool | None = None,
**others: dict)
+
+
+
+ +Expand source code + +
class DateTimePickerElement(InputInteractiveElement):
+    type = "datetimepicker"
+
+    @property
+    def attributes(self) -> Set[str]:  # type: ignore[override]
+        return super().attributes.union({"initial_date_time"})
+
+    def __init__(
+        self,
+        *,
+        action_id: Optional[str] = None,
+        initial_date_time: Optional[int] = None,
+        confirm: Optional[Union[dict, ConfirmObject]] = None,
+        focus_on_load: Optional[bool] = None,
+        **others: dict,
+    ):
+        """
+        An element that allows the selection of a time of day formatted as a UNIX timestamp.
+        On desktop clients, this time picker will take the form of a dropdown list and the
+        date picker will take the form of a dropdown calendar. Both options will have free-text
+        entry for precise choices. On mobile clients, the time picker and date
+        picker will use native UIs.
+        https://docs.slack.dev/reference/block-kit/block-elements/date-picker-element/
+
+        Args:
+            action_id (required): An identifier for the action triggered when a time is selected. You can use this
+                when you receive an interaction payload to identify the source of the action. Should be unique among
+                all other action_ids in the containing block. Maximum length for this field is 255 characters.
+            initial_date_time: The initial date and time that is selected when the element is loaded, represented as
+                a UNIX timestamp in seconds. This should be in the format of 10 digits, for example 1628633820
+                represents the date and time August 10th, 2021 at 03:17pm PST.
+                and mm is minutes with leading zeros (00 to 59), for example 22:25 for 10:25pm.
+            confirm: A confirm object that defines an optional confirmation dialog
+                that appears after a time is selected.
+            focus_on_load: Indicates whether the element will be set to auto focus within the view object.
+                Only one element can be set to true. Defaults to false.
+        """
+        super().__init__(
+            type=self.type,
+            action_id=action_id,
+            confirm=ConfirmObject.parse(confirm),  # type: ignore[arg-type]
+            focus_on_load=focus_on_load,
+        )
+        show_unknown_key_warning(self, others)
+
+        self.initial_date_time = initial_date_time
+
+    @JsonValidator("initial_date_time attribute must be between 0 and 99999999 seconds")
+    def _validate_initial_date_time_valid(self) -> bool:
+        return self.initial_date_time is None or (0 <= self.initial_date_time <= 9999999999)
+
+

Block Elements are things that exists inside of your Blocks. +https://docs.slack.dev/reference/block-kit/block-elements/

+

An element that allows the selection of a time of day formatted as a UNIX timestamp. +On desktop clients, this time picker will take the form of a dropdown list and the +date picker will take the form of a dropdown calendar. Both options will have free-text +entry for precise choices. On mobile clients, the time picker and date +picker will use native UIs. +https://docs.slack.dev/reference/block-kit/block-elements/date-picker-element/

+

Args

+
+
action_id : required
+
An identifier for the action triggered when a time is selected. You can use this +when you receive an interaction payload to identify the source of the action. Should be unique among +all other action_ids in the containing block. Maximum length for this field is 255 characters.
+
initial_date_time
+
The initial date and time that is selected when the element is loaded, represented as +a UNIX timestamp in seconds. This should be in the format of 10 digits, for example 1628633820 +represents the date and time August 10th, 2021 at 03:17pm PST. +and mm is minutes with leading zeros (00 to 59), for example 22:25 for 10:25pm.
+
confirm
+
A confirm object that defines an optional confirmation dialog +that appears after a time is selected.
+
focus_on_load
+
Indicates whether the element will be set to auto focus within the view object. +Only one element can be set to true. Defaults to false.
+
+

Ancestors

+ +

Class variables

+
+
var type
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
prop attributes : Set[str]
+
+
+ +Expand source code + +
@property
+def attributes(self) -> Set[str]:  # type: ignore[override]
+    return super().attributes.union({"initial_date_time"})
+
+

Build an unordered collection of unique elements.

+
+
+

Inherited members

+ +
+
+class DividerBlock +(*, block_id: str | None = None, **others: dict) +
+
+
+ +Expand source code + +
class DividerBlock(Block):
+    type = "divider"
+
+    def __init__(
+        self,
+        *,
+        block_id: Optional[str] = None,
+        **others: dict,
+    ):
+        """A content divider, like an <hr>, to split up different blocks inside of a message.
+        https://docs.slack.dev/reference/block-kit/blocks/divider-block
+
+        Args:
+            block_id: A string acting as a unique identifier for a block. If not specified, one will be generated.
+                You can use this block_id when you receive an interaction payload to identify the source of the action.
+                Maximum length for this field is 255 characters.
+                block_id should be unique for each message and each iteration of a message.
+                If a message is updated, use a new block_id.
+        """
+        super().__init__(type=self.type, block_id=block_id)
+        show_unknown_key_warning(self, others)
+
+

Blocks are a series of components that can be combined +to create visually rich and compellingly interactive messages. +https://docs.slack.dev/reference/block-kit/blocks

+

A content divider, like an


, to split up different blocks inside of a message. +https://docs.slack.dev/reference/block-kit/blocks/divider-block

+

Args

+
+
block_id
+
A string acting as a unique identifier for a block. If not specified, one will be generated. +You can use this block_id when you receive an interaction payload to identify the source of the action. +Maximum length for this field is 255 characters. +block_id should be unique for each message and each iteration of a message. +If a message is updated, use a new block_id.
+
+

Ancestors

+ +

Class variables

+
+
var type
+
+

The type of the None singleton.

+
+
+

Inherited members

+ +
+
+class EmailInputElement +(*,
action_id: str | None = None,
initial_value: str | None = None,
dispatch_action_config: dict | DispatchActionConfig | None = None,
focus_on_load: bool | None = None,
placeholder: str | dict | TextObject | None = None,
**others: dict)
+
+
+
+ +Expand source code + +
class EmailInputElement(InputInteractiveElement):
+    type = "email_text_input"
+
+    @property
+    def attributes(self) -> Set[str]:  # type: ignore[override]
+        return super().attributes.union(
+            {
+                "initial_value",
+                "dispatch_action_config",
+            }
+        )
+
+    def __init__(
+        self,
+        *,
+        action_id: Optional[str] = None,
+        initial_value: Optional[str] = None,
+        dispatch_action_config: Optional[Union[dict, DispatchActionConfig]] = None,
+        focus_on_load: Optional[bool] = None,
+        placeholder: Optional[Union[str, dict, TextObject]] = None,
+        **others: dict,
+    ):
+        """
+        https://docs.slack.dev/reference/block-kit/block-elements/email-input-element
+
+        Args:
+            action_id (required): An identifier for the input value when the parent modal is submitted.
+                You can use this when you receive a view_submission payload to identify the value of the input element.
+                Should be unique among all other action_ids in the containing block.
+                Maximum length for this field is 255 characters.
+            initial_value: The initial value in the email input when it is loaded.
+            dispatch_action_config:  dispatch configuration object that determines when during
+                text input the element returns a block_actions payload.
+            focus_on_load: Indicates whether the element will be set to auto focus within the view object.
+                Only one element can be set to true. Defaults to false.
+            placeholder: A plain_text only text object that defines the placeholder text shown in the
+                email input. Maximum length for the text in this field is 150 characters.
+        """
+        super().__init__(
+            type=self.type,
+            action_id=action_id,
+            placeholder=TextObject.parse(placeholder, PlainTextObject.type),  # type: ignore[arg-type]
+            focus_on_load=focus_on_load,
+        )
+        show_unknown_key_warning(self, others)
+
+        self.initial_value = initial_value
+        self.dispatch_action_config = dispatch_action_config
+
+

Block Elements are things that exists inside of your Blocks. +https://docs.slack.dev/reference/block-kit/block-elements/

+

https://docs.slack.dev/reference/block-kit/block-elements/email-input-element

+

Args

+
+
action_id : required
+
An identifier for the input value when the parent modal is submitted. +You can use this when you receive a view_submission payload to identify the value of the input element. +Should be unique among all other action_ids in the containing block. +Maximum length for this field is 255 characters.
+
initial_value
+
The initial value in the email input when it is loaded.
+
dispatch_action_config
+
dispatch configuration object that determines when during +text input the element returns a block_actions payload.
+
focus_on_load
+
Indicates whether the element will be set to auto focus within the view object. +Only one element can be set to true. Defaults to false.
+
placeholder
+
A plain_text only text object that defines the placeholder text shown in the +email input. Maximum length for the text in this field is 150 characters.
+
+

Ancestors

+ +

Class variables

+
+
var type
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
prop attributes : Set[str]
+
+
+ +Expand source code + +
@property
+def attributes(self) -> Set[str]:  # type: ignore[override]
+    return super().attributes.union(
+        {
+            "initial_value",
+            "dispatch_action_config",
+        }
+    )
+
+

Build an unordered collection of unique elements.

+
+
+

Inherited members

+ +
+
+class ExternalDataMultiSelectElement +(*,
placeholder: str | dict | TextObject | None = None,
action_id: str | None = None,
min_query_length: int | None = None,
initial_options: Sequence[dict | Option] | None = None,
confirm: dict | ConfirmObject | None = None,
max_selected_items: int | None = None,
focus_on_load: bool | None = None,
**others: dict)
+
+
+
+ +Expand source code + +
class ExternalDataMultiSelectElement(InputInteractiveElement):
+    type = "multi_external_select"
+
+    @property
+    def attributes(self) -> Set[str]:  # type: ignore[override]
+        return super().attributes.union({"min_query_length", "initial_options", "max_selected_items"})
+
+    def __init__(
+        self,
+        *,
+        placeholder: Optional[Union[str, dict, TextObject]] = None,
+        action_id: Optional[str] = None,
+        min_query_length: Optional[int] = None,
+        initial_options: Optional[Sequence[Union[dict, Option]]] = None,
+        confirm: Optional[Union[dict, ConfirmObject]] = None,
+        max_selected_items: Optional[int] = None,
+        focus_on_load: Optional[bool] = None,
+        **others: dict,
+    ):
+        """
+        This select menu will load its options from an external data source, allowing
+        for a dynamic list of options.
+        https://docs.slack.dev/reference/block-kit/block-elements/multi-select-menu-element#external_multi_select
+
+        Args:
+            placeholder (required): A plain_text only text object that defines the placeholder text shown on the menu.
+                Maximum length for the text in this field is 150 characters.
+            action_id (required): An identifier for the action triggered when a menu option is selected.
+                You can use this when you receive an interaction payload to identify the source of the action.
+                Should be unique among all other action_ids in the containing block.
+                Maximum length for this field is 255 characters.
+            min_query_length: When the typeahead field is used, a request will be sent on every character change.
+                If you prefer fewer requests or more fully ideated queries,
+                use the min_query_length attribute to tell Slack
+                the fewest number of typed characters required before dispatch.
+                The default value is 3
+            initial_options: An array of option objects that exactly match one or more of the options
+                within options or option_groups. These options will be selected when the menu initially loads.
+            confirm: A confirm object that defines an optional confirmation dialog that appears
+                before the multi-select choices are submitted.
+            max_selected_items: Specifies the maximum number of items that can be selected in the menu.
+                Minimum number is 1.
+            focus_on_load: Indicates whether the element will be set to auto focus within the view object.
+                Only one element can be set to true. Defaults to false.
+        """
+        super().__init__(
+            type=self.type,
+            action_id=action_id,
+            placeholder=TextObject.parse(placeholder, PlainTextObject.type),  # type: ignore[arg-type]
+            confirm=ConfirmObject.parse(confirm),  # type: ignore[arg-type]
+            focus_on_load=focus_on_load,
+        )
+        show_unknown_key_warning(self, others)
+
+        self.min_query_length = min_query_length
+        self.initial_options = Option.parse_all(initial_options)
+        self.max_selected_items = max_selected_items
+
+

Block Elements are things that exists inside of your Blocks. +https://docs.slack.dev/reference/block-kit/block-elements/

+

This select menu will load its options from an external data source, allowing +for a dynamic list of options. +https://docs.slack.dev/reference/block-kit/block-elements/multi-select-menu-element#external_multi_select

+

Args

+
+
placeholder : required
+
A plain_text only text object that defines the placeholder text shown on the menu. +Maximum length for the text in this field is 150 characters.
+
action_id : required
+
An identifier for the action triggered when a menu option is selected. +You can use this when you receive an interaction payload to identify the source of the action. +Should be unique among all other action_ids in the containing block. +Maximum length for this field is 255 characters.
+
min_query_length
+
When the typeahead field is used, a request will be sent on every character change. +If you prefer fewer requests or more fully ideated queries, +use the min_query_length attribute to tell Slack +the fewest number of typed characters required before dispatch. +The default value is 3
+
initial_options
+
An array of option objects that exactly match one or more of the options +within options or option_groups. These options will be selected when the menu initially loads.
+
confirm
+
A confirm object that defines an optional confirmation dialog that appears +before the multi-select choices are submitted.
+
max_selected_items
+
Specifies the maximum number of items that can be selected in the menu. +Minimum number is 1.
+
focus_on_load
+
Indicates whether the element will be set to auto focus within the view object. +Only one element can be set to true. Defaults to false.
+
+

Ancestors

+ +

Class variables

+
+
var type
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
prop attributes : Set[str]
+
+
+ +Expand source code + +
@property
+def attributes(self) -> Set[str]:  # type: ignore[override]
+    return super().attributes.union({"min_query_length", "initial_options", "max_selected_items"})
+
+

Build an unordered collection of unique elements.

+
+
+

Inherited members

+ +
+
+class ExternalDataSelectElement +(*,
action_id: str | None = None,
placeholder: str | TextObject | None = None,
initial_option: Option | OptionGroup | None = None,
min_query_length: int | None = None,
confirm: dict | ConfirmObject | None = None,
focus_on_load: bool | None = None,
**others: dict)
+
+
+
+ +Expand source code + +
class ExternalDataSelectElement(InputInteractiveElement):
+    type = "external_select"
+
+    @property
+    def attributes(self) -> Set[str]:  # type: ignore[override]
+        return super().attributes.union({"min_query_length", "initial_option"})
+
+    def __init__(
+        self,
+        *,
+        action_id: Optional[str] = None,
+        placeholder: Optional[Union[str, TextObject]] = None,
+        initial_option: Union[Optional[Option], Optional[OptionGroup]] = None,
+        min_query_length: Optional[int] = None,
+        confirm: Optional[Union[dict, ConfirmObject]] = None,
+        focus_on_load: Optional[bool] = None,
+        **others: dict,
+    ):
+        """
+        This select menu will load its options from an external data source, allowing
+        for a dynamic list of options.
+        https://docs.slack.dev/reference/block-kit/block-elements/select-menu-element#external_select
+
+        Args:
+            action_id (required): An identifier for the action triggered when a menu option is selected.
+                You can use this when you receive an interaction payload to identify the source of the action.
+                Should be unique among all other action_ids in the containing block.
+                Maximum length for this field is 255 characters.
+            placeholder (required): A plain_text only text object that defines the placeholder text shown on the menu.
+                Maximum length for the text in this field is 150 characters.
+            initial_option: A single option that exactly matches one of the options
+                within the options or option_groups loaded from the external data source.
+                This option will be selected when the menu initially loads.
+            min_query_length: When the typeahead field is used, a request will be sent on every character change.
+                If you prefer fewer requests or more fully ideated queries,
+                use the min_query_length attribute to tell Slack
+                the fewest number of typed characters required before dispatch.
+                The default value is 3.
+            confirm: A confirm object that defines an optional confirmation dialog
+                that appears after a menu item is selected.
+            focus_on_load: Indicates whether the element will be set to auto focus within the view object.
+                Only one element can be set to true. Defaults to false.
+        """
+        super().__init__(
+            type=self.type,
+            action_id=action_id,
+            placeholder=TextObject.parse(placeholder, PlainTextObject.type),  # type: ignore[arg-type]
+            confirm=ConfirmObject.parse(confirm),  # type: ignore[arg-type]
+            focus_on_load=focus_on_load,
+        )
+        show_unknown_key_warning(self, others)
+
+        self.min_query_length = min_query_length
+        self.initial_option = initial_option
+
+

Block Elements are things that exists inside of your Blocks. +https://docs.slack.dev/reference/block-kit/block-elements/

+

This select menu will load its options from an external data source, allowing +for a dynamic list of options. +https://docs.slack.dev/reference/block-kit/block-elements/select-menu-element#external_select

+

Args

+
+
action_id : required
+
An identifier for the action triggered when a menu option is selected. +You can use this when you receive an interaction payload to identify the source of the action. +Should be unique among all other action_ids in the containing block. +Maximum length for this field is 255 characters.
+
placeholder : required
+
A plain_text only text object that defines the placeholder text shown on the menu. +Maximum length for the text in this field is 150 characters.
+
initial_option
+
A single option that exactly matches one of the options +within the options or option_groups loaded from the external data source. +This option will be selected when the menu initially loads.
+
min_query_length
+
When the typeahead field is used, a request will be sent on every character change. +If you prefer fewer requests or more fully ideated queries, +use the min_query_length attribute to tell Slack +the fewest number of typed characters required before dispatch. +The default value is 3.
+
confirm
+
A confirm object that defines an optional confirmation dialog +that appears after a menu item is selected.
+
focus_on_load
+
Indicates whether the element will be set to auto focus within the view object. +Only one element can be set to true. Defaults to false.
+
+

Ancestors

+ +

Class variables

+
+
var type
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
prop attributes : Set[str]
+
+
+ +Expand source code + +
@property
+def attributes(self) -> Set[str]:  # type: ignore[override]
+    return super().attributes.union({"min_query_length", "initial_option"})
+
+

Build an unordered collection of unique elements.

+
+
+

Inherited members

+ +
+
+class FeedbackButtonObject +(*,
text: str | Dict[str, Any] | PlainTextObject,
accessibility_label: str | None = None,
value: str,
**others: Dict[str, Any])
+
+
+
+ +Expand source code + +
class FeedbackButtonObject(JsonObject):
+    attributes: Set[str] = set()
+
+    text_max_length = 75
+    value_max_length = 2000
+
+    @classmethod
+    def parse(cls, feedback_button: Union["FeedbackButtonObject", Dict[str, Any]]):
+        if feedback_button:
+            if isinstance(feedback_button, FeedbackButtonObject):
+                return feedback_button
+            elif isinstance(feedback_button, dict):
+                return FeedbackButtonObject(**feedback_button)
+            else:
+                # Not yet implemented: show some warning here
+                return None
+        return None
+
+    def __init__(
+        self,
+        *,
+        text: Union[str, Dict[str, Any], PlainTextObject],
+        accessibility_label: Optional[str] = None,
+        value: str,
+        **others: Dict[str, Any],
+    ):
+        """
+        A feedback button element object for either positive or negative feedback.
+        https://docs.slack.dev/reference/block-kit/block-elements/feedback-buttons-element#button-object-fields
+
+        Args:
+            text (required): An object containing some text. Maximum length for this field is 75 characters.
+            accessibility_label: A label for longer descriptive text about a button element. This label will be read out by
+                screen readers instead of the button `text` object.
+            value (required): The button value. Maximum length for this field is 2000 characters.
+        """
+        self._text: Optional[TextObject] = PlainTextObject.parse(text, default_type=PlainTextObject.type)
+        self._accessibility_label: Optional[str] = accessibility_label
+        self._value: Optional[str] = value
+        show_unknown_key_warning(self, others)
+
+    @JsonValidator(f"text attribute cannot exceed {text_max_length} characters")
+    def text_length(self) -> bool:
+        return self._text is None or len(self._text.text) <= self.text_max_length
+
+    @JsonValidator(f"value attribute cannot exceed {value_max_length} characters")
+    def value_length(self) -> bool:
+        return self._value is None or len(self._value) <= self.value_max_length
+
+    def to_dict(self) -> Dict[str, Any]:
+        self.validate_json()
+        json: Dict[str, Union[str, dict]] = {}
+        if self._text:
+            json["text"] = self._text.to_dict()
+        if self._accessibility_label:
+            json["accessibility_label"] = self._accessibility_label
+        if self._value:
+            json["value"] = self._value
+        return json
+
+

The base class for JSON serializable class objects

+

A feedback button element object for either positive or negative feedback. +https://docs.slack.dev/reference/block-kit/block-elements/feedback-buttons-element#button-object-fields

+

Args

+
+
text : required
+
An object containing some text. Maximum length for this field is 75 characters.
+
accessibility_label
+
A label for longer descriptive text about a button element. This label will be read out by +screen readers instead of the button text object.
+
value : required
+
The button value. Maximum length for this field is 2000 characters.
+
+

Ancestors

+ +

Class variables

+
+
var attributes : Set[str]
+
+

The type of the None singleton.

+
+
var text_max_length
+
+

The type of the None singleton.

+
+
var value_max_length
+
+

The type of the None singleton.

+
+
+

Static methods

+
+
+def parse(feedback_button: ForwardRef('FeedbackButtonObject') | Dict[str, Any]) +
+
+
+
+
+

Methods

+
+
+def text_length(self) ‑> bool +
+
+
+ +Expand source code + +
@JsonValidator(f"text attribute cannot exceed {text_max_length} characters")
+def text_length(self) -> bool:
+    return self._text is None or len(self._text.text) <= self.text_max_length
+
+
+
+
+def value_length(self) ‑> bool +
+
+
+ +Expand source code + +
@JsonValidator(f"value attribute cannot exceed {value_max_length} characters")
+def value_length(self) -> bool:
+    return self._value is None or len(self._value) <= self.value_max_length
+
+
+
+
+

Inherited members

+ +
+
+class FeedbackButtonsElement +(*,
action_id: str | None = None,
positive_button: dict | FeedbackButtonObject,
negative_button: dict | FeedbackButtonObject,
**others: dict)
+
+
+
+ +Expand source code + +
class FeedbackButtonsElement(InteractiveElement):
+    type = "feedback_buttons"
+
+    @property
+    def attributes(self) -> Set[str]:  # type: ignore[override]
+        return super().attributes.union({"positive_button", "negative_button"})
+
+    def __init__(
+        self,
+        *,
+        action_id: Optional[str] = None,
+        positive_button: Union[dict, FeedbackButtonObject],
+        negative_button: Union[dict, FeedbackButtonObject],
+        **others: dict,
+    ):
+        """Buttons to indicate positive or negative feedback.
+        https://docs.slack.dev/reference/block-kit/block-elements/feedback-buttons-element
+
+        Args:
+            action_id (required): An identifier for this action.
+                You can use this when you receive an interaction payload to identify the source of the action.
+                Should be unique among all other action_ids in the containing block.
+                Maximum length for this field is 255 characters.
+            positive_button (required): A button to indicate positive feedback.
+            negative_button (required): A button to indicate negative feedback.
+        """
+        super().__init__(action_id=action_id, type=self.type)
+        show_unknown_key_warning(self, others)
+
+        self.positive_button = FeedbackButtonObject.parse(positive_button)
+        self.negative_button = FeedbackButtonObject.parse(negative_button)
+
+

Block Elements are things that exists inside of your Blocks. +https://docs.slack.dev/reference/block-kit/block-elements/

+

Buttons to indicate positive or negative feedback. +https://docs.slack.dev/reference/block-kit/block-elements/feedback-buttons-element

+

Args

+
+
action_id : required
+
An identifier for this action. +You can use this when you receive an interaction payload to identify the source of the action. +Should be unique among all other action_ids in the containing block. +Maximum length for this field is 255 characters.
+
positive_button : required
+
A button to indicate positive feedback.
+
negative_button : required
+
A button to indicate negative feedback.
+
+

Ancestors

+ +

Class variables

+
+
var type
+
+

The type of the None singleton.

+
+
+

Inherited members

+ +
+
+class FileBlock +(*,
external_id: str,
source: str = 'remote',
block_id: str | None = None,
**others: dict)
+
+
+
+ +Expand source code + +
class FileBlock(Block):
+    type = "file"
+
+    @property
+    def attributes(self) -> Set[str]:  # type: ignore[override]
+        return super().attributes.union({"external_id", "source"})
+
+    def __init__(
+        self,
+        *,
+        external_id: str,
+        source: str = "remote",
+        block_id: Optional[str] = None,
+        **others: dict,
+    ):
+        """Displays a remote file.
+        https://docs.slack.dev/reference/block-kit/blocks/file-block
+
+        Args:
+            external_id (required): The external unique ID for this file.
+            source (required): At the moment, source will always be remote for a remote file.
+            block_id: A string acting as a unique identifier for a block. If not specified, one will be generated.
+                Maximum length for this field is 255 characters.
+                block_id should be unique for each message and each iteration of a message.
+                If a message is updated, use a new block_id.
+        """
+        super().__init__(type=self.type, block_id=block_id)
+        show_unknown_key_warning(self, others)
+
+        self.external_id = external_id
+        self.source = source
+
+

Blocks are a series of components that can be combined +to create visually rich and compellingly interactive messages. +https://docs.slack.dev/reference/block-kit/blocks

+

Displays a remote file. +https://docs.slack.dev/reference/block-kit/blocks/file-block

+

Args

+
+
external_id : required
+
The external unique ID for this file.
+
source : required
+
At the moment, source will always be remote for a remote file.
+
block_id
+
A string acting as a unique identifier for a block. If not specified, one will be generated. +Maximum length for this field is 255 characters. +block_id should be unique for each message and each iteration of a message. +If a message is updated, use a new block_id.
+
+

Ancestors

+ +

Class variables

+
+
var type
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
prop attributes : Set[str]
+
+
+ +Expand source code + +
@property
+def attributes(self) -> Set[str]:  # type: ignore[override]
+    return super().attributes.union({"external_id", "source"})
+
+

Build an unordered collection of unique elements.

+
+
+

Inherited members

+ +
+
+class HeaderBlock +(*,
block_id: str | None = None,
text: str | dict | TextObject | None = None,
**others: dict)
+
+
+
+ +Expand source code + +
class HeaderBlock(Block):
+    type = "header"
+    text_max_length = 150
+
+    @property
+    def attributes(self) -> Set[str]:  # type: ignore[override]
+        return super().attributes.union({"text"})
+
+    def __init__(
+        self,
+        *,
+        block_id: Optional[str] = None,
+        text: Optional[Union[str, dict, TextObject]] = None,
+        **others: dict,
+    ):
+        """A header is a plain-text block that displays in a larger, bold font.
+        https://docs.slack.dev/reference/block-kit/blocks/header-block
+
+        Args:
+            block_id: A string acting as a unique identifier for a block. If not specified, one will be generated.
+                Maximum length for this field is 255 characters.
+                block_id should be unique for each message and each iteration of a message.
+                If a message is updated, use a new block_id.
+            text (required): The text for the block, in the form of a plain_text text object.
+                Maximum length for the text in this field is 150 characters.
+        """
+        super().__init__(type=self.type, block_id=block_id)
+        show_unknown_key_warning(self, others)
+
+        self.text = TextObject.parse(text, default_type=PlainTextObject.type)  # type: ignore[arg-type]
+
+    @JsonValidator("text attribute must be specified")
+    def _validate_text(self):
+        return self.text is not None
+
+    @JsonValidator(f"text attribute cannot exceed {text_max_length} characters")
+    def _validate_alt_text_length(self):
+        return self.text is None or len(self.text.text) <= self.text_max_length
+
+

Blocks are a series of components that can be combined +to create visually rich and compellingly interactive messages. +https://docs.slack.dev/reference/block-kit/blocks

+

A header is a plain-text block that displays in a larger, bold font. +https://docs.slack.dev/reference/block-kit/blocks/header-block

+

Args

+
+
block_id
+
A string acting as a unique identifier for a block. If not specified, one will be generated. +Maximum length for this field is 255 characters. +block_id should be unique for each message and each iteration of a message. +If a message is updated, use a new block_id.
+
text : required
+
The text for the block, in the form of a plain_text text object. +Maximum length for the text in this field is 150 characters.
+
+

Ancestors

+ +

Class variables

+
+
var text_max_length
+
+

The type of the None singleton.

+
+
var type
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
prop attributes : Set[str]
+
+
+ +Expand source code + +
@property
+def attributes(self) -> Set[str]:  # type: ignore[override]
+    return super().attributes.union({"text"})
+
+

Build an unordered collection of unique elements.

+
+
+

Inherited members

+ +
+
+class IconButtonElement +(*,
action_id: str | None = None,
icon: str,
text: str | dict | TextObject,
accessibility_label: str | None = None,
value: str | None = None,
visible_to_user_ids: List[str] | None = None,
confirm: dict | ConfirmObject | None = None,
**others: dict)
+
+
+
+ +Expand source code + +
class IconButtonElement(InteractiveElement):
+    type = "icon_button"
+
+    @property
+    def attributes(self) -> Set[str]:  # type: ignore[override]
+        return super().attributes.union({"icon", "text", "accessibility_label", "value", "visible_to_user_ids", "confirm"})
+
+    def __init__(
+        self,
+        *,
+        action_id: Optional[str] = None,
+        icon: str,
+        text: Union[str, dict, TextObject],
+        accessibility_label: Optional[str] = None,
+        value: Optional[str] = None,
+        visible_to_user_ids: Optional[List[str]] = None,
+        confirm: Optional[Union[dict, ConfirmObject]] = None,
+        **others: dict,
+    ):
+        """An icon button to perform actions.
+        https://docs.slack.dev/reference/block-kit/block-elements/icon-button-element
+
+        Args:
+            action_id: An identifier for this action.
+                You can use this when you receive an interaction payload to identify the source of the action.
+                Should be unique among all other action_ids in the containing block.
+                Maximum length for this field is 255 characters.
+            icon (required): The icon to show (e.g., 'trash').
+            text (required): Defines an object containing some text.
+            accessibility_label: A label for longer descriptive text about a button element.
+                This label will be read out by screen readers instead of the button text object.
+                Maximum length for this field is 75 characters.
+            value: The button value.
+                Maximum length for this field is 2000 characters.
+            visible_to_user_ids: User IDs for which the icon appears.
+                Maximum length for this field is 10 user IDs.
+            confirm: A confirm object that defines an optional confirmation dialog after the button is clicked.
+        """
+        super().__init__(action_id=action_id, type=self.type)
+        show_unknown_key_warning(self, others)
+
+        self.icon = icon
+        self.text = TextObject.parse(text, PlainTextObject.type)
+        self.accessibility_label = accessibility_label
+        self.value = value
+        self.visible_to_user_ids = visible_to_user_ids
+        self.confirm = ConfirmObject.parse(confirm) if confirm else None
+
+

Block Elements are things that exists inside of your Blocks. +https://docs.slack.dev/reference/block-kit/block-elements/

+

An icon button to perform actions. +https://docs.slack.dev/reference/block-kit/block-elements/icon-button-element

+

Args

+
+
action_id
+
An identifier for this action. +You can use this when you receive an interaction payload to identify the source of the action. +Should be unique among all other action_ids in the containing block. +Maximum length for this field is 255 characters.
+
icon : required
+
The icon to show (e.g., 'trash').
+
text : required
+
Defines an object containing some text.
+
accessibility_label
+
A label for longer descriptive text about a button element. +This label will be read out by screen readers instead of the button text object. +Maximum length for this field is 75 characters.
+
value
+
The button value. +Maximum length for this field is 2000 characters.
+
visible_to_user_ids
+
User IDs for which the icon appears. +Maximum length for this field is 10 user IDs.
+
confirm
+
A confirm object that defines an optional confirmation dialog after the button is clicked.
+
+

Ancestors

+ +

Class variables

+
+
var type
+
+

The type of the None singleton.

+
+
+

Inherited members

+ +
+
+class ImageBlock +(*,
alt_text: str,
image_url: str | None = None,
slack_file: Dict[str, Any] | SlackFile | None = None,
title: str | dict | PlainTextObject | None = None,
block_id: str | None = None,
**others: dict)
+
+
+
+ +Expand source code + +
class ImageBlock(Block):
+    type = "image"
+
+    @property
+    def attributes(self) -> Set[str]:  # type: ignore[override]
+        return super().attributes.union({"alt_text", "image_url", "title", "slack_file"})
+
+    image_url_max_length = 3000
+    alt_text_max_length = 2000
+    title_max_length = 2000
+
+    def __init__(
+        self,
+        *,
+        alt_text: str,
+        image_url: Optional[str] = None,
+        slack_file: Optional[Union[Dict[str, Any], SlackFile]] = None,
+        title: Optional[Union[str, dict, PlainTextObject]] = None,
+        block_id: Optional[str] = None,
+        **others: dict,
+    ):
+        """A simple image block, designed to make those cat photos really pop.
+        https://docs.slack.dev/reference/block-kit/blocks/image-block
+
+        Args:
+            alt_text (required): A plain-text summary of the image. This should not contain any markup.
+                Maximum length for this field is 2000 characters.
+            image_url: The URL of the image to be displayed.
+                Maximum length for this field is 3000 characters.
+            slack_file: A Slack image file object that defines the source of the image.
+            title: An optional title for the image in the form of a text object that can only be of type: plain_text.
+                Maximum length for the text in this field is 2000 characters.
+            block_id: A string acting as a unique identifier for a block. If not specified, one will be generated.
+                Maximum length for this field is 255 characters.
+                block_id should be unique for each message and each iteration of a message.
+                If a message is updated, use a new block_id.
+        """
+        super().__init__(type=self.type, block_id=block_id)
+        show_unknown_key_warning(self, others)
+
+        self.image_url = image_url
+        self.alt_text = alt_text
+        parsed_title = None
+        if title is not None:
+            if isinstance(title, str):
+                parsed_title = PlainTextObject(text=title)
+            elif isinstance(title, dict):
+                if title.get("type") != PlainTextObject.type:
+                    raise SlackObjectFormationError(f"Unsupported type for title in an image block: {title.get('type')}")
+                parsed_title = PlainTextObject(text=title.get("text"), emoji=title.get("emoji"))  # type: ignore[arg-type]
+            elif isinstance(title, PlainTextObject):
+                parsed_title = title
+            else:
+                raise SlackObjectFormationError(f"Unsupported type for title in an image block: {type(title)}")
+        if slack_file is not None:
+            self.slack_file = (
+                slack_file if slack_file is None or isinstance(slack_file, SlackFile) else SlackFile(**slack_file)
+            )
+        self.title = parsed_title
+
+    @JsonValidator(f"image_url attribute cannot exceed {image_url_max_length} characters")
+    def _validate_image_url_length(self):
+        return self.image_url is None or len(self.image_url) <= self.image_url_max_length
+
+    @JsonValidator(f"alt_text attribute cannot exceed {alt_text_max_length} characters")
+    def _validate_alt_text_length(self):
+        return len(self.alt_text) <= self.alt_text_max_length
+
+    @JsonValidator(f"title attribute cannot exceed {title_max_length} characters")
+    def _validate_title_length(self):
+        return self.title is None or self.title.text is None or len(self.title.text) <= self.title_max_length
+
+

Blocks are a series of components that can be combined +to create visually rich and compellingly interactive messages. +https://docs.slack.dev/reference/block-kit/blocks

+

A simple image block, designed to make those cat photos really pop. +https://docs.slack.dev/reference/block-kit/blocks/image-block

+

Args

+
+
alt_text : required
+
A plain-text summary of the image. This should not contain any markup. +Maximum length for this field is 2000 characters.
+
image_url
+
The URL of the image to be displayed. +Maximum length for this field is 3000 characters.
+
slack_file
+
A Slack image file object that defines the source of the image.
+
title
+
An optional title for the image in the form of a text object that can only be of type: plain_text. +Maximum length for the text in this field is 2000 characters.
+
block_id
+
A string acting as a unique identifier for a block. If not specified, one will be generated. +Maximum length for this field is 255 characters. +block_id should be unique for each message and each iteration of a message. +If a message is updated, use a new block_id.
+
+

Ancestors

+ +

Class variables

+
+
var alt_text_max_length
+
+

The type of the None singleton.

+
+
var image_url_max_length
+
+

The type of the None singleton.

+
+
var title_max_length
+
+

The type of the None singleton.

+
+
var type
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
prop attributes : Set[str]
+
+
+ +Expand source code + +
@property
+def attributes(self) -> Set[str]:  # type: ignore[override]
+    return super().attributes.union({"alt_text", "image_url", "title", "slack_file"})
+
+

Build an unordered collection of unique elements.

+
+
+

Inherited members

+ +
+
+class ImageElement +(*,
alt_text: str | None = None,
image_url: str | None = None,
slack_file: Dict[str, Any] | SlackFile | None = None,
**others: dict)
+
+
+
+ +Expand source code + +
class ImageElement(BlockElement):
+    type = "image"
+    image_url_max_length = 3000
+    alt_text_max_length = 2000
+
+    @property
+    def attributes(self) -> Set[str]:  # type: ignore[override]
+        return super().attributes.union({"alt_text", "image_url", "slack_file"})
+
+    def __init__(
+        self,
+        *,
+        alt_text: Optional[str] = None,
+        image_url: Optional[str] = None,
+        slack_file: Optional[Union[Dict[str, Any], SlackFile]] = None,
+        **others: dict,
+    ):
+        """An element to insert an image - this element can be used in section and
+        context blocks only. If you want a block with only an image in it,
+        you're looking for the image block.
+        https://docs.slack.dev/reference/block-kit/block-elements/image-element
+
+        Args:
+            alt_text (required): A plain-text summary of the image. This should not contain any markup.
+            image_url: The URL of the image to be displayed.
+            slack_file: A Slack image file object that defines the source of the image.
+        """
+        super().__init__(type=self.type)
+        show_unknown_key_warning(self, others)
+
+        self.image_url = image_url
+        self.alt_text = alt_text
+        self.slack_file = slack_file if slack_file is None or isinstance(slack_file, SlackFile) else SlackFile(**slack_file)
+
+    @JsonValidator(f"image_url attribute cannot exceed {image_url_max_length} characters")
+    def _validate_image_url_length(self) -> bool:
+        return self.image_url is None or len(self.image_url) <= self.image_url_max_length
+
+    @JsonValidator(f"alt_text attribute cannot exceed {alt_text_max_length} characters")
+    def _validate_alt_text_length(self) -> bool:
+        return len(self.alt_text) <= self.alt_text_max_length  # type: ignore[arg-type]
+
+

Block Elements are things that exists inside of your Blocks. +https://docs.slack.dev/reference/block-kit/block-elements/

+

An element to insert an image - this element can be used in section and +context blocks only. If you want a block with only an image in it, +you're looking for the image block. +https://docs.slack.dev/reference/block-kit/block-elements/image-element

+

Args

+
+
alt_text : required
+
A plain-text summary of the image. This should not contain any markup.
+
image_url
+
The URL of the image to be displayed.
+
slack_file
+
A Slack image file object that defines the source of the image.
+
+

Ancestors

+ +

Class variables

+
+
var alt_text_max_length
+
+

The type of the None singleton.

+
+
var image_url_max_length
+
+

The type of the None singleton.

+
+
var type
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
prop attributes : Set[str]
+
+
+ +Expand source code + +
@property
+def attributes(self) -> Set[str]:  # type: ignore[override]
+    return super().attributes.union({"alt_text", "image_url", "slack_file"})
+
+

Build an unordered collection of unique elements.

+
+
+

Inherited members

+ +
+
+class InputBlock +(*,
label: str | dict | PlainTextObject,
element: str | dict | InputInteractiveElement,
block_id: str | None = None,
hint: str | dict | PlainTextObject | None = None,
dispatch_action: bool | None = None,
optional: bool | None = None,
**others: dict)
+
+
+
+ +Expand source code + +
class InputBlock(Block):
+    type = "input"
+    label_max_length = 2000
+    hint_max_length = 2000
+
+    @property
+    def attributes(self) -> Set[str]:  # type: ignore[override]
+        return super().attributes.union({"label", "hint", "element", "optional", "dispatch_action"})
+
+    def __init__(
+        self,
+        *,
+        label: Union[str, dict, PlainTextObject],
+        element: Union[str, dict, InputInteractiveElement],
+        block_id: Optional[str] = None,
+        hint: Optional[Union[str, dict, PlainTextObject]] = None,
+        dispatch_action: Optional[bool] = None,
+        optional: Optional[bool] = None,
+        **others: dict,
+    ):
+        """A block that collects information from users - it can hold a plain-text input element,
+        a select menu element, a multi-select menu element, or a datepicker.
+        https://docs.slack.dev/reference/block-kit/blocks/input-block
+
+        Args:
+            label (required): A label that appears above an input element in the form of a text object
+                that must have type of plain_text. Maximum length for the text in this field is 2000 characters.
+            element (required): An plain-text input element, a checkbox element, a radio button element,
+                a select menu element, a multi-select menu element, or a datepicker.
+            block_id: A string acting as a unique identifier for a block. If not specified, one will be generated.
+                Maximum length for this field is 255 characters.
+                block_id should be unique for each message or view and each iteration of a message or view.
+                If a message or view is updated, use a new block_id.
+            hint: An optional hint that appears below an input element in a lighter grey.
+                It must be a text object with a type of plain_text.
+                Maximum length for the text in this field is 2000 characters.
+            dispatch_action: A boolean that indicates whether or not the use of elements in this block
+                should dispatch a block_actions payload. Defaults to false.
+            optional: A boolean that indicates whether the input element may be empty when a user submits the modal.
+                Defaults to false.
+        """
+        super().__init__(type=self.type, block_id=block_id)
+        show_unknown_key_warning(self, others)
+
+        self.label = TextObject.parse(label, default_type=PlainTextObject.type)
+        self.element = BlockElement.parse(element)  # type: ignore[arg-type]
+        self.hint = TextObject.parse(hint, default_type=PlainTextObject.type)  # type: ignore[arg-type]
+        self.dispatch_action = dispatch_action
+        self.optional = optional
+
+    @JsonValidator(f"label attribute cannot exceed {label_max_length} characters")
+    def _validate_label_length(self):
+        return self.label is None or self.label.text is None or len(self.label.text) <= self.label_max_length
+
+    @JsonValidator(f"hint attribute cannot exceed {hint_max_length} characters")
+    def _validate_hint_length(self):
+        return self.hint is None or self.hint.text is None or len(self.hint.text) <= self.label_max_length
+
+    @JsonValidator(
+        (
+            "element attribute must be a string, select element, multi-select element, "
+            "or a datepicker. (Sub-classes of InputInteractiveElement)"
+        )
+    )
+    def _validate_element_type(self):
+        return self.element is None or isinstance(self.element, (str, InputInteractiveElement))
+
+

Blocks are a series of components that can be combined +to create visually rich and compellingly interactive messages. +https://docs.slack.dev/reference/block-kit/blocks

+

A block that collects information from users - it can hold a plain-text input element, +a select menu element, a multi-select menu element, or a datepicker. +https://docs.slack.dev/reference/block-kit/blocks/input-block

+

Args

+
+
label : required
+
A label that appears above an input element in the form of a text object +that must have type of plain_text. Maximum length for the text in this field is 2000 characters.
+
element : required
+
An plain-text input element, a checkbox element, a radio button element, +a select menu element, a multi-select menu element, or a datepicker.
+
block_id
+
A string acting as a unique identifier for a block. If not specified, one will be generated. +Maximum length for this field is 255 characters. +block_id should be unique for each message or view and each iteration of a message or view. +If a message or view is updated, use a new block_id.
+
hint
+
An optional hint that appears below an input element in a lighter grey. +It must be a text object with a type of plain_text. +Maximum length for the text in this field is 2000 characters.
+
dispatch_action
+
A boolean that indicates whether or not the use of elements in this block +should dispatch a block_actions payload. Defaults to false.
+
optional
+
A boolean that indicates whether the input element may be empty when a user submits the modal. +Defaults to false.
+
+

Ancestors

+ +

Class variables

+
+
var hint_max_length
+
+

The type of the None singleton.

+
+
var label_max_length
+
+

The type of the None singleton.

+
+
var type
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
prop attributes : Set[str]
+
+
+ +Expand source code + +
@property
+def attributes(self) -> Set[str]:  # type: ignore[override]
+    return super().attributes.union({"label", "hint", "element", "optional", "dispatch_action"})
+
+

Build an unordered collection of unique elements.

+
+
+

Inherited members

+ +
+
+class InputInteractiveElement +(*,
action_id: str | None = None,
placeholder: str | TextObject | None = None,
type: str | None = None,
subtype: str | None = None,
confirm: dict | ConfirmObject | None = None,
focus_on_load: bool | None = None,
**others: dict)
+
+
+
+ +Expand source code + +
class InputInteractiveElement(InteractiveElement, metaclass=ABCMeta):
+    placeholder_max_length = 150
+
+    attributes = {"type", "action_id", "placeholder", "confirm", "focus_on_load"}
+
+    @property
+    def subtype(self) -> Optional[str]:
+        return self.type
+
+    def __init__(
+        self,
+        *,
+        action_id: Optional[str] = None,
+        placeholder: Optional[Union[str, TextObject]] = None,
+        type: Optional[str] = None,
+        subtype: Optional[str] = None,
+        confirm: Optional[Union[dict, ConfirmObject]] = None,
+        focus_on_load: Optional[bool] = None,
+        **others: dict,
+    ):
+        """InteractiveElement that is usable in input blocks
+
+        We generally recommend using the concrete subclasses for better supports of available properties.
+        """
+        if subtype:
+            self._subtype_warning()
+        super().__init__(action_id=action_id, type=type or subtype)
+
+        # Note that we don't intentionally have show_unknown_key_warning for the unknown key warnings here.
+        # It's fine to pass any kwargs to the held dict here although the class does not do any validation.
+        # show_unknown_key_warning(self, others)
+
+        self.placeholder = TextObject.parse(placeholder)  # type: ignore[arg-type]
+        self.confirm = ConfirmObject.parse(confirm)  # type: ignore[arg-type]
+        self.focus_on_load = focus_on_load
+
+    @JsonValidator(f"placeholder attribute cannot exceed {placeholder_max_length} characters")
+    def _validate_placeholder_length(self) -> bool:
+        return (
+            self.placeholder is None
+            or self.placeholder.text is None
+            or len(self.placeholder.text) <= self.placeholder_max_length
+        )
+
+

Block Elements are things that exists inside of your Blocks. +https://docs.slack.dev/reference/block-kit/block-elements/

+

InteractiveElement that is usable in input blocks

+

We generally recommend using the concrete subclasses for better supports of available properties.

+

Ancestors

+ +

Subclasses

+ +

Class variables

+
+
var attributes
+
+

The type of the None singleton.

+
+
var placeholder_max_length
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
prop subtype : str | None
+
+
+ +Expand source code + +
@property
+def subtype(self) -> Optional[str]:
+    return self.type
+
+
+
+
+

Inherited members

+ +
+
+class InteractiveElement +(*,
action_id: str | None = None,
type: str | None = None,
subtype: str | None = None,
**others: dict)
+
+
+
+ +Expand source code + +
class InteractiveElement(BlockElement):
+    action_id_max_length = 255
+
+    @property
+    def attributes(self) -> Set[str]:  # type: ignore[override]
+        return super().attributes.union({"alt_text", "action_id"})
+
+    def __init__(
+        self,
+        *,
+        action_id: Optional[str] = None,
+        type: Optional[str] = None,
+        subtype: Optional[str] = None,
+        **others: dict,
+    ):
+        """An interactive block element.
+
+        We generally recommend using the concrete subclasses for better supports of available properties.
+        """
+        if subtype:
+            self._subtype_warning()
+        super().__init__(type=type or subtype)
+
+        # Note that we don't intentionally have show_unknown_key_warning for the unknown key warnings here.
+        # It's fine to pass any kwargs to the held dict here although the class does not do any validation.
+        # show_unknown_key_warning(self, others)
+
+        self.action_id = action_id
+
+    @JsonValidator(f"action_id attribute cannot exceed {action_id_max_length} characters")
+    def _validate_action_id_length(self) -> bool:
+        return self.action_id is None or len(self.action_id) <= self.action_id_max_length
+
+

Block Elements are things that exists inside of your Blocks. +https://docs.slack.dev/reference/block-kit/block-elements/

+

An interactive block element.

+

We generally recommend using the concrete subclasses for better supports of available properties.

+

Ancestors

+ +

Subclasses

+ +

Class variables

+
+
var action_id_max_length
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
prop attributes : Set[str]
+
+
+ +Expand source code + +
@property
+def attributes(self) -> Set[str]:  # type: ignore[override]
+    return super().attributes.union({"alt_text", "action_id"})
+
+

Build an unordered collection of unique elements.

+
+
+

Inherited members

+ +
+
+class LinkButtonElement +(*,
text: str | dict | PlainTextObject,
url: str,
action_id: str | None = None,
style: str | None = None,
**others: dict)
+
+
+
+ +Expand source code + +
class LinkButtonElement(ButtonElement):
+    def __init__(
+        self,
+        *,
+        text: Union[str, dict, PlainTextObject],
+        url: str,
+        action_id: Optional[str] = None,
+        style: Optional[str] = None,
+        **others: dict,
+    ):
+        """A simple button that simply opens a given URL. You will still receive an
+        interaction payload and will need to send an acknowledgement response.
+        This is a helper class that makes creating links simpler.
+        https://docs.slack.dev/reference/block-kit/block-elements/button-element/
+
+        Args:
+            text (required): A text object that defines the button's text.
+                Can only be of type: plain_text.
+                Maximum length for the text in this field is 75 characters.
+            url (required): A URL to load in the user's browser when the button is clicked.
+                Maximum length for this field is 3000 characters.
+                If you're using url, you'll still receive an interaction payload
+                and will need to send an acknowledgement response.
+            action_id (required): An identifier for this action.
+                You can use this when you receive an interaction payload to identify the source of the action.
+                Should be unique among all other action_ids in the containing block.
+                Maximum length for this field is 255 characters.
+            style: Decorates buttons with alternative visual color schemes. Use this option with restraint.
+                "primary" gives buttons a green outline and text, ideal for affirmation or confirmation actions.
+                "primary" should only be used for one button within a set.
+                "danger" gives buttons a red outline and text, and should be used when the action is destructive.
+                Use "danger" even more sparingly than "primary".
+                If you don't include this field, the default button style will be used.
+        """
+        super().__init__(
+            # NOTE: value must be always absent
+            text=text,
+            url=url,
+            action_id=action_id,
+            value=None,
+            style=style,
+        )
+        show_unknown_key_warning(self, others)
+
+

Block Elements are things that exists inside of your Blocks. +https://docs.slack.dev/reference/block-kit/block-elements/

+

A simple button that simply opens a given URL. You will still receive an +interaction payload and will need to send an acknowledgement response. +This is a helper class that makes creating links simpler. +https://docs.slack.dev/reference/block-kit/block-elements/button-element/

+

Args

+
+
text : required
+
A text object that defines the button's text. +Can only be of type: plain_text. +Maximum length for the text in this field is 75 characters.
+
url : required
+
A URL to load in the user's browser when the button is clicked. +Maximum length for this field is 3000 characters. +If you're using url, you'll still receive an interaction payload +and will need to send an acknowledgement response.
+
action_id : required
+
An identifier for this action. +You can use this when you receive an interaction payload to identify the source of the action. +Should be unique among all other action_ids in the containing block. +Maximum length for this field is 255 characters.
+
style
+
Decorates buttons with alternative visual color schemes. Use this option with restraint. +"primary" gives buttons a green outline and text, ideal for affirmation or confirmation actions. +"primary" should only be used for one button within a set. +"danger" gives buttons a red outline and text, and should be used when the action is destructive. +Use "danger" even more sparingly than "primary". +If you don't include this field, the default button style will be used.
+
+

Ancestors

+ +

Inherited members

+ +
+
+class MarkdownBlock +(*, text: str, block_id: str | None = None, **others: dict) +
+
+
+ +Expand source code + +
class MarkdownBlock(Block):
+    type = "markdown"
+    text_max_length = 12000
+
+    @property
+    def attributes(self) -> Set[str]:  # type: ignore[override]
+        return super().attributes.union({"text"})
+
+    def __init__(
+        self,
+        *,
+        text: str,
+        block_id: Optional[str] = None,
+        **others: dict,
+    ):
+        """Displays formatted markdown.
+        https://docs.slack.dev/reference/block-kit/blocks/markdown-block/
+
+        Args:
+            block_id: A string acting as a unique identifier for a block. If not specified, one will be generated.
+                Maximum length for this field is 255 characters.
+                block_id should be unique for each message and each iteration of a message.
+                If a message is updated, use a new block_id.
+            text (required): The standard markdown-formatted text. Limit 12,000 characters max.
+        """
+        super().__init__(type=self.type, block_id=block_id)
+        show_unknown_key_warning(self, others)
+
+        self.text = text
+
+    @JsonValidator("text attribute must be specified")
+    def _validate_text(self):
+        return self.text != ""
+
+    @JsonValidator(f"text attribute cannot exceed {text_max_length} characters")
+    def _validate_alt_text_length(self):
+        return len(self.text) <= self.text_max_length
+
+

Blocks are a series of components that can be combined +to create visually rich and compellingly interactive messages. +https://docs.slack.dev/reference/block-kit/blocks

+

Displays formatted markdown. +https://docs.slack.dev/reference/block-kit/blocks/markdown-block/

+

Args

+
+
block_id
+
A string acting as a unique identifier for a block. If not specified, one will be generated. +Maximum length for this field is 255 characters. +block_id should be unique for each message and each iteration of a message. +If a message is updated, use a new block_id.
+
text : required
+
The standard markdown-formatted text. Limit 12,000 characters max.
+
+

Ancestors

+ +

Class variables

+
+
var text_max_length
+
+

The type of the None singleton.

+
+
var type
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
prop attributes : Set[str]
+
+
+ +Expand source code + +
@property
+def attributes(self) -> Set[str]:  # type: ignore[override]
+    return super().attributes.union({"text"})
+
+

Build an unordered collection of unique elements.

+
+
+

Inherited members

+ +
+
+class MarkdownTextObject +(*, text: str, verbatim: bool | None = None) +
+
+
+ +Expand source code + +
class MarkdownTextObject(TextObject):
+    """mrkdwn typed text object"""
+
+    type = "mrkdwn"
+
+    @property
+    def attributes(self) -> Set[str]:  # type: ignore[override]
+        return super().attributes.union({"verbatim"})
+
+    def __init__(self, *, text: str, verbatim: Optional[bool] = None):
+        """A Markdown text object, meaning markdown characters will be parsed as
+        formatting information.
+        https://docs.slack.dev/reference/block-kit/composition-objects/text-object
+
+        Args:
+            text (required): The text for the block. This field accepts any of the standard text formatting markup
+                when type is mrkdwn.
+            verbatim: When set to false (as is default) URLs will be auto-converted into links,
+                conversation names will be link-ified, and certain mentions will be automatically parsed.
+                Using a value of true will skip any preprocessing of this nature,
+                although you can still include manual parsing strings. This field is only usable when type is mrkdwn.
+        """
+        super().__init__(text=text, type=self.type)
+        self.verbatim = verbatim
+
+    @staticmethod
+    def from_str(text: str) -> "MarkdownTextObject":
+        """Transforms a string into the required object shape to act as a MarkdownTextObject"""
+        return MarkdownTextObject(text=text)
+
+    @staticmethod
+    def direct_from_string(text: str) -> Dict[str, Any]:
+        """Transforms a string into the required object shape to act as a MarkdownTextObject"""
+        return MarkdownTextObject.from_str(text).to_dict()
+
+    @staticmethod
+    def from_link(link: Link, title: str = "") -> "MarkdownTextObject":
+        """
+        Transform a Link object directly into the required object shape
+        to act as a MarkdownTextObject
+        """
+        if title:
+            title = f": {title}"
+        return MarkdownTextObject(text=f"{link}{title}")
+
+    @staticmethod
+    def direct_from_link(link: Link, title: str = "") -> Dict[str, Any]:
+        """
+        Transform a Link object directly into the required object shape
+        to act as a MarkdownTextObject
+        """
+        return MarkdownTextObject.from_link(link, title).to_dict()
+
+

mrkdwn typed text object

+

A Markdown text object, meaning markdown characters will be parsed as +formatting information. +https://docs.slack.dev/reference/block-kit/composition-objects/text-object

+

Args

+
+
text : required
+
The text for the block. This field accepts any of the standard text formatting markup +when type is mrkdwn.
+
verbatim
+
When set to false (as is default) URLs will be auto-converted into links, +conversation names will be link-ified, and certain mentions will be automatically parsed. +Using a value of true will skip any preprocessing of this nature, +although you can still include manual parsing strings. This field is only usable when type is mrkdwn.
+
+

Ancestors

+ +

Class variables

+
+
var type
+
+

The type of the None singleton.

+
+
+

Static methods

+
+ +
+
+ +Expand source code + +
@staticmethod
+def direct_from_link(link: Link, title: str = "") -> Dict[str, Any]:
+    """
+    Transform a Link object directly into the required object shape
+    to act as a MarkdownTextObject
+    """
+    return MarkdownTextObject.from_link(link, title).to_dict()
+
+

Transform a Link object directly into the required object shape +to act as a MarkdownTextObject

+
+
+def direct_from_string(text: str) ‑> Dict[str, Any] +
+
+
+ +Expand source code + +
@staticmethod
+def direct_from_string(text: str) -> Dict[str, Any]:
+    """Transforms a string into the required object shape to act as a MarkdownTextObject"""
+    return MarkdownTextObject.from_str(text).to_dict()
+
+

Transforms a string into the required object shape to act as a MarkdownTextObject

+
+ +
+
+ +Expand source code + +
@staticmethod
+def from_link(link: Link, title: str = "") -> "MarkdownTextObject":
+    """
+    Transform a Link object directly into the required object shape
+    to act as a MarkdownTextObject
+    """
+    if title:
+        title = f": {title}"
+    return MarkdownTextObject(text=f"{link}{title}")
+
+

Transform a Link object directly into the required object shape +to act as a MarkdownTextObject

+
+
+def from_str(text: str) ‑> MarkdownTextObject +
+
+
+ +Expand source code + +
@staticmethod
+def from_str(text: str) -> "MarkdownTextObject":
+    """Transforms a string into the required object shape to act as a MarkdownTextObject"""
+    return MarkdownTextObject(text=text)
+
+

Transforms a string into the required object shape to act as a MarkdownTextObject

+
+
+

Instance variables

+
+
prop attributes : Set[str]
+
+
+ +Expand source code + +
@property
+def attributes(self) -> Set[str]:  # type: ignore[override]
+    return super().attributes.union({"verbatim"})
+
+

Build an unordered collection of unique elements.

+
+
+

Inherited members

+ +
+
+class NumberInputElement +(*,
action_id: str | None = None,
is_decimal_allowed: bool | None = False,
initial_value: int | float | str | None = None,
min_value: int | float | str | None = None,
max_value: int | float | str | None = None,
dispatch_action_config: dict | DispatchActionConfig | None = None,
focus_on_load: bool | None = None,
placeholder: str | dict | TextObject | None = None,
**others: dict)
+
+
+
+ +Expand source code + +
class NumberInputElement(InputInteractiveElement):
+    type = "number_input"
+
+    @property
+    def attributes(self) -> Set[str]:  # type: ignore[override]
+        return super().attributes.union(
+            {
+                "initial_value",
+                "is_decimal_allowed",
+                "min_value",
+                "max_value",
+                "dispatch_action_config",
+            }
+        )
+
+    def __init__(
+        self,
+        *,
+        action_id: Optional[str] = None,
+        is_decimal_allowed: Optional[bool] = False,
+        initial_value: Optional[Union[int, float, str]] = None,
+        min_value: Optional[Union[int, float, str]] = None,
+        max_value: Optional[Union[int, float, str]] = None,
+        dispatch_action_config: Optional[Union[dict, DispatchActionConfig]] = None,
+        focus_on_load: Optional[bool] = None,
+        placeholder: Optional[Union[str, dict, TextObject]] = None,
+        **others: dict,
+    ):
+        """
+        https://docs.slack.dev/reference/block-kit/block-elements/number-input-element/
+
+        Args:
+            action_id (required): An identifier for the input value when the parent modal is submitted.
+                You can use this when you receive a view_submission payload to identify the value of the input element.
+                Should be unique among all other action_ids in the containing block.
+                Maximum length for this field is 255 characters.
+            is_decimal_allowed (required): Decimal numbers are allowed if is_decimal_allowed= true, set the value to
+                false otherwise.
+            initial_value: The initial value in the number input when it is loaded.
+            min_value: The minimum value, cannot be greater than max_value.
+            max_value: The maximum value, cannot be less than min_value.
+            dispatch_action_config: A dispatch configuration object that determines when
+                during text input the element returns a block_actions payload.
+            focus_on_load: Indicates whether the element will be set to auto focus within the view object.
+                Only one element can be set to true. Defaults to false.
+            placeholder: A plain_text only text object that defines the placeholder text shown
+                in the plain-text input. Maximum length for the text in this field is 150 characters.
+        """
+        super().__init__(
+            type=self.type,
+            action_id=action_id,
+            placeholder=TextObject.parse(placeholder, PlainTextObject.type),  # type: ignore[arg-type]
+            focus_on_load=focus_on_load,
+        )
+        show_unknown_key_warning(self, others)
+
+        self.initial_value = str(initial_value) if initial_value is not None else None
+        self.is_decimal_allowed = is_decimal_allowed
+        self.min_value = str(min_value) if min_value is not None else None
+        self.max_value = str(max_value) if max_value is not None else None
+        self.dispatch_action_config = dispatch_action_config
+
+

Block Elements are things that exists inside of your Blocks. +https://docs.slack.dev/reference/block-kit/block-elements/

+

https://docs.slack.dev/reference/block-kit/block-elements/number-input-element/

+

Args

+
+
action_id : required
+
An identifier for the input value when the parent modal is submitted. +You can use this when you receive a view_submission payload to identify the value of the input element. +Should be unique among all other action_ids in the containing block. +Maximum length for this field is 255 characters.
+
is_decimal_allowed : required
+
Decimal numbers are allowed if is_decimal_allowed= true, set the value to +false otherwise.
+
initial_value
+
The initial value in the number input when it is loaded.
+
min_value
+
The minimum value, cannot be greater than max_value.
+
max_value
+
The maximum value, cannot be less than min_value.
+
dispatch_action_config
+
A dispatch configuration object that determines when +during text input the element returns a block_actions payload.
+
focus_on_load
+
Indicates whether the element will be set to auto focus within the view object. +Only one element can be set to true. Defaults to false.
+
placeholder
+
A plain_text only text object that defines the placeholder text shown +in the plain-text input. Maximum length for the text in this field is 150 characters.
+
+

Ancestors

+ +

Class variables

+
+
var type
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
prop attributes : Set[str]
+
+
+ +Expand source code + +
@property
+def attributes(self) -> Set[str]:  # type: ignore[override]
+    return super().attributes.union(
+        {
+            "initial_value",
+            "is_decimal_allowed",
+            "min_value",
+            "max_value",
+            "dispatch_action_config",
+        }
+    )
+
+

Build an unordered collection of unique elements.

+
+
+

Inherited members

+ +
+
+class Option +(*,
value: str,
label: str | None = None,
text: str | Dict[str, Any] | TextObject | None = None,
description: str | Dict[str, Any] | TextObject | None = None,
url: str | None = None,
**others: Dict[str, Any])
+
+
+
+ +Expand source code + +
class Option(JsonObject):
+    """Option object used in dialogs, legacy message actions (interactivity in attachments),
+    and blocks. JSON must be retrieved with an explicit option_type - the Slack API has
+    different required formats in different situations
+    """
+
+    attributes: Set[str] = set()
+    logger = logging.getLogger(__name__)
+
+    label_max_length = 75
+    value_max_length = 150
+
+    def __init__(
+        self,
+        *,
+        value: str,
+        label: Optional[str] = None,
+        text: Optional[Union[str, Dict[str, Any], TextObject]] = None,  # Block Kit
+        description: Optional[Union[str, Dict[str, Any], TextObject]] = None,
+        url: Optional[str] = None,
+        **others: Dict[str, Any],
+    ):
+        """
+        An object that represents a single selectable item in a block element (
+        SelectElement, OverflowMenuElement) or dialog element
+        (StaticDialogSelectElement)
+
+        Blocks:
+        https://docs.slack.dev/reference/block-kit/composition-objects/option-object
+
+        Dialogs:
+        https://docs.slack.dev/legacy/legacy-dialogs/#select_elements
+
+        Legacy interactive attachments:
+        https://docs.slack.dev/legacy/legacy-messaging/legacy-interactive-message-field-guide/#option_fields
+
+        Args:
+            label: A short, user-facing string to label this option to users.
+                Cannot exceed 75 characters.
+            value: A short string that identifies this particular option to your
+                application. It will be part of the payload when this option is selected
+                . Cannot exceed 150 characters.
+            description: A user-facing string that provides more details about
+                this option. Only supported in legacy message actions, not in blocks or
+                dialogs.
+        """
+        if text:
+            # For better compatibility with Block Kit ("mrkdwn" does not work for it),
+            # we've changed the default text object type to plain_text since version 3.10.0
+            self._text: Optional[TextObject] = TextObject.parse(
+                text=text,  # "text" here can be either a str or a TextObject
+                default_type=PlainTextObject.type,
+            )
+            self._label: Optional[str] = None
+        else:
+            self._text = None
+            self._label = label
+
+        # for backward-compatibility with version 2.0-2.5, the following fields return str values
+        self.text: Optional[str] = self._text.text if self._text else None
+        self.label: Optional[str] = self._label
+
+        self.value: str = value
+
+        # for backward-compatibility with version 2.0-2.5, the following fields return str values
+        if isinstance(description, str):
+            self.description = description
+            self._block_description = PlainTextObject.from_str(description)
+        elif isinstance(description, dict):
+            self.description = description["text"]
+            self._block_description = TextObject.parse(description)  # type: ignore[assignment]
+        elif isinstance(description, TextObject):
+            self.description = description.text
+            self._block_description = description  # type: ignore[assignment]
+        else:
+            self.description = None  # type: ignore[assignment]
+            self._block_description = None  # type: ignore[assignment]
+
+        # A URL to load in the user's browser when the option is clicked.
+        # The url attribute is only available in overflow menus.
+        # Maximum length for this field is 3000 characters.
+        # If you're using url, you'll still receive an interaction payload
+        # and will need to send an acknowledgement response.
+        self.url: Optional[str] = url
+        show_unknown_key_warning(self, others)
+
+    @JsonValidator(f"label attribute cannot exceed {label_max_length} characters")
+    def _validate_label_length(self) -> bool:
+        return self._label is None or len(self._label) <= self.label_max_length
+
+    @JsonValidator(f"text attribute cannot exceed {label_max_length} characters")
+    def _validate_text_length(self) -> bool:
+        return self._text is None or self._text.text is None or len(self._text.text) <= self.label_max_length
+
+    @JsonValidator(f"value attribute cannot exceed {value_max_length} characters")
+    def _validate_value_length(self) -> bool:
+        return len(self.value) <= self.value_max_length
+
+    @classmethod
+    def parse_all(cls, options: Optional[Sequence[Union[Dict[str, Any], "Option"]]]) -> Optional[List["Option"]]:
+        if options is None:
+            return None
+        option_objects: List[Option] = []
+        for o in options:
+            if isinstance(o, dict):
+                d = copy.copy(o)
+                option_objects.append(Option(**d))
+            elif isinstance(o, Option):
+                option_objects.append(o)
+            else:
+                cls.logger.warning(f"Unknown option object detected and skipped ({o})")
+        return option_objects
+
+    def to_dict(self, option_type: str = "block") -> Dict[str, Any]:
+        """
+        Different parent classes must call this with a valid value from OptionTypes -
+        either "dialog", "action", or "block", so that JSON is returned in the
+        correct shape.
+        """
+        self.validate_json()
+        if option_type == "dialog":
+            return {"label": self.label, "value": self.value}
+        elif option_type == "action" or option_type == "attachment":
+            # "action" can be confusing but it means a legacy message action in attachments
+            # we don't remove the type name for backward compatibility though
+            json: Dict[str, Any] = {"text": self.label, "value": self.value}
+            if self.description is not None:
+                json["description"] = self.description
+            return json
+        else:  # if option_type == "block"; this should be the most common case
+            text: TextObject = self._text or PlainTextObject.from_str(self.label)  # type: ignore[arg-type]
+            json = {
+                "text": text.to_dict(),
+                "value": self.value,
+            }
+            if self._block_description:
+                json["description"] = self._block_description.to_dict()
+            if self.url:
+                json["url"] = self.url
+            return json
+
+    @staticmethod
+    def from_single_value(value_and_label: str):
+        """Creates a simple Option instance with the same value and label"""
+        return Option(value=value_and_label, label=value_and_label)
+
+

Option object used in dialogs, legacy message actions (interactivity in attachments), +and blocks. JSON must be retrieved with an explicit option_type - the Slack API has +different required formats in different situations

+

An object that represents a single selectable item in a block element ( +SelectElement, OverflowMenuElement) or dialog element +(StaticDialogSelectElement)

+

Blocks: +https://docs.slack.dev/reference/block-kit/composition-objects/option-object

+

Dialogs: +https://docs.slack.dev/legacy/legacy-dialogs/#select_elements

+

Legacy interactive attachments: +https://docs.slack.dev/legacy/legacy-messaging/legacy-interactive-message-field-guide/#option_fields

+

Args

+
+
label
+
A short, user-facing string to label this option to users. +Cannot exceed 75 characters.
+
value
+
A short string that identifies this particular option to your +application. It will be part of the payload when this option is selected +. Cannot exceed 150 characters.
+
description
+
A user-facing string that provides more details about +this option. Only supported in legacy message actions, not in blocks or +dialogs.
+
+

Ancestors

+ +

Class variables

+
+
var attributes : Set[str]
+
+

The type of the None singleton.

+
+
var label_max_length
+
+

The type of the None singleton.

+
+
var logger
+
+

The type of the None singleton.

+
+
var value_max_length
+
+

The type of the None singleton.

+
+
+

Static methods

+
+
+def from_single_value(value_and_label: str) +
+
+
+ +Expand source code + +
@staticmethod
+def from_single_value(value_and_label: str):
+    """Creates a simple Option instance with the same value and label"""
+    return Option(value=value_and_label, label=value_and_label)
+
+

Creates a simple Option instance with the same value and label

+
+
+def parse_all(options: Sequence[Dict[str, Any] | ForwardRef('Option')] | None) ‑> List[Option] | None +
+
+
+
+
+

Methods

+
+
+def to_dict(self, option_type: str = 'block') ‑> Dict[str, Any] +
+
+
+ +Expand source code + +
def to_dict(self, option_type: str = "block") -> Dict[str, Any]:
+    """
+    Different parent classes must call this with a valid value from OptionTypes -
+    either "dialog", "action", or "block", so that JSON is returned in the
+    correct shape.
+    """
+    self.validate_json()
+    if option_type == "dialog":
+        return {"label": self.label, "value": self.value}
+    elif option_type == "action" or option_type == "attachment":
+        # "action" can be confusing but it means a legacy message action in attachments
+        # we don't remove the type name for backward compatibility though
+        json: Dict[str, Any] = {"text": self.label, "value": self.value}
+        if self.description is not None:
+            json["description"] = self.description
+        return json
+    else:  # if option_type == "block"; this should be the most common case
+        text: TextObject = self._text or PlainTextObject.from_str(self.label)  # type: ignore[arg-type]
+        json = {
+            "text": text.to_dict(),
+            "value": self.value,
+        }
+        if self._block_description:
+            json["description"] = self._block_description.to_dict()
+        if self.url:
+            json["url"] = self.url
+        return json
+
+

Different parent classes must call this with a valid value from OptionTypes - +either "dialog", "action", or "block", so that JSON is returned in the +correct shape.

+
+
+

Inherited members

+ +
+
+class OptionGroup +(*,
label: str | Dict[str, Any] | TextObject | None = None,
options: Sequence[Dict[str, Any] | Option],
**others: Dict[str, Any])
+
+
+
+ +Expand source code + +
class OptionGroup(JsonObject):
+    """
+    JSON must be retrieved with an explicit option_type - the Slack API has
+    different required formats in different situations
+    """
+
+    attributes: Set[str] = set()
+    label_max_length = 75
+    options_max_length = 100
+    logger = logging.getLogger(__name__)
+
+    def __init__(
+        self,
+        *,
+        label: Optional[Union[str, Dict[str, Any], TextObject]] = None,
+        options: Sequence[Union[Dict[str, Any], Option]],
+        **others: Dict[str, Any],
+    ):
+        """
+        Create a group of Option objects - pass in a label (that will be part of the
+        UI) and a list of Option objects.
+
+        Blocks:
+        https://docs.slack.dev/reference/block-kit/composition-objects/option-group-object
+
+        Dialogs:
+        https://docs.slack.dev/legacy/legacy-dialogs/#select_elements
+
+        Legacy interactive attachments:
+        https://docs.slack.dev/legacy/legacy-messaging/legacy-interactive-message-field-guide/#option_groups
+
+        Args:
+            label: Text to display at the top of this group of options.
+            options: A list of no more than 100 Option objects.
+        """  # noqa prevent flake8 blowing up on the long URL
+        # default_type=PlainTextObject.type is for backward-compatibility
+        self._label: Optional[TextObject] = TextObject.parse(label, default_type=PlainTextObject.type)  # type: ignore[arg-type] # noqa: E501
+        self.label: Optional[str] = self._label.text if self._label else None
+        self.options = Option.parse_all(options)  # compatible with version 2.5
+        show_unknown_key_warning(self, others)
+
+    @JsonValidator(f"label attribute cannot exceed {label_max_length} characters")
+    def _validate_label_length(self):
+        return self.label is None or len(self.label) <= self.label_max_length
+
+    @JsonValidator(f"options attribute cannot exceed {options_max_length} elements")
+    def _validate_options_length(self):
+        return self.options is None or len(self.options) <= self.options_max_length
+
+    @classmethod
+    def parse_all(
+        cls, option_groups: Optional[Sequence[Union[Dict[str, Any], "OptionGroup"]]]
+    ) -> Optional[List["OptionGroup"]]:
+        if option_groups is None:
+            return None
+        option_group_objects = []
+        for o in option_groups:
+            if isinstance(o, dict):
+                d = copy.copy(o)
+                option_group_objects.append(OptionGroup(**d))
+            elif isinstance(o, OptionGroup):
+                option_group_objects.append(o)
+            else:
+                cls.logger.warning(f"Unknown option group object detected and skipped ({o})")
+        return option_group_objects
+
+    def to_dict(self, option_type: str = "block") -> Dict[str, Any]:
+        self.validate_json()
+        dict_options = [o.to_dict(option_type) for o in self.options]  # type: ignore[union-attr]
+        if option_type == "dialog":
+            return {
+                "label": self.label,
+                "options": dict_options,
+            }
+        elif option_type == "action":
+            return {
+                "text": self.label,
+                "options": dict_options,
+            }
+        else:  # if option_type == "block"; this should be the most common case
+            dict_label: Dict[str, Any] = self._label.to_dict()  # type: ignore[union-attr]
+            return {
+                "label": dict_label,
+                "options": dict_options,
+            }
+
+

JSON must be retrieved with an explicit option_type - the Slack API has +different required formats in different situations

+

Create a group of Option objects - pass in a label (that will be part of the +UI) and a list of Option objects.

+

Blocks: +https://docs.slack.dev/reference/block-kit/composition-objects/option-group-object

+

Dialogs: +https://docs.slack.dev/legacy/legacy-dialogs/#select_elements

+

Legacy interactive attachments: +https://docs.slack.dev/legacy/legacy-messaging/legacy-interactive-message-field-guide/#option_groups

+

Args

+
+
label
+
Text to display at the top of this group of options.
+
options
+
A list of no more than 100 Option objects.
+
+

Ancestors

+ +

Class variables

+
+
var attributes : Set[str]
+
+

The type of the None singleton.

+
+
var label_max_length
+
+

The type of the None singleton.

+
+
var logger
+
+

The type of the None singleton.

+
+
var options_max_length
+
+

The type of the None singleton.

+
+
+

Static methods

+
+
+def parse_all(option_groups: Sequence[Dict[str, Any] | ForwardRef('OptionGroup')] | None) ‑> List[OptionGroup] | None +
+
+
+
+
+

Inherited members

+ +
+
+class OverflowMenuElement +(*,
action_id: str | None = None,
options: Sequence[Option],
confirm: dict | ConfirmObject | None = None,
**others: dict)
+
+
+
+ +Expand source code + +
class OverflowMenuElement(InteractiveElement):
+    type = "overflow"
+    options_min_length = 1
+    options_max_length = 5
+
+    @property
+    def attributes(self) -> Set[str]:  # type: ignore[override]
+        return super().attributes.union({"confirm", "options"})
+
+    def __init__(
+        self,
+        *,
+        action_id: Optional[str] = None,
+        options: Sequence[Option],
+        confirm: Optional[Union[dict, ConfirmObject]] = None,
+        **others: dict,
+    ):
+        """
+        This is like a cross between a button and a select menu - when a user clicks
+        on this overflow button, they will be presented with a list of options to
+        choose from. Unlike the select menu, there is no typeahead field, and the
+        button always appears with an ellipsis ("…") rather than customisable text.
+
+        As such, it is usually used if you want a more compact layout than a select
+        menu, or to supply a list of less visually important actions after a row of
+        buttons. You can also specify simple URL links as overflow menu options,
+        instead of actions.
+
+        https://docs.slack.dev/reference/block-kit/block-elements/overflow-menu-element
+
+        Args:
+            action_id (required): An identifier for the action triggered when a menu option is selected.
+                You can use this when you receive an interaction payload to identify the source of the action.
+                Should be unique among all other action_ids in the containing block.
+                Maximum length for this field is 255 characters.
+            options (required): An array of option objects to display in the menu.
+                Maximum number of options is 5, minimum is 1.
+            confirm: A confirm object that defines an optional confirmation dialog that appears
+                after a menu item is selected.
+        """
+        super().__init__(action_id=action_id, type=self.type)
+        show_unknown_key_warning(self, others)
+
+        self.options = options
+        self.confirm = ConfirmObject.parse(confirm)  # type: ignore[arg-type]
+
+    @JsonValidator(f"options attribute must have between {options_min_length} " f"and {options_max_length} items")
+    def _validate_options_length(self) -> bool:
+        return self.options_min_length <= len(self.options) <= self.options_max_length
+
+

Block Elements are things that exists inside of your Blocks. +https://docs.slack.dev/reference/block-kit/block-elements/

+

This is like a cross between a button and a select menu - when a user clicks +on this overflow button, they will be presented with a list of options to +choose from. Unlike the select menu, there is no typeahead field, and the +button always appears with an ellipsis ("…") rather than customisable text.

+

As such, it is usually used if you want a more compact layout than a select +menu, or to supply a list of less visually important actions after a row of +buttons. You can also specify simple URL links as overflow menu options, +instead of actions.

+

https://docs.slack.dev/reference/block-kit/block-elements/overflow-menu-element

+

Args

+
+
action_id : required
+
An identifier for the action triggered when a menu option is selected. +You can use this when you receive an interaction payload to identify the source of the action. +Should be unique among all other action_ids in the containing block. +Maximum length for this field is 255 characters.
+
options : required
+
An array of option objects to display in the menu. +Maximum number of options is 5, minimum is 1.
+
confirm
+
A confirm object that defines an optional confirmation dialog that appears +after a menu item is selected.
+
+

Ancestors

+ +

Class variables

+
+
var options_max_length
+
+

The type of the None singleton.

+
+
var options_min_length
+
+

The type of the None singleton.

+
+
var type
+
+

The type of the None singleton.

+
+
+

Inherited members

+ +
+
+class PlainTextInputElement +(*,
action_id: str | None = None,
placeholder: str | dict | TextObject | None = None,
initial_value: str | None = None,
multiline: bool | None = None,
min_length: int | None = None,
max_length: int | None = None,
dispatch_action_config: dict | DispatchActionConfig | None = None,
focus_on_load: bool | None = None,
**others: dict)
+
+
+
+ +Expand source code + +
class PlainTextInputElement(InputInteractiveElement):
+    type = "plain_text_input"
+
+    @property
+    def attributes(self) -> Set[str]:  # type: ignore[override]
+        return super().attributes.union(
+            {
+                "initial_value",
+                "multiline",
+                "min_length",
+                "max_length",
+                "dispatch_action_config",
+            }
+        )
+
+    def __init__(
+        self,
+        *,
+        action_id: Optional[str] = None,
+        placeholder: Optional[Union[str, dict, TextObject]] = None,
+        initial_value: Optional[str] = None,
+        multiline: Optional[bool] = None,
+        min_length: Optional[int] = None,
+        max_length: Optional[int] = None,
+        dispatch_action_config: Optional[Union[dict, DispatchActionConfig]] = None,
+        focus_on_load: Optional[bool] = None,
+        **others: dict,
+    ):
+        """
+        A plain-text input, similar to the HTML <input> tag, creates a field
+        where a user can enter freeform data. It can appear as a single-line
+        field or a larger textarea using the multiline flag. Plain-text input
+        elements can be used inside of SectionBlocks and ActionsBlocks.
+        https://docs.slack.dev/reference/block-kit/block-elements/plain-text-input-element
+
+        Args:
+            action_id (required): An identifier for the input value when the parent modal is submitted.
+                You can use this when you receive a view_submission payload to identify the value of the input element.
+                Should be unique among all other action_ids in the containing block.
+                Maximum length for this field is 255 characters.
+            placeholder: A plain_text only text object that defines the placeholder text shown
+                in the plain-text input. Maximum length for the text in this field is 150 characters.
+            initial_value: The initial value in the plain-text input when it is loaded.
+            multiline: Indicates whether the input will be a single line (false) or a larger textarea (true).
+                Defaults to false.
+            min_length: The minimum length of input that the user must provide. If the user provides less,
+                they will receive an error. Maximum value is 3000.
+            max_length: The maximum length of input that the user can provide. If the user provides more,
+                they will receive an error.
+            dispatch_action_config: A dispatch configuration object that determines when
+                during text input the element returns a block_actions payload.
+            focus_on_load: Indicates whether the element will be set to auto focus within the view object.
+                Only one element can be set to true. Defaults to false.
+        """
+        super().__init__(
+            type=self.type,
+            action_id=action_id,
+            placeholder=TextObject.parse(placeholder, PlainTextObject.type),  # type: ignore[arg-type]
+            focus_on_load=focus_on_load,
+        )
+        show_unknown_key_warning(self, others)
+
+        self.initial_value = initial_value
+        self.multiline = multiline
+        self.min_length = min_length
+        self.max_length = max_length
+        self.dispatch_action_config = dispatch_action_config
+
+

Block Elements are things that exists inside of your Blocks. +https://docs.slack.dev/reference/block-kit/block-elements/

+

A plain-text input, similar to the HTML tag, creates a field +where a user can enter freeform data. It can appear as a single-line +field or a larger textarea using the multiline flag. Plain-text input +elements can be used inside of SectionBlocks and ActionsBlocks. +https://docs.slack.dev/reference/block-kit/block-elements/plain-text-input-element

+

Args

+
+
action_id : required
+
An identifier for the input value when the parent modal is submitted. +You can use this when you receive a view_submission payload to identify the value of the input element. +Should be unique among all other action_ids in the containing block. +Maximum length for this field is 255 characters.
+
placeholder
+
A plain_text only text object that defines the placeholder text shown +in the plain-text input. Maximum length for the text in this field is 150 characters.
+
initial_value
+
The initial value in the plain-text input when it is loaded.
+
multiline
+
Indicates whether the input will be a single line (false) or a larger textarea (true). +Defaults to false.
+
min_length
+
The minimum length of input that the user must provide. If the user provides less, +they will receive an error. Maximum value is 3000.
+
max_length
+
The maximum length of input that the user can provide. If the user provides more, +they will receive an error.
+
dispatch_action_config
+
A dispatch configuration object that determines when +during text input the element returns a block_actions payload.
+
focus_on_load
+
Indicates whether the element will be set to auto focus within the view object. +Only one element can be set to true. Defaults to false.
+
+

Ancestors

+ +

Class variables

+
+
var type
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
prop attributes : Set[str]
+
+
+ +Expand source code + +
@property
+def attributes(self) -> Set[str]:  # type: ignore[override]
+    return super().attributes.union(
+        {
+            "initial_value",
+            "multiline",
+            "min_length",
+            "max_length",
+            "dispatch_action_config",
+        }
+    )
+
+

Build an unordered collection of unique elements.

+
+
+

Inherited members

+ +
+
+class PlainTextObject +(*, text: str, emoji: bool | None = None) +
+
+
+ +Expand source code + +
class PlainTextObject(TextObject):
+    """plain_text typed text object"""
+
+    type = "plain_text"
+
+    @property
+    def attributes(self) -> Set[str]:  # type: ignore[override]
+        return super().attributes.union({"emoji"})
+
+    def __init__(self, *, text: str, emoji: Optional[bool] = None):
+        """A plain text object, meaning markdown characters will not be parsed as
+        formatting information.
+        https://docs.slack.dev/reference/block-kit/composition-objects/text-object
+
+        Args:
+            text (required): The text for the block. This field accepts any of the standard text formatting markup
+                when type is mrkdwn.
+            emoji: Indicates whether emojis in a text field should be escaped into the colon emoji format.
+                This field is only usable when type is plain_text.
+        """
+        super().__init__(text=text, type=self.type)
+        self.emoji = emoji
+
+    @staticmethod
+    def from_str(text: str) -> "PlainTextObject":
+        return PlainTextObject(text=text, emoji=True)
+
+    @staticmethod
+    def direct_from_string(text: str) -> Dict[str, Any]:
+        """Transforms a string into the required object shape to act as a PlainTextObject"""
+        return PlainTextObject.from_str(text).to_dict()
+
+

plain_text typed text object

+

A plain text object, meaning markdown characters will not be parsed as +formatting information. +https://docs.slack.dev/reference/block-kit/composition-objects/text-object

+

Args

+
+
text : required
+
The text for the block. This field accepts any of the standard text formatting markup +when type is mrkdwn.
+
emoji
+
Indicates whether emojis in a text field should be escaped into the colon emoji format. +This field is only usable when type is plain_text.
+
+

Ancestors

+ +

Class variables

+
+
var type
+
+

The type of the None singleton.

+
+
+

Static methods

+
+
+def direct_from_string(text: str) ‑> Dict[str, Any] +
+
+
+ +Expand source code + +
@staticmethod
+def direct_from_string(text: str) -> Dict[str, Any]:
+    """Transforms a string into the required object shape to act as a PlainTextObject"""
+    return PlainTextObject.from_str(text).to_dict()
+
+

Transforms a string into the required object shape to act as a PlainTextObject

+
+
+def from_str(text: str) ‑> PlainTextObject +
+
+
+ +Expand source code + +
@staticmethod
+def from_str(text: str) -> "PlainTextObject":
+    return PlainTextObject(text=text, emoji=True)
+
+
+
+
+

Instance variables

+
+
prop attributes : Set[str]
+
+
+ +Expand source code + +
@property
+def attributes(self) -> Set[str]:  # type: ignore[override]
+    return super().attributes.union({"emoji"})
+
+

Build an unordered collection of unique elements.

+
+
+

Inherited members

+ +
+
+class RadioButtonsElement +(*,
action_id: str | None = None,
options: Sequence[dict | Option] | None = None,
initial_option: dict | Option | None = None,
confirm: dict | ConfirmObject | None = None,
focus_on_load: bool | None = None,
**others: dict)
+
+
+
+ +Expand source code + +
class RadioButtonsElement(InputInteractiveElement):
+    type = "radio_buttons"
+
+    @property
+    def attributes(self) -> Set[str]:  # type: ignore[override]
+        return super().attributes.union({"options", "initial_option"})
+
+    def __init__(
+        self,
+        *,
+        action_id: Optional[str] = None,
+        options: Optional[Sequence[Union[dict, Option]]] = None,
+        initial_option: Optional[Union[dict, Option]] = None,
+        confirm: Optional[Union[dict, ConfirmObject]] = None,
+        focus_on_load: Optional[bool] = None,
+        **others: dict,
+    ):
+        """A radio button group that allows a user to choose one item from a list of possible options.
+        https://docs.slack.dev/reference/block-kit/block-elements/radio-button-group-element
+
+        Args:
+            action_id (required): An identifier for the action triggered when the radio button group is changed.
+                You can use this when you receive an interaction payload to identify the source of the action.
+                Should be unique among all other action_ids in the containing block.
+                Maximum length for this field is 255 characters.
+            options (required): An array of option objects. A maximum of 10 options are allowed.
+            initial_option: An option object that exactly matches one of the options.
+                This option will be selected when the radio button group initially loads.
+            confirm: A confirm object that defines an optional confirmation dialog that appears
+                after clicking one of the radio buttons in this element.
+            focus_on_load: Indicates whether the element will be set to auto focus within the view object.
+                Only one element can be set to true. Defaults to false.
+        """
+        super().__init__(
+            type=self.type,
+            action_id=action_id,
+            confirm=ConfirmObject.parse(confirm),  # type: ignore[arg-type]
+            focus_on_load=focus_on_load,
+        )
+        show_unknown_key_warning(self, others)
+
+        self.options = options
+        self.initial_option = initial_option
+
+

Block Elements are things that exists inside of your Blocks. +https://docs.slack.dev/reference/block-kit/block-elements/

+

A radio button group that allows a user to choose one item from a list of possible options. +https://docs.slack.dev/reference/block-kit/block-elements/radio-button-group-element

+

Args

+
+
action_id : required
+
An identifier for the action triggered when the radio button group is changed. +You can use this when you receive an interaction payload to identify the source of the action. +Should be unique among all other action_ids in the containing block. +Maximum length for this field is 255 characters.
+
options : required
+
An array of option objects. A maximum of 10 options are allowed.
+
initial_option
+
An option object that exactly matches one of the options. +This option will be selected when the radio button group initially loads.
+
confirm
+
A confirm object that defines an optional confirmation dialog that appears +after clicking one of the radio buttons in this element.
+
focus_on_load
+
Indicates whether the element will be set to auto focus within the view object. +Only one element can be set to true. Defaults to false.
+
+

Ancestors

+ +

Class variables

+
+
var type
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
prop attributes : Set[str]
+
+
+ +Expand source code + +
@property
+def attributes(self) -> Set[str]:  # type: ignore[override]
+    return super().attributes.union({"options", "initial_option"})
+
+

Build an unordered collection of unique elements.

+
+
+

Inherited members

+ +
+
+class RawTextObject +(*, text: str) +
+
+
+ +Expand source code + +
class RawTextObject(TextObject):
+    """raw_text typed text object"""
+
+    type = "raw_text"
+
+    @property
+    def attributes(self) -> Set[str]:  # type: ignore[override]
+        return {"text", "type"}
+
+    def __init__(self, *, text: str):
+        """A raw text object used in table block cells.
+        https://docs.slack.dev/reference/block-kit/composition-objects/text-object/
+        https://docs.slack.dev/reference/block-kit/blocks/table-block
+
+        Args:
+            text (required): The text content for the table block cell.
+        """
+        super().__init__(text=text, type=self.type)
+
+    @staticmethod
+    def from_str(text: str) -> "RawTextObject":
+        """Transforms a string into a RawTextObject"""
+        return RawTextObject(text=text)
+
+    @staticmethod
+    def direct_from_string(text: str) -> Dict[str, Any]:
+        """Transforms a string into the required object shape to act as a RawTextObject"""
+        return RawTextObject.from_str(text).to_dict()
+
+    @JsonValidator("text attribute must have at least 1 character")
+    def _validate_text_min_length(self):
+        return len(self.text) >= 1
+
+

raw_text typed text object

+

A raw text object used in table block cells. +https://docs.slack.dev/reference/block-kit/composition-objects/text-object/ +https://docs.slack.dev/reference/block-kit/blocks/table-block

+

Args

+
+
text : required
+
The text content for the table block cell.
+
+

Ancestors

+ +

Class variables

+
+
var type
+
+

The type of the None singleton.

+
+
+

Static methods

+
+
+def direct_from_string(text: str) ‑> Dict[str, Any] +
+
+
+ +Expand source code + +
@staticmethod
+def direct_from_string(text: str) -> Dict[str, Any]:
+    """Transforms a string into the required object shape to act as a RawTextObject"""
+    return RawTextObject.from_str(text).to_dict()
+
+

Transforms a string into the required object shape to act as a RawTextObject

+
+
+def from_str(text: str) ‑> RawTextObject +
+
+
+ +Expand source code + +
@staticmethod
+def from_str(text: str) -> "RawTextObject":
+    """Transforms a string into a RawTextObject"""
+    return RawTextObject(text=text)
+
+

Transforms a string into a RawTextObject

+
+
+

Instance variables

+
+
prop attributes : Set[str]
+
+
+ +Expand source code + +
@property
+def attributes(self) -> Set[str]:  # type: ignore[override]
+    return {"text", "type"}
+
+

Build an unordered collection of unique elements.

+
+
+

Inherited members

+ +
+
+class RichTextBlock +(*,
elements: Sequence[dict | RichTextElement],
block_id: str | None = None,
**others: dict)
+
+
+
+ +Expand source code + +
class RichTextBlock(Block):
+    type = "rich_text"
+
+    @property
+    def attributes(self) -> Set[str]:  # type: ignore[override]
+        return super().attributes.union({"elements"})
+
+    def __init__(
+        self,
+        *,
+        elements: Sequence[Union[dict, RichTextElement]],
+        block_id: Optional[str] = None,
+        **others: dict,
+    ):
+        """A block that is used to hold interactive elements.
+        https://docs.slack.dev/reference/block-kit/blocks/rich-text-block
+
+        Args:
+            elements (required): An array of rich text objects -
+                rich_text_section, rich_text_list, rich_text_quote, rich_text_preformatted
+            block_id: A unique identifier for a block. If not specified, one will be generated.
+                Maximum length for this field is 255 characters.
+                block_id should be unique for each message or view and each iteration of a message or view.
+                If a message or view is updated, use a new block_id.
+        """
+        super().__init__(type=self.type, block_id=block_id)
+        show_unknown_key_warning(self, others)
+
+        self.elements = BlockElement.parse_all(elements)
+
+

Blocks are a series of components that can be combined +to create visually rich and compellingly interactive messages. +https://docs.slack.dev/reference/block-kit/blocks

+

A block that is used to hold interactive elements. +https://docs.slack.dev/reference/block-kit/blocks/rich-text-block

+

Args

+
+
elements : required
+
An array of rich text objects - +rich_text_section, rich_text_list, rich_text_quote, rich_text_preformatted
+
block_id
+
A unique identifier for a block. If not specified, one will be generated. +Maximum length for this field is 255 characters. +block_id should be unique for each message or view and each iteration of a message or view. +If a message or view is updated, use a new block_id.
+
+

Ancestors

+ +

Class variables

+
+
var type
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
prop attributes : Set[str]
+
+
+ +Expand source code + +
@property
+def attributes(self) -> Set[str]:  # type: ignore[override]
+    return super().attributes.union({"elements"})
+
+

Build an unordered collection of unique elements.

+
+
+

Inherited members

+ +
+
+class RichTextElement +(*, type: str | None = None, subtype: str | None = None, **others: dict) +
+
+
+ +Expand source code + +
class RichTextElement(BlockElement):
+    pass
+
+

Block Elements are things that exists inside of your Blocks. +https://docs.slack.dev/reference/block-kit/block-elements/

+

Ancestors

+ +

Subclasses

+ +

Inherited members

+ +
+
+class RichTextElementParts +
+
+
+ +Expand source code + +
class RichTextElementParts:
+    class TextStyle:
+        def __init__(
+            self,
+            *,
+            bold: Optional[bool] = None,
+            italic: Optional[bool] = None,
+            strike: Optional[bool] = None,
+            code: Optional[bool] = None,
+            underline: Optional[bool] = None,
+        ):
+            self.bold = bold
+            self.italic = italic
+            self.strike = strike
+            self.code = code
+            self.underline = underline
+
+        def to_dict(self, *args) -> dict:
+            result = {
+                "bold": self.bold,
+                "italic": self.italic,
+                "strike": self.strike,
+                "code": self.code,
+                "underline": self.underline,
+            }
+            return {k: v for k, v in result.items() if v is not None}
+
+    class Text(RichTextElement):
+        type = "text"
+
+        @property
+        def attributes(self) -> Set[str]:  # type: ignore[override]
+            return super().attributes.union({"text", "style"})
+
+        def __init__(
+            self,
+            *,
+            text: str,
+            style: Optional[Union[dict, "RichTextElementParts.TextStyle"]] = None,
+            **others: dict,
+        ):
+            super().__init__(type=self.type)
+            show_unknown_key_warning(self, others)
+            self.text = text
+            self.style = style
+
+    class Channel(RichTextElement):
+        type = "channel"
+
+        @property
+        def attributes(self) -> Set[str]:  # type: ignore[override]
+            return super().attributes.union({"channel_id", "style"})
+
+        def __init__(
+            self,
+            *,
+            channel_id: str,
+            style: Optional[Union[dict, "RichTextElementParts.TextStyle"]] = None,
+            **others: dict,
+        ):
+            super().__init__(type=self.type)
+            show_unknown_key_warning(self, others)
+            self.channel_id = channel_id
+            self.style = style
+
+    class User(RichTextElement):
+        type = "user"
+
+        @property
+        def attributes(self) -> Set[str]:  # type: ignore[override]
+            return super().attributes.union({"user_id", "style"})
+
+        def __init__(
+            self,
+            *,
+            user_id: str,
+            style: Optional[Union[dict, "RichTextElementParts.TextStyle"]] = None,
+            **others: dict,
+        ):
+            super().__init__(type=self.type)
+            show_unknown_key_warning(self, others)
+            self.user_id = user_id
+            self.style = style
+
+    class Emoji(RichTextElement):
+        type = "emoji"
+
+        @property
+        def attributes(self) -> Set[str]:  # type: ignore[override]
+            return super().attributes.union({"name", "skin_tone", "unicode", "style"})
+
+        def __init__(
+            self,
+            *,
+            name: str,
+            skin_tone: Optional[int] = None,
+            unicode: Optional[str] = None,
+            style: Optional[Union[dict, "RichTextElementParts.TextStyle"]] = None,
+            **others: dict,
+        ):
+            super().__init__(type=self.type)
+            show_unknown_key_warning(self, others)
+            self.name = name
+            self.skin_tone = skin_tone
+            self.unicode = unicode
+            self.style = style
+
+    class Link(RichTextElement):
+        type = "link"
+
+        @property
+        def attributes(self) -> Set[str]:  # type: ignore[override]
+            return super().attributes.union({"url", "text", "style"})
+
+        def __init__(
+            self,
+            *,
+            url: str,
+            text: Optional[str] = None,
+            style: Optional[Union[dict, "RichTextElementParts.TextStyle"]] = None,
+            **others: dict,
+        ):
+            super().__init__(type=self.type)
+            show_unknown_key_warning(self, others)
+            self.url = url
+            self.text = text
+            self.style = style
+
+    class Team(RichTextElement):
+        type = "team"
+
+        @property
+        def attributes(self) -> Set[str]:  # type: ignore[override]
+            return super().attributes.union({"team_id", "style"})
+
+        def __init__(
+            self,
+            *,
+            team_id: str,
+            style: Optional[Union[dict, "RichTextElementParts.TextStyle"]] = None,
+            **others: dict,
+        ):
+            super().__init__(type=self.type)
+            show_unknown_key_warning(self, others)
+            self.team_id = team_id
+            self.style = style
+
+    class UserGroup(RichTextElement):
+        type = "usergroup"
+
+        @property
+        def attributes(self) -> Set[str]:  # type: ignore[override]
+            return super().attributes.union({"usergroup_id", "style"})
+
+        def __init__(
+            self,
+            *,
+            usergroup_id: str,
+            style: Optional[Union[dict, "RichTextElementParts.TextStyle"]] = None,
+            **others: dict,
+        ):
+            super().__init__(type=self.type)
+            show_unknown_key_warning(self, others)
+            self.usergroup_id = usergroup_id
+            self.style = style
+
+    class Date(RichTextElement):
+        type = "date"
+
+        @property
+        def attributes(self) -> Set[str]:  # type: ignore[override]
+            return super().attributes.union({"timestamp", "format", "url", "fallback"})
+
+        def __init__(
+            self,
+            *,
+            timestamp: int,
+            format: str,
+            url: Optional[str] = None,
+            fallback: Optional[str] = None,
+            **others: dict,
+        ):
+            super().__init__(type=self.type)
+            show_unknown_key_warning(self, others)
+            self.timestamp = timestamp
+            self.format = format
+            self.url = url
+            self.fallback = fallback
+
+    class Broadcast(RichTextElement):
+        type = "broadcast"
+
+        @property
+        def attributes(self) -> Set[str]:  # type: ignore[override]
+            return super().attributes.union({"range"})
+
+        def __init__(
+            self,
+            *,
+            range: str,  # channel, here, ..
+            **others: dict,
+        ):
+            super().__init__(type=self.type)
+            show_unknown_key_warning(self, others)
+            self.range = range
+
+    class Color(RichTextElement):
+        type = "color"
+
+        @property
+        def attributes(self) -> Set[str]:  # type: ignore[override]
+            return super().attributes.union({"value"})
+
+        def __init__(
+            self,
+            *,
+            value: str,
+            **others: dict,
+        ):
+            super().__init__(type=self.type)
+            show_unknown_key_warning(self, others)
+            self.value = value
+
+
+

Class variables

+
+
var Broadcast
+
+

Block Elements are things that exists inside of your Blocks. +https://docs.slack.dev/reference/block-kit/block-elements/

+
+
var Channel
+
+

Block Elements are things that exists inside of your Blocks. +https://docs.slack.dev/reference/block-kit/block-elements/

+
+
var Color
+
+

Block Elements are things that exists inside of your Blocks. +https://docs.slack.dev/reference/block-kit/block-elements/

+
+
var Date
+
+

Block Elements are things that exists inside of your Blocks. +https://docs.slack.dev/reference/block-kit/block-elements/

+
+
var Emoji
+
+

Block Elements are things that exists inside of your Blocks. +https://docs.slack.dev/reference/block-kit/block-elements/

+
+ +
+

Block Elements are things that exists inside of your Blocks. +https://docs.slack.dev/reference/block-kit/block-elements/

+
+
var Team
+
+

Block Elements are things that exists inside of your Blocks. +https://docs.slack.dev/reference/block-kit/block-elements/

+
+
var Text
+
+

Block Elements are things that exists inside of your Blocks. +https://docs.slack.dev/reference/block-kit/block-elements/

+
+
var TextStyle
+
+

The type of the None singleton.

+
+
var User
+
+

Block Elements are things that exists inside of your Blocks. +https://docs.slack.dev/reference/block-kit/block-elements/

+
+
var UserGroup
+
+

Block Elements are things that exists inside of your Blocks. +https://docs.slack.dev/reference/block-kit/block-elements/

+
+
+
+
+class RichTextInputElement +(*,
action_id: str | None = None,
placeholder: str | dict | TextObject | None = None,
initial_value: Dict[str, Any] | ForwardRef('RichTextBlock') | None = None,
dispatch_action_config: dict | DispatchActionConfig | None = None,
focus_on_load: bool | None = None,
**others: dict)
+
+
+
+ +Expand source code + +
class RichTextInputElement(InputInteractiveElement):
+    type = "rich_text_input"
+
+    @property
+    def attributes(self) -> Set[str]:  # type: ignore[override]
+        return super().attributes.union(
+            {
+                "initial_value",
+                "dispatch_action_config",
+            }
+        )
+
+    def __init__(
+        self,
+        *,
+        action_id: Optional[str] = None,
+        placeholder: Optional[Union[str, dict, TextObject]] = None,
+        # To avoid circular imports, the RichTextBlock type here is intentionally a string
+        initial_value: Optional[Union[Dict[str, Any], "RichTextBlock"]] = None,  # type: ignore[name-defined] # noqa: F821
+        dispatch_action_config: Optional[Union[dict, DispatchActionConfig]] = None,
+        focus_on_load: Optional[bool] = None,
+        **others: dict,
+    ):
+        super().__init__(
+            type=self.type,
+            action_id=action_id,
+            placeholder=TextObject.parse(placeholder, PlainTextObject.type),  # type: ignore[arg-type]
+            focus_on_load=focus_on_load,
+        )
+        show_unknown_key_warning(self, others)
+
+        self.initial_value = initial_value
+        self.dispatch_action_config = dispatch_action_config
+
+

Block Elements are things that exists inside of your Blocks. +https://docs.slack.dev/reference/block-kit/block-elements/

+

InteractiveElement that is usable in input blocks

+

We generally recommend using the concrete subclasses for better supports of available properties.

+

Ancestors

+ +

Class variables

+
+
var type
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
prop attributes : Set[str]
+
+
+ +Expand source code + +
@property
+def attributes(self) -> Set[str]:  # type: ignore[override]
+    return super().attributes.union(
+        {
+            "initial_value",
+            "dispatch_action_config",
+        }
+    )
+
+

Build an unordered collection of unique elements.

+
+
+

Inherited members

+ +
+
+class RichTextListElement +(*,
elements: Sequence[dict | RichTextElement],
style: str | None = None,
indent: int | None = None,
offset: int | None = None,
border: int | None = None,
**others: dict)
+
+
+
+ +Expand source code + +
class RichTextListElement(RichTextElement):
+    type = "rich_text_list"
+
+    @property
+    def attributes(self) -> Set[str]:  # type: ignore[override]
+        return super().attributes.union({"elements", "style", "indent", "offset", "border"})
+
+    def __init__(
+        self,
+        *,
+        elements: Sequence[Union[dict, RichTextElement]],
+        style: Optional[str] = None,  # bullet, ordered
+        indent: Optional[int] = None,
+        offset: Optional[int] = None,
+        border: Optional[int] = None,
+        **others: dict,
+    ):
+        super().__init__(type=self.type)
+        show_unknown_key_warning(self, others)
+        self.elements = BlockElement.parse_all(elements)
+        self.style = style
+        self.indent = indent
+        self.offset = offset
+        self.border = border
+
+

Block Elements are things that exists inside of your Blocks. +https://docs.slack.dev/reference/block-kit/block-elements/

+

Ancestors

+ +

Class variables

+
+
var type
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
prop attributes : Set[str]
+
+
+ +Expand source code + +
@property
+def attributes(self) -> Set[str]:  # type: ignore[override]
+    return super().attributes.union({"elements", "style", "indent", "offset", "border"})
+
+

Build an unordered collection of unique elements.

+
+
+

Inherited members

+ +
+
+class RichTextPreformattedElement +(*,
elements: Sequence[dict | RichTextElement],
border: int | None = None,
**others: dict)
+
+
+
+ +Expand source code + +
class RichTextPreformattedElement(RichTextElement):
+    type = "rich_text_preformatted"
+
+    @property
+    def attributes(self) -> Set[str]:  # type: ignore[override]
+        return super().attributes.union({"elements", "border"})
+
+    def __init__(
+        self,
+        *,
+        elements: Sequence[Union[dict, RichTextElement]],
+        border: Optional[int] = None,
+        **others: dict,
+    ):
+        super().__init__(type=self.type)
+        show_unknown_key_warning(self, others)
+        self.elements = BlockElement.parse_all(elements)
+        self.border = border
+
+

Block Elements are things that exists inside of your Blocks. +https://docs.slack.dev/reference/block-kit/block-elements/

+

Ancestors

+ +

Class variables

+
+
var type
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
prop attributes : Set[str]
+
+
+ +Expand source code + +
@property
+def attributes(self) -> Set[str]:  # type: ignore[override]
+    return super().attributes.union({"elements", "border"})
+
+

Build an unordered collection of unique elements.

+
+
+

Inherited members

+ +
+
+class RichTextQuoteElement +(*,
elements: Sequence[dict | RichTextElement],
**others: dict)
+
+
+
+ +Expand source code + +
class RichTextQuoteElement(RichTextElement):
+    type = "rich_text_quote"
+
+    @property
+    def attributes(self) -> Set[str]:  # type: ignore[override]
+        return super().attributes.union({"elements"})
+
+    def __init__(
+        self,
+        *,
+        elements: Sequence[Union[dict, RichTextElement]],
+        **others: dict,
+    ):
+        super().__init__(type=self.type)
+        show_unknown_key_warning(self, others)
+        self.elements = BlockElement.parse_all(elements)
+
+

Block Elements are things that exists inside of your Blocks. +https://docs.slack.dev/reference/block-kit/block-elements/

+

Ancestors

+ +

Class variables

+
+
var type
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
prop attributes : Set[str]
+
+
+ +Expand source code + +
@property
+def attributes(self) -> Set[str]:  # type: ignore[override]
+    return super().attributes.union({"elements"})
+
+

Build an unordered collection of unique elements.

+
+
+

Inherited members

+ +
+
+class RichTextSectionElement +(*,
elements: Sequence[dict | RichTextElement],
**others: dict)
+
+
+
+ +Expand source code + +
class RichTextSectionElement(RichTextElement):
+    type = "rich_text_section"
+
+    @property
+    def attributes(self) -> Set[str]:  # type: ignore[override]
+        return super().attributes.union({"elements"})
+
+    def __init__(
+        self,
+        *,
+        elements: Sequence[Union[dict, RichTextElement]],
+        **others: dict,
+    ):
+        super().__init__(type=self.type)
+        show_unknown_key_warning(self, others)
+        self.elements = BlockElement.parse_all(elements)
+
+

Block Elements are things that exists inside of your Blocks. +https://docs.slack.dev/reference/block-kit/block-elements/

+

Ancestors

+ +

Class variables

+
+
var type
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
prop attributes : Set[str]
+
+
+ +Expand source code + +
@property
+def attributes(self) -> Set[str]:  # type: ignore[override]
+    return super().attributes.union({"elements"})
+
+

Build an unordered collection of unique elements.

+
+
+

Inherited members

+ +
+
+class SectionBlock +(*,
block_id: str | None = None,
text: str | dict | TextObject | None = None,
fields: Sequence[str | dict | TextObject] | None = None,
accessory: dict | BlockElement | None = None,
expand: bool | None = None,
**others: dict)
+
+
+
+ +Expand source code + +
class SectionBlock(Block):
+    type = "section"
+    fields_max_length = 10
+    text_max_length = 3000
+
+    @property
+    def attributes(self) -> Set[str]:  # type: ignore[override]
+        return super().attributes.union({"text", "fields", "accessory", "expand"})
+
+    def __init__(
+        self,
+        *,
+        block_id: Optional[str] = None,
+        text: Optional[Union[str, dict, TextObject]] = None,
+        fields: Optional[Sequence[Union[str, dict, TextObject]]] = None,
+        accessory: Optional[Union[dict, BlockElement]] = None,
+        expand: Optional[bool] = None,
+        **others: dict,
+    ):
+        """A section is one of the most flexible blocks available.
+        https://docs.slack.dev/reference/block-kit/blocks/section-block
+
+        Args:
+            block_id (required): A string acting as a unique identifier for a block.
+                If not specified, one will be generated.
+                You can use this block_id when you receive an interaction payload to identify the source of the action.
+                Maximum length for this field is 255 characters.
+                block_id should be unique for each message and each iteration of a message.
+                If a message is updated, use a new block_id.
+            text (preferred): The text for the block, in the form of a text object.
+                Maximum length for the text in this field is 3000 characters.
+                This field is not required if a valid array of fields objects is provided instead.
+            fields (required if no text is provided): Required if no text is provided.
+                An array of text objects. Any text objects included with fields will be rendered
+                in a compact format that allows for 2 columns of side-by-side text.
+                Maximum number of items is 10. Maximum length for the text in each item is 2000 characters.
+            accessory: One of the available element objects.
+            expand: Whether or not this section block's text should always expand when rendered.
+                If false or not provided, it may be rendered with a 'see more' option to expand and show the full text.
+                For AI Assistant apps, this allows the app to post long messages without users needing
+                to click 'see more' to expand the message.
+        """
+        super().__init__(type=self.type, block_id=block_id)
+        show_unknown_key_warning(self, others)
+
+        self.text = TextObject.parse(text)  # type: ignore[arg-type]
+        field_objects = []
+        for f in fields or []:
+            if isinstance(f, str):
+                field_objects.append(MarkdownTextObject.from_str(f))
+            elif isinstance(f, TextObject):
+                field_objects.append(f)  # type: ignore[arg-type]
+            elif isinstance(f, dict) and "type" in f:
+                d = copy.copy(f)
+                t = d.pop("type")
+                if t == MarkdownTextObject.type:
+                    field_objects.append(MarkdownTextObject(**d))
+                else:
+                    field_objects.append(PlainTextObject(**d))  # type: ignore[arg-type]
+            else:
+                self.logger.warning(f"Unsupported filed detected and skipped {f}")
+        self.fields = field_objects
+        self.accessory = BlockElement.parse(accessory)  # type: ignore[arg-type]
+        self.expand = expand
+
+    @JsonValidator("text or fields attribute must be specified")
+    def _validate_text_or_fields_populated(self):
+        return self.text is not None or self.fields
+
+    @JsonValidator(f"fields attribute cannot exceed {fields_max_length} items")
+    def _validate_fields_length(self):
+        return self.fields is None or len(self.fields) <= self.fields_max_length
+
+    @JsonValidator(f"text attribute cannot exceed {text_max_length} characters")
+    def _validate_alt_text_length(self):
+        return self.text is None or len(self.text.text) <= self.text_max_length
+
+

Blocks are a series of components that can be combined +to create visually rich and compellingly interactive messages. +https://docs.slack.dev/reference/block-kit/blocks

+

A section is one of the most flexible blocks available. +https://docs.slack.dev/reference/block-kit/blocks/section-block

+

Args

+
+
block_id : required
+
A string acting as a unique identifier for a block. +If not specified, one will be generated. +You can use this block_id when you receive an interaction payload to identify the source of the action. +Maximum length for this field is 255 characters. +block_id should be unique for each message and each iteration of a message. +If a message is updated, use a new block_id.
+
text : preferred
+
The text for the block, in the form of a text object. +Maximum length for the text in this field is 3000 characters. +This field is not required if a valid array of fields objects is provided instead.
+
fields : required if no text is provided
+
Required if no text is provided. +An array of text objects. Any text objects included with fields will be rendered +in a compact format that allows for 2 columns of side-by-side text. +Maximum number of items is 10. Maximum length for the text in each item is 2000 characters.
+
accessory
+
One of the available element objects.
+
expand
+
Whether or not this section block's text should always expand when rendered. +If false or not provided, it may be rendered with a 'see more' option to expand and show the full text. +For AI Assistant apps, this allows the app to post long messages without users needing +to click 'see more' to expand the message.
+
+

Ancestors

+ +

Class variables

+
+
var fields_max_length
+
+

The type of the None singleton.

+
+
var text_max_length
+
+

The type of the None singleton.

+
+
var type
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
prop attributes : Set[str]
+
+
+ +Expand source code + +
@property
+def attributes(self) -> Set[str]:  # type: ignore[override]
+    return super().attributes.union({"text", "fields", "accessory", "expand"})
+
+

Build an unordered collection of unique elements.

+
+
+

Inherited members

+ +
+
+class SelectElement +(*,
action_id: str | None = None,
placeholder: str | None = None,
options: Sequence[Option] | None = None,
option_groups: Sequence[OptionGroup] | None = None,
initial_option: Option | None = None,
confirm: dict | ConfirmObject | None = None,
focus_on_load: bool | None = None,
**others: dict)
+
+
+
+ +Expand source code + +
class SelectElement(InputInteractiveElement):
+    type = "static_select"
+    options_max_length = 100
+    option_groups_max_length = 100
+
+    @property
+    def attributes(self) -> Set[str]:  # type: ignore[override]
+        return super().attributes.union({"options", "option_groups", "initial_option"})
+
+    def __init__(
+        self,
+        *,
+        action_id: Optional[str] = None,
+        placeholder: Optional[str] = None,
+        options: Optional[Sequence[Option]] = None,
+        option_groups: Optional[Sequence[OptionGroup]] = None,
+        initial_option: Optional[Option] = None,
+        confirm: Optional[Union[dict, ConfirmObject]] = None,
+        focus_on_load: Optional[bool] = None,
+        **others: dict,
+    ):
+        """This is the simplest form of select menu, with a static list of options passed in when defining the element.
+        https://docs.slack.dev/reference/block-kit/block-elements/select-menu-element#static_select
+
+        Args:
+            action_id (required): An identifier for the action triggered when a menu option is selected.
+                You can use this when you receive an interaction payload to identify the source of the action.
+                Should be unique among all other action_ids in the containing block.
+                Maximum length for this field is 255 characters.
+            placeholder (required): A plain_text only text object that defines the placeholder text shown on the menu.
+                Maximum length for the text in this field is 150 characters.
+            options (either options or option_groups is required): An array of option objects.
+                Maximum number of options is 100.
+                If option_groups is specified, this field should not be.
+            option_groups (either options or option_groups is required): An array of option group objects.
+                Maximum number of option groups is 100.
+                If options is specified, this field should not be.
+            initial_option: A single option that exactly matches one of the options or option_groups.
+                This option will be selected when the menu initially loads.
+            confirm: A confirm object that defines an optional confirmation dialog
+                that appears after a menu item is selected.
+            focus_on_load: Indicates whether the element will be set to auto focus within the view object.
+                Only one element can be set to true. Defaults to false.
+        """
+        super().__init__(
+            type=self.type,
+            action_id=action_id,
+            placeholder=TextObject.parse(placeholder, PlainTextObject.type),  # type: ignore[arg-type]
+            confirm=ConfirmObject.parse(confirm),  # type: ignore[arg-type]
+            focus_on_load=focus_on_load,
+        )
+        show_unknown_key_warning(self, others)
+
+        self.options = options
+        self.option_groups = option_groups
+        self.initial_option = initial_option
+
+    @JsonValidator(f"options attribute cannot exceed {options_max_length} elements")
+    def _validate_options_length(self) -> bool:
+        return self.options is None or len(self.options) <= self.options_max_length
+
+    @JsonValidator(f"option_groups attribute cannot exceed {option_groups_max_length} elements")
+    def _validate_option_groups_length(self) -> bool:
+        return self.option_groups is None or len(self.option_groups) <= self.option_groups_max_length
+
+    @JsonValidator("options and option_groups cannot both be specified")
+    def _validate_options_and_option_groups_both_specified(self) -> bool:
+        return not (self.options is not None and self.option_groups is not None)
+
+    @JsonValidator("options or option_groups must be specified")
+    def _validate_neither_options_or_option_groups_is_specified(self) -> bool:
+        return self.options is not None or self.option_groups is not None
+
+

Block Elements are things that exists inside of your Blocks. +https://docs.slack.dev/reference/block-kit/block-elements/

+

This is the simplest form of select menu, with a static list of options passed in when defining the element. +https://docs.slack.dev/reference/block-kit/block-elements/select-menu-element#static_select

+

Args

+
+
action_id : required
+
An identifier for the action triggered when a menu option is selected. +You can use this when you receive an interaction payload to identify the source of the action. +Should be unique among all other action_ids in the containing block. +Maximum length for this field is 255 characters.
+
placeholder : required
+
A plain_text only text object that defines the placeholder text shown on the menu. +Maximum length for the text in this field is 150 characters.
+
options : either options or option_groups is required
+
An array of option objects. +Maximum number of options is 100. +If option_groups is specified, this field should not be.
+
option_groups : either options or option_groups is required
+
An array of option group objects. +Maximum number of option groups is 100. +If options is specified, this field should not be.
+
initial_option
+
A single option that exactly matches one of the options or option_groups. +This option will be selected when the menu initially loads.
+
confirm
+
A confirm object that defines an optional confirmation dialog +that appears after a menu item is selected.
+
focus_on_load
+
Indicates whether the element will be set to auto focus within the view object. +Only one element can be set to true. Defaults to false.
+
+

Ancestors

+ +

Class variables

+
+
var option_groups_max_length
+
+

The type of the None singleton.

+
+
var options_max_length
+
+

The type of the None singleton.

+
+
var type
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
prop attributes : Set[str]
+
+
+ +Expand source code + +
@property
+def attributes(self) -> Set[str]:  # type: ignore[override]
+    return super().attributes.union({"options", "option_groups", "initial_option"})
+
+

Build an unordered collection of unique elements.

+
+
+

Inherited members

+ +
+
+class StaticMultiSelectElement +(*,
placeholder: str | dict | TextObject | None = None,
action_id: str | None = None,
options: Sequence[Option] | None = None,
option_groups: Sequence[OptionGroup] | None = None,
initial_options: Sequence[Option] | None = None,
confirm: dict | ConfirmObject | None = None,
max_selected_items: int | None = None,
focus_on_load: bool | None = None,
**others: dict)
+
+
+
+ +Expand source code + +
class StaticMultiSelectElement(InputInteractiveElement):
+    type = "multi_static_select"
+    options_max_length = 100
+    option_groups_max_length = 100
+
+    @property
+    def attributes(self) -> Set[str]:  # type: ignore[override]
+        return super().attributes.union({"options", "option_groups", "initial_options", "max_selected_items"})
+
+    def __init__(
+        self,
+        *,
+        placeholder: Optional[Union[str, dict, TextObject]] = None,
+        action_id: Optional[str] = None,
+        options: Optional[Sequence[Option]] = None,
+        option_groups: Optional[Sequence[OptionGroup]] = None,
+        initial_options: Optional[Sequence[Option]] = None,
+        confirm: Optional[Union[dict, ConfirmObject]] = None,
+        max_selected_items: Optional[int] = None,
+        focus_on_load: Optional[bool] = None,
+        **others: dict,
+    ):
+        """
+        This is the simplest form of select menu, with a static list of options passed in when defining the element.
+        https://docs.slack.dev/reference/block-kit/block-elements/multi-select-menu-element#static_multi_select
+
+        Args:
+            placeholder (required): A plain_text only text object that defines the placeholder text shown on the menu.
+                Maximum length for the text in this field is 150 characters.
+            action_id (required): An identifier for the action triggered when a menu option is selected.
+                You can use this when you receive an interaction payload to identify the source of the action.
+                Should be unique among all other action_ids in the containing block.
+                Maximum length for this field is 255 characters.
+            options (either options or option_groups is required): An array of option objects.
+                Maximum number of options is 100.
+                If option_groups is specified, this field should not be.
+            option_groups (either options or option_groups is required): An array of option group objects.
+                Maximum number of option groups is 100.
+                If options is specified, this field should not be.
+            initial_options: An array of option objects that exactly match one or more of the options
+                within options or option_groups. These options will be selected when the menu initially loads.
+            confirm: A confirm object that defines an optional confirmation dialog
+                that appears before the multi-select choices are submitted.
+            max_selected_items: Specifies the maximum number of items that can be selected in the menu.
+                Minimum number is 1.
+            focus_on_load: Indicates whether the element will be set to auto focus within the view object.
+                Only one element can be set to true. Defaults to false.
+        """
+        super().__init__(
+            type=self.type,
+            action_id=action_id,
+            placeholder=TextObject.parse(placeholder, PlainTextObject.type),  # type: ignore[arg-type]
+            confirm=ConfirmObject.parse(confirm),  # type: ignore[arg-type]
+            focus_on_load=focus_on_load,
+        )
+        show_unknown_key_warning(self, others)
+
+        self.options = Option.parse_all(options)
+        self.option_groups = OptionGroup.parse_all(option_groups)
+        self.initial_options = Option.parse_all(initial_options)
+        self.max_selected_items = max_selected_items
+
+    @JsonValidator(f"options attribute cannot exceed {options_max_length} elements")
+    def _validate_options_length(self) -> bool:
+        return self.options is None or len(self.options) <= self.options_max_length
+
+    @JsonValidator(f"option_groups attribute cannot exceed {option_groups_max_length} elements")
+    def _validate_option_groups_length(self) -> bool:
+        return self.option_groups is None or len(self.option_groups) <= self.option_groups_max_length
+
+    @JsonValidator("options and option_groups cannot both be specified")
+    def _validate_options_and_option_groups_both_specified(self) -> bool:
+        return self.options is None or self.option_groups is None
+
+    @JsonValidator("options or option_groups must be specified")
+    def _validate_neither_options_or_option_groups_is_specified(self) -> bool:
+        return self.options is not None or self.option_groups is not None
+
+

Block Elements are things that exists inside of your Blocks. +https://docs.slack.dev/reference/block-kit/block-elements/

+

This is the simplest form of select menu, with a static list of options passed in when defining the element. +https://docs.slack.dev/reference/block-kit/block-elements/multi-select-menu-element#static_multi_select

+

Args

+
+
placeholder : required
+
A plain_text only text object that defines the placeholder text shown on the menu. +Maximum length for the text in this field is 150 characters.
+
action_id : required
+
An identifier for the action triggered when a menu option is selected. +You can use this when you receive an interaction payload to identify the source of the action. +Should be unique among all other action_ids in the containing block. +Maximum length for this field is 255 characters.
+
options : either options or option_groups is required
+
An array of option objects. +Maximum number of options is 100. +If option_groups is specified, this field should not be.
+
option_groups : either options or option_groups is required
+
An array of option group objects. +Maximum number of option groups is 100. +If options is specified, this field should not be.
+
initial_options
+
An array of option objects that exactly match one or more of the options +within options or option_groups. These options will be selected when the menu initially loads.
+
confirm
+
A confirm object that defines an optional confirmation dialog +that appears before the multi-select choices are submitted.
+
max_selected_items
+
Specifies the maximum number of items that can be selected in the menu. +Minimum number is 1.
+
focus_on_load
+
Indicates whether the element will be set to auto focus within the view object. +Only one element can be set to true. Defaults to false.
+
+

Ancestors

+ +

Class variables

+
+
var option_groups_max_length
+
+

The type of the None singleton.

+
+
var options_max_length
+
+

The type of the None singleton.

+
+
var type
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
prop attributes : Set[str]
+
+
+ +Expand source code + +
@property
+def attributes(self) -> Set[str]:  # type: ignore[override]
+    return super().attributes.union({"options", "option_groups", "initial_options", "max_selected_items"})
+
+

Build an unordered collection of unique elements.

+
+
+

Inherited members

+ +
+
+class StaticSelectElement +(*,
placeholder: str | dict | TextObject | None = None,
action_id: str | None = None,
options: Sequence[dict | Option] | None = None,
option_groups: Sequence[dict | OptionGroup] | None = None,
initial_option: dict | Option | None = None,
confirm: dict | ConfirmObject | None = None,
focus_on_load: bool | None = None,
**others: dict)
+
+
+
+ +Expand source code + +
class StaticSelectElement(InputInteractiveElement):
+    type = "static_select"
+    options_max_length = 100
+    option_groups_max_length = 100
+
+    @property
+    def attributes(self) -> Set[str]:  # type: ignore[override]
+        return super().attributes.union({"options", "option_groups", "initial_option"})
+
+    def __init__(
+        self,
+        *,
+        placeholder: Optional[Union[str, dict, TextObject]] = None,
+        action_id: Optional[str] = None,
+        options: Optional[Sequence[Union[dict, Option]]] = None,
+        option_groups: Optional[Sequence[Union[dict, OptionGroup]]] = None,
+        initial_option: Optional[Union[dict, Option]] = None,
+        confirm: Optional[Union[dict, ConfirmObject]] = None,
+        focus_on_load: Optional[bool] = None,
+        **others: dict,
+    ):
+        """This is the simplest form of select menu, with a static list of options passed in when defining the element.
+        https://docs.slack.dev/reference/block-kit/block-elements/select-menu-element#static_select
+
+        Args:
+            placeholder (required): A plain_text only text object that defines the placeholder text shown on the menu.
+                Maximum length for the text in this field is 150 characters.
+            action_id (required): An identifier for the action triggered when a menu option is selected.
+                You can use this when you receive an interaction payload to identify the source of the action.
+                Should be unique among all other action_ids in the containing block.
+                Maximum length for this field is 255 characters.
+            options (either options or option_groups is required): An array of option objects.
+                Maximum number of options is 100.
+                If option_groups is specified, this field should not be.
+            option_groups (either options or option_groups is required): An array of option group objects.
+                Maximum number of option groups is 100.
+                If options is specified, this field should not be.
+            initial_option: A single option that exactly matches one of the options or option_groups.
+                This option will be selected when the menu initially loads.
+            confirm: A confirm object that defines an optional confirmation dialog
+                that appears after a menu item is selected.
+            focus_on_load: Indicates whether the element will be set to auto focus within the view object.
+                Only one element can be set to true. Defaults to false.
+        """
+        super().__init__(
+            type=self.type,
+            action_id=action_id,
+            placeholder=TextObject.parse(placeholder, PlainTextObject.type),  # type: ignore[arg-type]
+            confirm=ConfirmObject.parse(confirm),  # type: ignore[arg-type]
+            focus_on_load=focus_on_load,
+        )
+        show_unknown_key_warning(self, others)
+
+        self.options = options
+        self.option_groups = option_groups
+        self.initial_option = initial_option
+
+    @JsonValidator(f"options attribute cannot exceed {options_max_length} elements")
+    def _validate_options_length(self) -> bool:
+        return self.options is None or len(self.options) <= self.options_max_length
+
+    @JsonValidator(f"option_groups attribute cannot exceed {option_groups_max_length} elements")
+    def _validate_option_groups_length(self) -> bool:
+        return self.option_groups is None or len(self.option_groups) <= self.option_groups_max_length
+
+    @JsonValidator("options and option_groups cannot both be specified")
+    def _validate_options_and_option_groups_both_specified(self) -> bool:
+        return not (self.options is not None and self.option_groups is not None)
+
+    @JsonValidator("options or option_groups must be specified")
+    def _validate_neither_options_or_option_groups_is_specified(self) -> bool:
+        return self.options is not None or self.option_groups is not None
+
+

Block Elements are things that exists inside of your Blocks. +https://docs.slack.dev/reference/block-kit/block-elements/

+

This is the simplest form of select menu, with a static list of options passed in when defining the element. +https://docs.slack.dev/reference/block-kit/block-elements/select-menu-element#static_select

+

Args

+
+
placeholder : required
+
A plain_text only text object that defines the placeholder text shown on the menu. +Maximum length for the text in this field is 150 characters.
+
action_id : required
+
An identifier for the action triggered when a menu option is selected. +You can use this when you receive an interaction payload to identify the source of the action. +Should be unique among all other action_ids in the containing block. +Maximum length for this field is 255 characters.
+
options : either options or option_groups is required
+
An array of option objects. +Maximum number of options is 100. +If option_groups is specified, this field should not be.
+
option_groups : either options or option_groups is required
+
An array of option group objects. +Maximum number of option groups is 100. +If options is specified, this field should not be.
+
initial_option
+
A single option that exactly matches one of the options or option_groups. +This option will be selected when the menu initially loads.
+
confirm
+
A confirm object that defines an optional confirmation dialog +that appears after a menu item is selected.
+
focus_on_load
+
Indicates whether the element will be set to auto focus within the view object. +Only one element can be set to true. Defaults to false.
+
+

Ancestors

+ +

Class variables

+
+
var option_groups_max_length
+
+

The type of the None singleton.

+
+
var options_max_length
+
+

The type of the None singleton.

+
+
var type
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
prop attributes : Set[str]
+
+
+ +Expand source code + +
@property
+def attributes(self) -> Set[str]:  # type: ignore[override]
+    return super().attributes.union({"options", "option_groups", "initial_option"})
+
+

Build an unordered collection of unique elements.

+
+
+

Inherited members

+ +
+
+class TableBlock +(*,
rows: Sequence[Sequence[Dict[str, Any]]],
column_settings: Sequence[Dict[str, Any] | None] | None = None,
block_id: str | None = None,
**others: dict)
+
+
+
+ +Expand source code + +
class TableBlock(Block):
+    type = "table"
+
+    @property
+    def attributes(self) -> Set[str]:  # type: ignore[override]
+        return super().attributes.union({"rows", "column_settings"})
+
+    def __init__(
+        self,
+        *,
+        rows: Sequence[Sequence[Dict[str, Any]]],
+        column_settings: Optional[Sequence[Optional[Dict[str, Any]]]] = None,
+        block_id: Optional[str] = None,
+        **others: dict,
+    ):
+        """Displays structured information in a table.
+        https://docs.slack.dev/reference/block-kit/blocks/table-block
+
+        Args:
+            rows (required): An array consisting of table rows. Maximum 100 rows.
+                Each row object is an array with a max of 20 table cells.
+                Table cells can have a type of raw_text or rich_text.
+            column_settings: An array describing column behavior. If there are fewer items in the column_settings array
+                than there are columns in the table, then the items in the the column_settings array will describe
+                the same number of columns in the table as there are in the array itself.
+                Any additional columns will have the default behavior. Maximum 20 items.
+                See below for column settings schema.
+            block_id: A unique identifier for a block. If not specified, a block_id will be generated.
+                You can use this block_id when you receive an interaction payload to identify the source of the action.
+                Maximum length for this field is 255 characters.
+                block_id should be unique for each message and each iteration of a message.
+                If a message is updated, use a new block_id.
+        """
+        super().__init__(type=self.type, block_id=block_id)
+        show_unknown_key_warning(self, others)
+
+        self.rows = rows
+        self.column_settings = column_settings
+
+    @JsonValidator("rows attribute must be specified")
+    def _validate_rows(self):
+        return self.rows is not None and len(self.rows) > 0
+
+

Blocks are a series of components that can be combined +to create visually rich and compellingly interactive messages. +https://docs.slack.dev/reference/block-kit/blocks

+

Displays structured information in a table. +https://docs.slack.dev/reference/block-kit/blocks/table-block

+

Args

+
+
rows : required
+
An array consisting of table rows. Maximum 100 rows. +Each row object is an array with a max of 20 table cells. +Table cells can have a type of raw_text or rich_text.
+
column_settings
+
An array describing column behavior. If there are fewer items in the column_settings array +than there are columns in the table, then the items in the the column_settings array will describe +the same number of columns in the table as there are in the array itself. +Any additional columns will have the default behavior. Maximum 20 items. +See below for column settings schema.
+
block_id
+
A unique identifier for a block. If not specified, a block_id will be generated. +You can use this block_id when you receive an interaction payload to identify the source of the action. +Maximum length for this field is 255 characters. +block_id should be unique for each message and each iteration of a message. +If a message is updated, use a new block_id.
+
+

Ancestors

+ +

Class variables

+
+
var type
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
prop attributes : Set[str]
+
+
+ +Expand source code + +
@property
+def attributes(self) -> Set[str]:  # type: ignore[override]
+    return super().attributes.union({"rows", "column_settings"})
+
+

Build an unordered collection of unique elements.

+
+
+

Inherited members

+ +
+
+class TextObject +(text: str,
type: str | None = None,
subtype: str | None = None,
emoji: bool | None = None,
**kwargs)
+
+
+
+ +Expand source code + +
class TextObject(JsonObject):
+    """The interface for text objects (types: plain_text, mrkdwn)"""
+
+    attributes = {"text", "type", "emoji"}
+    logger = logging.getLogger(__name__)
+
+    def _subtype_warning(self):
+        warnings.warn(
+            "subtype is deprecated since slackclient 2.6.0, use type instead",
+            DeprecationWarning,
+        )
+
+    @property
+    def subtype(self) -> Optional[str]:
+        return self.type
+
+    @classmethod
+    def parse(
+        cls,
+        text: Union[str, Dict[str, Any], "TextObject"],
+        default_type: str = "mrkdwn",
+    ) -> Optional["TextObject"]:
+        if not text:
+            return None
+        elif isinstance(text, str):
+            if default_type == PlainTextObject.type:
+                return PlainTextObject.from_str(text)
+            else:
+                return MarkdownTextObject.from_str(text)
+        elif isinstance(text, dict):
+            d = copy.copy(text)
+            t = d.pop("type")
+            if t == PlainTextObject.type:
+                return PlainTextObject(**d)
+            else:
+                return MarkdownTextObject(**d)
+        elif isinstance(text, TextObject):
+            return text
+        else:
+            cls.logger.warning(f"Unknown type ({type(text)}) detected when parsing a TextObject")
+            return None
+
+    def __init__(
+        self,
+        text: str,
+        type: Optional[str] = None,
+        subtype: Optional[str] = None,
+        emoji: Optional[bool] = None,
+        **kwargs,
+    ):
+        """Super class for new text "objects" used in Block kit"""
+        if subtype:
+            self._subtype_warning()
+
+        self.text = text
+        self.type = type if type else subtype
+        self.emoji = emoji
+
+

The interface for text objects (types: plain_text, mrkdwn)

+

Super class for new text "objects" used in Block kit

+

Ancestors

+ +

Subclasses

+ +

Class variables

+
+
var attributes
+
+

The type of the None singleton.

+
+
var logger
+
+

The type of the None singleton.

+
+
+

Static methods

+
+
+def parse(text: str | Dict[str, Any] | ForwardRef('TextObject'),
default_type: str = 'mrkdwn') ‑> TextObject | None
+
+
+
+
+
+

Instance variables

+
+
prop subtype : str | None
+
+
+ +Expand source code + +
@property
+def subtype(self) -> Optional[str]:
+    return self.type
+
+
+
+
+

Inherited members

+ +
+
+class TimePickerElement +(*,
action_id: str | None = None,
placeholder: str | dict | TextObject | None = None,
initial_time: str | None = None,
confirm: dict | ConfirmObject | None = None,
focus_on_load: bool | None = None,
timezone: str | None = None,
**others: dict)
+
+
+
+ +Expand source code + +
class TimePickerElement(InputInteractiveElement):
+    type = "timepicker"
+
+    @property
+    def attributes(self) -> Set[str]:  # type: ignore[override]
+        return super().attributes.union({"initial_time", "timezone"})
+
+    def __init__(
+        self,
+        *,
+        action_id: Optional[str] = None,
+        placeholder: Optional[Union[str, dict, TextObject]] = None,
+        initial_time: Optional[str] = None,
+        confirm: Optional[Union[dict, ConfirmObject]] = None,
+        focus_on_load: Optional[bool] = None,
+        timezone: Optional[str] = None,
+        **others: dict,
+    ):
+        """
+        An element which allows selection of a time of day.
+        On desktop clients, this time picker will take the form of a dropdown list
+        with free-text entry for precise choices.
+        On mobile clients, the time picker will use native time picker UIs.
+        https://docs.slack.dev/reference/block-kit/block-elements/time-picker-element
+
+        Args:
+            action_id (required): An identifier for the action triggered when a time is selected.
+                You can use this when you receive an interaction payload to identify the source of the action.
+                Should be unique among all other action_ids in the containing block.
+                Maximum length for this field is 255 characters.
+            placeholder: A plain_text only text object that defines the placeholder text shown on the timepicker.
+                Maximum length for the text in this field is 150 characters.
+            initial_time: The initial time that is selected when the element is loaded.
+                This should be in the format HH:mm, where HH is the 24-hour format of an hour (00 to 23)
+                and mm is minutes with leading zeros (00 to 59), for example 22:25 for 10:25pm.
+            confirm: A confirm object that defines an optional confirmation dialog
+                that appears after a time is selected.
+            focus_on_load: Indicates whether the element will be set to auto focus within the view object.
+                Only one element can be set to true. Defaults to false.
+            timezone: The timezone to consider for this input value.
+        """
+        super().__init__(
+            type=self.type,
+            action_id=action_id,
+            placeholder=TextObject.parse(placeholder, PlainTextObject.type),  # type: ignore[arg-type]
+            confirm=ConfirmObject.parse(confirm),  # type: ignore[arg-type]
+            focus_on_load=focus_on_load,
+        )
+        show_unknown_key_warning(self, others)
+
+        self.initial_time = initial_time
+        self.timezone = timezone
+
+    @JsonValidator("initial_time attribute must be in format 'HH:mm'")
+    def _validate_initial_time_valid(self) -> bool:
+        return self.initial_time is None or re.match(r"([0-1][0-9]|2[0-3]):([0-5][0-9])", self.initial_time) is not None
+
+

Block Elements are things that exists inside of your Blocks. +https://docs.slack.dev/reference/block-kit/block-elements/

+

An element which allows selection of a time of day. +On desktop clients, this time picker will take the form of a dropdown list +with free-text entry for precise choices. +On mobile clients, the time picker will use native time picker UIs. +https://docs.slack.dev/reference/block-kit/block-elements/time-picker-element

+

Args

+
+
action_id : required
+
An identifier for the action triggered when a time is selected. +You can use this when you receive an interaction payload to identify the source of the action. +Should be unique among all other action_ids in the containing block. +Maximum length for this field is 255 characters.
+
placeholder
+
A plain_text only text object that defines the placeholder text shown on the timepicker. +Maximum length for the text in this field is 150 characters.
+
initial_time
+
The initial time that is selected when the element is loaded. +This should be in the format HH:mm, where HH is the 24-hour format of an hour (00 to 23) +and mm is minutes with leading zeros (00 to 59), for example 22:25 for 10:25pm.
+
confirm
+
A confirm object that defines an optional confirmation dialog +that appears after a time is selected.
+
focus_on_load
+
Indicates whether the element will be set to auto focus within the view object. +Only one element can be set to true. Defaults to false.
+
timezone
+
The timezone to consider for this input value.
+
+

Ancestors

+ +

Class variables

+
+
var type
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
prop attributes : Set[str]
+
+
+ +Expand source code + +
@property
+def attributes(self) -> Set[str]:  # type: ignore[override]
+    return super().attributes.union({"initial_time", "timezone"})
+
+

Build an unordered collection of unique elements.

+
+
+

Inherited members

+ +
+
+class UrlInputElement +(*,
action_id: str | None = None,
initial_value: str | None = None,
dispatch_action_config: dict | DispatchActionConfig | None = None,
focus_on_load: bool | None = None,
placeholder: str | dict | TextObject | None = None,
**others: dict)
+
+
+
+ +Expand source code + +
class UrlInputElement(InputInteractiveElement):
+    type = "url_text_input"
+
+    @property
+    def attributes(self) -> Set[str]:  # type: ignore[override]
+        return super().attributes.union(
+            {
+                "initial_value",
+                "dispatch_action_config",
+            }
+        )
+
+    def __init__(
+        self,
+        *,
+        action_id: Optional[str] = None,
+        initial_value: Optional[str] = None,
+        dispatch_action_config: Optional[Union[dict, DispatchActionConfig]] = None,
+        focus_on_load: Optional[bool] = None,
+        placeholder: Optional[Union[str, dict, TextObject]] = None,
+        **others: dict,
+    ):
+        """
+        A URL input element, similar to the Plain-text input element,
+        creates a single line field where a user can enter URL-encoded data.
+        https://docs.slack.dev/reference/block-kit/block-elements/url-input-element
+
+        Args:
+            action_id (required): An identifier for the input value when the parent modal is submitted.
+                You can use this when you receive a view_submission payload to identify the value of the input element.
+                Should be unique among all other action_ids in the containing block.
+                Maximum length for this field is 255 characters.
+            initial_value: The initial value in the URL input when it is loaded.
+            dispatch_action_config: A dispatch configuration object that determines when during text input
+                the element returns a block_actions payload.
+            focus_on_load: Indicates whether the element will be set to auto focus within the view object.
+                Only one element can be set to true. Defaults to false.
+            placeholder: A plain_text only text object that defines the placeholder text shown in the URL input.
+                Maximum length for the text in this field is 150 characters.
+        """
+        super().__init__(
+            type=self.type,
+            action_id=action_id,
+            placeholder=TextObject.parse(placeholder, PlainTextObject.type),  # type: ignore[arg-type]
+            focus_on_load=focus_on_load,
+        )
+        show_unknown_key_warning(self, others)
+
+        self.initial_value = initial_value
+        self.dispatch_action_config = dispatch_action_config
+
+

Block Elements are things that exists inside of your Blocks. +https://docs.slack.dev/reference/block-kit/block-elements/

+

A URL input element, similar to the Plain-text input element, +creates a single line field where a user can enter URL-encoded data. +https://docs.slack.dev/reference/block-kit/block-elements/url-input-element

+

Args

+
+
action_id : required
+
An identifier for the input value when the parent modal is submitted. +You can use this when you receive a view_submission payload to identify the value of the input element. +Should be unique among all other action_ids in the containing block. +Maximum length for this field is 255 characters.
+
initial_value
+
The initial value in the URL input when it is loaded.
+
dispatch_action_config
+
A dispatch configuration object that determines when during text input +the element returns a block_actions payload.
+
focus_on_load
+
Indicates whether the element will be set to auto focus within the view object. +Only one element can be set to true. Defaults to false.
+
placeholder
+
A plain_text only text object that defines the placeholder text shown in the URL input. +Maximum length for the text in this field is 150 characters.
+
+

Ancestors

+ +

Class variables

+
+
var type
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
prop attributes : Set[str]
+
+
+ +Expand source code + +
@property
+def attributes(self) -> Set[str]:  # type: ignore[override]
+    return super().attributes.union(
+        {
+            "initial_value",
+            "dispatch_action_config",
+        }
+    )
+
+

Build an unordered collection of unique elements.

+
+
+

Inherited members

+ +
+
+class UserMultiSelectElement +(*,
action_id: str | None = None,
placeholder: str | dict | TextObject | None = None,
initial_users: Sequence[str] | None = None,
confirm: dict | ConfirmObject | None = None,
max_selected_items: int | None = None,
focus_on_load: bool | None = None,
**others: dict)
+
+
+
+ +Expand source code + +
class UserMultiSelectElement(InputInteractiveElement):
+    type = "multi_users_select"
+
+    @property
+    def attributes(self) -> Set[str]:  # type: ignore[override]
+        return super().attributes.union({"initial_users", "max_selected_items"})
+
+    def __init__(
+        self,
+        *,
+        action_id: Optional[str] = None,
+        placeholder: Optional[Union[str, dict, TextObject]] = None,
+        initial_users: Optional[Sequence[str]] = None,
+        confirm: Optional[Union[dict, ConfirmObject]] = None,
+        max_selected_items: Optional[int] = None,
+        focus_on_load: Optional[bool] = None,
+        **others: dict,
+    ):
+        """
+        This select menu will populate its options with a list of Slack users visible to
+        the current user in the active workspace.
+        https://docs.slack.dev/reference/block-kit/block-elements/multi-select-menu-element#users_multi_select
+
+        Args:
+            action_id (required): An identifier for the action triggered when a menu option is selected.
+                You can use this when you receive an interaction payload to identify the source of the action.
+                Should be unique among all other action_ids in the containing block.
+                Maximum length for this field is 255 characters.
+            placeholder (required): A plain_text only text object that defines the placeholder text shown on the menu.
+                Maximum length for the text in this field is 150 characters.
+            initial_users: An array of user IDs of any valid users to be pre-selected when the menu loads.
+            confirm: A confirm object that defines an optional confirmation dialog that appears
+                before the multi-select choices are submitted.
+            max_selected_items: Specifies the maximum number of items that can be selected in the menu.
+                Minimum number is 1.
+            focus_on_load: Indicates whether the element will be set to auto focus within the view object.
+                Only one element can be set to true. Defaults to false.
+        """
+        super().__init__(
+            type=self.type,
+            action_id=action_id,
+            placeholder=TextObject.parse(placeholder, PlainTextObject.type),  # type: ignore[arg-type]
+            confirm=ConfirmObject.parse(confirm),  # type: ignore[arg-type]
+            focus_on_load=focus_on_load,
+        )
+        show_unknown_key_warning(self, others)
+
+        self.initial_users = initial_users
+        self.max_selected_items = max_selected_items
+
+

Block Elements are things that exists inside of your Blocks. +https://docs.slack.dev/reference/block-kit/block-elements/

+

This select menu will populate its options with a list of Slack users visible to +the current user in the active workspace. +https://docs.slack.dev/reference/block-kit/block-elements/multi-select-menu-element#users_multi_select

+

Args

+
+
action_id : required
+
An identifier for the action triggered when a menu option is selected. +You can use this when you receive an interaction payload to identify the source of the action. +Should be unique among all other action_ids in the containing block. +Maximum length for this field is 255 characters.
+
placeholder : required
+
A plain_text only text object that defines the placeholder text shown on the menu. +Maximum length for the text in this field is 150 characters.
+
initial_users
+
An array of user IDs of any valid users to be pre-selected when the menu loads.
+
confirm
+
A confirm object that defines an optional confirmation dialog that appears +before the multi-select choices are submitted.
+
max_selected_items
+
Specifies the maximum number of items that can be selected in the menu. +Minimum number is 1.
+
focus_on_load
+
Indicates whether the element will be set to auto focus within the view object. +Only one element can be set to true. Defaults to false.
+
+

Ancestors

+ +

Class variables

+
+
var type
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
prop attributes : Set[str]
+
+
+ +Expand source code + +
@property
+def attributes(self) -> Set[str]:  # type: ignore[override]
+    return super().attributes.union({"initial_users", "max_selected_items"})
+
+

Build an unordered collection of unique elements.

+
+
+

Inherited members

+ +
+
+class UserSelectElement +(*,
placeholder: str | dict | TextObject | None = None,
action_id: str | None = None,
initial_user: str | None = None,
confirm: dict | ConfirmObject | None = None,
focus_on_load: bool | None = None,
**others: dict)
+
+
+
+ +Expand source code + +
class UserSelectElement(InputInteractiveElement):
+    type = "users_select"
+
+    @property
+    def attributes(self) -> Set[str]:  # type: ignore[override]
+        return super().attributes.union({"initial_user"})
+
+    def __init__(
+        self,
+        *,
+        placeholder: Optional[Union[str, dict, TextObject]] = None,
+        action_id: Optional[str] = None,
+        initial_user: Optional[str] = None,
+        confirm: Optional[Union[dict, ConfirmObject]] = None,
+        focus_on_load: Optional[bool] = None,
+        **others: dict,
+    ):
+        """
+        This select menu will populate its options with a list of Slack users visible to
+        the current user in the active workspace.
+        https://docs.slack.dev/reference/block-kit/block-elements/select-menu-element#users_select
+
+        Args:
+            placeholder (required): A plain_text only text object that defines the placeholder text shown on the menu.
+                Maximum length for the text in this field is 150 characters.
+            action_id (required): An identifier for the action triggered when a menu option is selected.
+                You can use this when you receive an interaction payload to identify the source of the action.
+                Should be unique among all other action_ids in the containing block.
+                Maximum length for this field is 255 characters.
+            initial_user: The user ID of any valid user to be pre-selected when the menu loads.
+            confirm: A confirm object that defines an optional confirmation dialog
+                that appears after a menu item is selected.
+            focus_on_load: Indicates whether the element will be set to auto focus within the view object.
+                Only one element can be set to true. Defaults to false.
+        """
+        super().__init__(
+            type=self.type,
+            action_id=action_id,
+            placeholder=TextObject.parse(placeholder, PlainTextObject.type),  # type: ignore[arg-type]
+            confirm=ConfirmObject.parse(confirm),  # type: ignore[arg-type]
+            focus_on_load=focus_on_load,
+        )
+        show_unknown_key_warning(self, others)
+
+        self.initial_user = initial_user
+
+

Block Elements are things that exists inside of your Blocks. +https://docs.slack.dev/reference/block-kit/block-elements/

+

This select menu will populate its options with a list of Slack users visible to +the current user in the active workspace. +https://docs.slack.dev/reference/block-kit/block-elements/select-menu-element#users_select

+

Args

+
+
placeholder : required
+
A plain_text only text object that defines the placeholder text shown on the menu. +Maximum length for the text in this field is 150 characters.
+
action_id : required
+
An identifier for the action triggered when a menu option is selected. +You can use this when you receive an interaction payload to identify the source of the action. +Should be unique among all other action_ids in the containing block. +Maximum length for this field is 255 characters.
+
initial_user
+
The user ID of any valid user to be pre-selected when the menu loads.
+
confirm
+
A confirm object that defines an optional confirmation dialog +that appears after a menu item is selected.
+
focus_on_load
+
Indicates whether the element will be set to auto focus within the view object. +Only one element can be set to true. Defaults to false.
+
+

Ancestors

+ +

Class variables

+
+
var type
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
prop attributes : Set[str]
+
+
+ +Expand source code + +
@property
+def attributes(self) -> Set[str]:  # type: ignore[override]
+    return super().attributes.union({"initial_user"})
+
+

Build an unordered collection of unique elements.

+
+
+

Inherited members

+ +
+
+class VideoBlock +(*,
block_id: str | None = None,
alt_text: str | None = None,
video_url: str | None = None,
thumbnail_url: str | None = None,
title: str | dict | PlainTextObject | None = None,
title_url: str | None = None,
description: str | dict | PlainTextObject | None = None,
provider_icon_url: str | None = None,
provider_name: str | None = None,
author_name: str | None = None,
**others: dict)
+
+
+
+ +Expand source code + +
class VideoBlock(Block):
+    type = "video"
+    title_max_length = 200
+    author_name_max_length = 50
+
+    @property
+    def attributes(self) -> Set[str]:  # type: ignore[override]
+        return super().attributes.union(
+            {
+                "alt_text",
+                "video_url",
+                "thumbnail_url",
+                "title",
+                "title_url",
+                "description",
+                "provider_icon_url",
+                "provider_name",
+                "author_name",
+            }
+        )
+
+    def __init__(
+        self,
+        *,
+        block_id: Optional[str] = None,
+        alt_text: Optional[str] = None,
+        video_url: Optional[str] = None,
+        thumbnail_url: Optional[str] = None,
+        title: Optional[Union[str, dict, PlainTextObject]] = None,
+        title_url: Optional[str] = None,
+        description: Optional[Union[str, dict, PlainTextObject]] = None,
+        provider_icon_url: Optional[str] = None,
+        provider_name: Optional[str] = None,
+        author_name: Optional[str] = None,
+        **others: dict,
+    ):
+        """A video block is designed to embed videos in all app surfaces
+        (e.g. link unfurls, messages, modals, App Home) —
+        anywhere you can put blocks! To use the video block within your app,
+        you must have the links.embed:write scope.
+        https://docs.slack.dev/reference/block-kit/blocks/video-block
+
+        Args:
+            block_id: A string acting as a unique identifier for a block. If not specified, one will be generated.
+                Maximum length for this field is 255 characters.
+                block_id should be unique for each message and each iteration of a message.
+                If a message is updated, use a new block_id.
+            alt_text (required): A tooltip for the video. Required for accessibility
+            video_url (required): The URL to be embedded. Must match any existing unfurl domains within the app
+                and point to a HTTPS URL.
+            thumbnail_url (required): The thumbnail image URL
+            title (required): Video title in plain text format. Must be less than 200 characters.
+            title_url: Hyperlink for the title text. Must correspond to the non-embeddable URL for the video.
+                Must go to an HTTPS URL.
+            description: Description for video in plain text format.
+            provider_icon_url: Icon for the video provider - ex. Youtube icon
+            provider_name: The originating application or domain of the video ex. Youtube
+            author_name: Author name to be displayed. Must be less than 50 characters.
+        """
+        super().__init__(type=self.type, block_id=block_id)
+        show_unknown_key_warning(self, others)
+
+        self.alt_text = alt_text
+        self.video_url = video_url
+        self.thumbnail_url = thumbnail_url
+        self.title = TextObject.parse(title, default_type=PlainTextObject.type)  # type: ignore[arg-type]
+        self.title_url = title_url
+        self.description = TextObject.parse(description, default_type=PlainTextObject.type)  # type: ignore[arg-type]
+        self.provider_icon_url = provider_icon_url
+        self.provider_name = provider_name
+        self.author_name = author_name
+
+    @JsonValidator("alt_text attribute must be specified")
+    def _validate_alt_text(self):
+        return self.alt_text is not None
+
+    @JsonValidator("video_url attribute must be specified")
+    def _validate_video_url(self):
+        return self.video_url is not None
+
+    @JsonValidator("thumbnail_url attribute must be specified")
+    def _validate_thumbnail_url(self):
+        return self.thumbnail_url is not None
+
+    @JsonValidator("title attribute must be specified")
+    def _validate_title(self):
+        return self.title is not None
+
+    @JsonValidator(f"title attribute cannot exceed {title_max_length} characters")
+    def _validate_title_length(self):
+        return self.title is None or len(self.title.text) < self.title_max_length
+
+    @JsonValidator(f"author_name attribute cannot exceed {author_name_max_length} characters")
+    def _validate_author_name_length(self):
+        return self.author_name is None or len(self.author_name) < self.author_name_max_length
+
+

Blocks are a series of components that can be combined +to create visually rich and compellingly interactive messages. +https://docs.slack.dev/reference/block-kit/blocks

+

A video block is designed to embed videos in all app surfaces +(e.g. link unfurls, messages, modals, App Home) — +anywhere you can put blocks! To use the video block within your app, +you must have the links.embed:write scope. +https://docs.slack.dev/reference/block-kit/blocks/video-block

+

Args

+
+
block_id
+
A string acting as a unique identifier for a block. If not specified, one will be generated. +Maximum length for this field is 255 characters. +block_id should be unique for each message and each iteration of a message. +If a message is updated, use a new block_id.
+
alt_text : required
+
A tooltip for the video. Required for accessibility
+
video_url : required
+
The URL to be embedded. Must match any existing unfurl domains within the app +and point to a HTTPS URL.
+
thumbnail_url : required
+
The thumbnail image URL
+
title : required
+
Video title in plain text format. Must be less than 200 characters.
+
title_url
+
Hyperlink for the title text. Must correspond to the non-embeddable URL for the video. +Must go to an HTTPS URL.
+
description
+
Description for video in plain text format.
+
provider_icon_url
+
Icon for the video provider - ex. Youtube icon
+
provider_name
+
The originating application or domain of the video ex. Youtube
+
author_name
+
Author name to be displayed. Must be less than 50 characters.
+
+

Ancestors

+ +

Class variables

+
+
var author_name_max_length
+
+

The type of the None singleton.

+
+
var title_max_length
+
+

The type of the None singleton.

+
+
var type
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
prop attributes : Set[str]
+
+
+ +Expand source code + +
@property
+def attributes(self) -> Set[str]:  # type: ignore[override]
+    return super().attributes.union(
+        {
+            "alt_text",
+            "video_url",
+            "thumbnail_url",
+            "title",
+            "title_url",
+            "description",
+            "provider_icon_url",
+            "provider_name",
+            "author_name",
+        }
+    )
+
+

Build an unordered collection of unique elements.

+
+
+

Inherited members

+ +
+
+
+
+ +
+ + + diff --git a/docs/reference/models/dialoags.html b/docs/reference/models/dialoags.html new file mode 100644 index 000000000..cc2ff46a2 --- /dev/null +++ b/docs/reference/models/dialoags.html @@ -0,0 +1,2653 @@ + + + + + + +slack_sdk.models.dialoags API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.models.dialoags

+
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class AbstractDialogSelector +(*,
name: str,
label: str,
optional: bool = False,
value: Option | str | None = None,
placeholder: str | None = None)
+
+
+
+ +Expand source code + +
class AbstractDialogSelector(JsonObject, metaclass=ABCMeta):
+    DataSourceTypes = DynamicSelectElementTypes.union({"external", "static"})
+
+    attributes = {"data_source", "label", "name", "optional", "placeholder", "type"}
+
+    name_max_length = 300
+    label_max_length = 48
+    placeholder_max_length = 150
+
+    @property
+    @abstractmethod
+    def data_source(self) -> str:
+        pass
+
+    def __init__(
+        self,
+        *,
+        name: str,
+        label: str,
+        optional: bool = False,
+        value: Optional[Union[Option, str]] = None,
+        placeholder: Optional[str] = None,
+    ):
+        self.name = name
+        self.label = label
+        self.optional = optional
+        self.value = value
+        self.placeholder = placeholder
+        self.type = "select"
+
+    @JsonValidator(f"name attribute cannot exceed {name_max_length} characters")
+    def name_length(self) -> bool:
+        return len(self.name) < self.name_max_length
+
+    @JsonValidator(f"label attribute cannot exceed {label_max_length} characters")
+    def label_length(self) -> bool:
+        return len(self.label) < self.label_max_length
+
+    @JsonValidator(f"placeholder attribute cannot exceed {placeholder_max_length} characters")
+    def placeholder_length(self) -> bool:
+        return self.placeholder is None or len(self.placeholder) < self.placeholder_max_length
+
+    @EnumValidator("data_source", DataSourceTypes)
+    def data_source_valid(self) -> bool:
+        return self.data_source in self.DataSourceTypes
+
+    def to_dict(self) -> dict:
+        json = super().to_dict()
+        if self.data_source == "external":
+            if isinstance(self.value, Option):
+                json["selected_options"] = extract_json([self.value], "dialog")
+            elif self.value is not None:
+                json["selected_options"] = Option.from_single_value(self.value)
+        else:
+            if isinstance(self.value, Option):
+                json["value"] = self.value.value
+            elif self.value is not None:
+                json["value"] = self.value
+        return json
+
+

The base class for JSON serializable class objects

+

Ancestors

+ +

Subclasses

+ +

Class variables

+
+
var DataSourceTypes
+
+

The type of the None singleton.

+
+
var attributes
+
+

The type of the None singleton.

+
+
var label_max_length
+
+

The type of the None singleton.

+
+
var name_max_length
+
+

The type of the None singleton.

+
+
var placeholder_max_length
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
prop data_source : str
+
+
+ +Expand source code + +
@property
+@abstractmethod
+def data_source(self) -> str:
+    pass
+
+
+
+
+

Methods

+
+
+def data_source_valid(self) ‑> bool +
+
+
+ +Expand source code + +
@EnumValidator("data_source", DataSourceTypes)
+def data_source_valid(self) -> bool:
+    return self.data_source in self.DataSourceTypes
+
+
+
+
+def label_length(self) ‑> bool +
+
+
+ +Expand source code + +
@JsonValidator(f"label attribute cannot exceed {label_max_length} characters")
+def label_length(self) -> bool:
+    return len(self.label) < self.label_max_length
+
+
+
+
+def name_length(self) ‑> bool +
+
+
+ +Expand source code + +
@JsonValidator(f"name attribute cannot exceed {name_max_length} characters")
+def name_length(self) -> bool:
+    return len(self.name) < self.name_max_length
+
+
+
+
+def placeholder_length(self) ‑> bool +
+
+
+ +Expand source code + +
@JsonValidator(f"placeholder attribute cannot exceed {placeholder_max_length} characters")
+def placeholder_length(self) -> bool:
+    return self.placeholder is None or len(self.placeholder) < self.placeholder_max_length
+
+
+
+
+

Inherited members

+ +
+
+class DialogBuilder +
+
+
+ +Expand source code + +
class DialogBuilder(JsonObject):
+    attributes: Set[str] = set()
+
+    _callback_id: Optional[str]
+    _elements: List[Union[DialogTextComponent, AbstractDialogSelector]]
+    _submit_label: Optional[str]
+    _notify_on_cancel: bool
+    _state: Optional[str]
+
+    title_max_length = 24
+    submit_label_max_length = 24
+    elements_max_length = 10
+    state_max_length = 3000
+
+    def __init__(self):
+        """
+        Create a DialogBuilder to more easily construct the JSON required to submit a
+        dialog to Slack
+        """
+        self._title = None
+        self._callback_id = None
+        self._elements = []
+        self._submit_label = None
+        self._notify_on_cancel = False
+        self._state = None
+
+    def title(self, title: str) -> "DialogBuilder":
+        """
+        Specify a title for this dialog
+
+        Args:
+          title: must not exceed 24 characters
+        """
+        self._title = title
+        return self
+
+    def state(self, state: Union[dict, str]) -> "DialogBuilder":
+        """
+        Pass state into this dialog - dictionaries will be automatically formatted to
+        JSON
+
+        Args:
+            state: Extra state information that you need to pass from this dialog
+                back to your application on submission
+        """
+        if isinstance(state, dict):
+            self._state = dumps(state)
+        else:
+            self._state = state
+        return self
+
+    def callback_id(self, callback_id: str) -> "DialogBuilder":
+        """
+        Specify a callback ID for this dialog, which your application will then
+        receive upon dialog submission
+
+        Args:
+          callback_id: a string identifying this particular dialog
+        """
+        self._callback_id = callback_id
+        return self
+
+    def submit_label(self, label: str) -> "DialogBuilder":
+        """
+        The label to use on the 'Submit' button on the dialog. Defaults to 'Submit'
+        if not specified.
+
+        Args:
+            label: must not exceed 24 characters, and must be a single word (no
+                spaces)
+        """
+        self._submit_label = label
+        return self
+
+    def notify_on_cancel(self, notify: bool) -> "DialogBuilder":
+        """
+        Whether this dialog should send a request to your application even if the
+        user cancels their interaction. Defaults to False.
+
+        Args:
+            notify: Set to True to indicate that your application should receive a
+                request even if the user cancels interaction with the dialog.
+        """
+        self._notify_on_cancel = notify
+        return self
+
+    def text_field(
+        self,
+        *,
+        name: str,
+        label: str,
+        optional: bool = False,
+        placeholder: Optional[str] = None,
+        hint: Optional[str] = None,
+        value: Optional[str] = None,
+        min_length: int = 0,
+        max_length: int = 150,
+        subtype: Optional[str] = None,
+    ) -> "DialogBuilder":
+        """
+        Text elements are single-line plain text fields.
+
+        https://docs.slack.dev/legacy/legacy-dialogs/#attributes_text_elements
+
+        Args:
+            name: Name of form element. Required. No more than 300 characters.
+            label: Label displayed to user. Required. 48 character maximum.
+            optional: Provide true when the form element is not required. By
+                default, form elements are required.
+            placeholder: A string displayed as needed to help guide users in
+                completing the element. 150 character maximum.
+            hint: Helpful text provided to assist users in answering a question.
+                Up to 150 characters.
+            value: A default value for this field. Up to 150 characters.
+            min_length: Minimum input length allowed for element. Up to 150
+                characters. Defaults to 0.
+            max_length: Maximum input length allowed for element. Up to 150
+                characters. Defaults to 150.
+            subtype: A subtype for this text input. Accepts email, number, tel,
+                    or url. In some form factors, optimized input is provided for this
+                    subtype.
+        """
+        self._elements.append(
+            DialogTextField(
+                name=name,
+                label=label,
+                optional=optional,
+                placeholder=placeholder,
+                hint=hint,
+                value=value,
+                min_length=min_length,
+                max_length=max_length,
+                subtype=subtype,
+            )
+        )
+        return self
+
+    def text_area(
+        self,
+        *,
+        name: str,
+        label: str,
+        optional: bool = False,
+        placeholder: Optional[str] = None,
+        hint: Optional[str] = None,
+        value: Optional[str] = None,
+        min_length: int = 0,
+        max_length: int = 3000,
+        subtype: Optional[str] = None,
+    ) -> "DialogBuilder":
+        """
+        A textarea is a multi-line plain text editing control. You've likely
+        encountered these on the world wide web. Use this element if you want a
+        relatively long answer from users. The element UI provides a remaining
+        character count to the max_length you have set or the default,
+        3000.
+
+        https://docs.slack.dev/legacy/legacy-dialogs/#attributes_textarea_elements
+
+        Args:
+            name: Name of form element. Required. No more than 300 characters.
+            label: Label displayed to user. Required. 48 character maximum.
+            optional: Provide true when the form element is not required. By
+                default, form elements are required.
+            placeholder: A string displayed as needed to help guide users in
+                completing the element. 150 character maximum.
+            hint: Helpful text provided to assist users in answering a question.
+                Up to 150 characters.
+            value: A default value for this field. Up to 3000 characters.
+            min_length: Minimum input length allowed for element. 1-3000
+                characters. Defaults to 0.
+            max_length: Maximum input length allowed for element. 0-3000
+                characters. Defaults to 3000.
+            subtype: A subtype for this text input. Accepts email, number, tel,
+                or url. In some form factors, optimized input is provided for this
+                subtype.
+        """
+        self._elements.append(
+            DialogTextArea(
+                name=name,
+                label=label,
+                optional=optional,
+                placeholder=placeholder,
+                hint=hint,
+                value=value,
+                min_length=min_length,
+                max_length=max_length,
+                subtype=subtype,
+            )
+        )
+        return self
+
+    def static_selector(
+        self,
+        *,
+        name: str,
+        label: str,
+        options: Union[Sequence[Option], Sequence[OptionGroup]],
+        optional: bool = False,
+        value: Optional[str] = None,
+        placeholder: Optional[str] = None,
+    ) -> "DialogBuilder":
+        """
+        Use the select element for multiple choice selections allowing users to pick
+        a single item from a list. True to web roots, this selection is displayed as
+        a dropdown menu.
+
+        A select element may contain up to 100 selections, provided as a list of
+        Option or OptionGroup objects
+
+        https://docs.slack.dev/legacy/legacy-dialogs/#attributes_select_elements
+
+        Args:
+            name: Name of form element. Required. No more than 300 characters.
+            label: Label displayed to user. Required. No more than 48 characters.
+            options: A list of up to 100 Option or OptionGroup objects. Object
+                types cannot be mixed.
+            optional: Provide true when the form element is not required. By
+                default, form elements are required.
+            value: Provide a default selected value.
+            placeholder: A string displayed as needed to help guide users in
+                completing the element. 150 character maximum.
+        """
+        self._elements.append(
+            DialogStaticSelector(
+                name=name,
+                label=label,
+                options=options,
+                optional=optional,
+                value=value,
+                placeholder=placeholder,
+            )
+        )
+        return self
+
+    def external_selector(
+        self,
+        *,
+        name: str,
+        label: str,
+        optional: bool = False,
+        value: Optional[Option] = None,
+        placeholder: Optional[str] = None,
+        min_query_length: Optional[int] = None,
+    ) -> "DialogBuilder":
+        """
+        Use the select element for multiple choice selections allowing users to pick
+        a single item from a list. True to web roots, this selection is displayed as
+        a dropdown menu.
+
+        A list of options can be loaded from an external URL and used in your dialog
+        menus.
+
+        https://docs.slack.dev/legacy/legacy-dialogs/#dynamic_select_elements_external
+
+        Args:
+            name: Name of form element. Required. No more than 300 characters.
+            label: Label displayed to user. Required. No more than 48 characters.
+            min_query_length: Specify the number of characters that must be
+                typed by a user into a dynamic select menu before dispatching to your
+                application.
+            optional: Provide true when the form element is not required. By
+                default, form elements are required.
+            value: Provide a default selected value. This should be a single
+                Option or OptionGroup that exactly matches one that will be returned
+                from your external endpoint.
+            placeholder: A string displayed as needed to help guide users in
+                completing the element. 150 character maximum.
+        """
+        self._elements.append(
+            DialogExternalSelector(
+                name=name,
+                label=label,
+                optional=optional,
+                value=value,
+                placeholder=placeholder,
+                min_query_length=min_query_length,
+            )
+        )
+        return self
+
+    def user_selector(
+        self,
+        *,
+        name: str,
+        label: str,
+        optional: bool = False,
+        value: Optional[str] = None,
+        placeholder: Optional[str] = None,
+    ) -> "DialogBuilder":
+        """
+        Now you can easily populate a select menu with a list of users. For example,
+        when you are creating a bug tracking app, you want to include a field for an
+        assignee. Slack pre-populates the user list in client-side, so your app
+        doesn't need access to a related OAuth scope.
+
+        https://docs.slack.dev/legacy/legacy-dialogs/#dynamic_select_elements_users
+
+        Args:
+            name: Name of form element. Required. No more than 300 characters.
+            label: Label displayed to user. Required. No more than 48 characters.
+            optional: Provide true when the form element is not required. By
+                default, form elements are required.
+            value: Provide a default selected value.
+            placeholder: A string displayed as needed to help guide users in
+                completing the element. 150 character maximum.
+        """
+        self._elements.append(
+            DialogUserSelector(
+                name=name,
+                label=label,
+                optional=optional,
+                value=value,
+                placeholder=placeholder,
+            )
+        )
+        return self
+
+    def channel_selector(
+        self,
+        *,
+        name: str,
+        label: str,
+        optional: bool = False,
+        value: Optional[str] = None,
+        placeholder: Optional[str] = None,
+    ) -> "DialogBuilder":
+        """
+        You can also provide a select menu with a list of channels. Specify your
+        data_source as channels to limit only to public channels
+
+        https://docs.slack.dev/legacy/legacy-dialogs/#dynamic_select_elements_channels_conversations
+
+        Args:
+            name: Name of form element. Required. No more than 300 characters.
+            label: Label displayed to user. Required. No more than 48 characters.
+            optional: Provide true when the form element is not required. By
+                default, form elements are required.
+            value: Provide a default selected value.
+            placeholder: A string displayed as needed to help guide users in
+                completing the element. 150 character maximum.
+        """
+        self._elements.append(
+            DialogChannelSelector(
+                name=name,
+                label=label,
+                optional=optional,
+                value=value,
+                placeholder=placeholder,
+            )
+        )
+        return self
+
+    def conversation_selector(
+        self,
+        *,
+        name: str,
+        label: str,
+        optional: bool = False,
+        value: Optional[str] = None,
+        placeholder: Optional[str] = None,
+    ) -> "DialogBuilder":
+        """
+        You can also provide a select menu with a list of conversations - including
+        private channels, direct messages, MPIMs, and whatever else we consider a
+        conversation-like thing.
+
+        https://docs.slack.dev/legacy/legacy-dialogs/#dynamic_select_elements_channels_conversations
+
+        Args:
+            name: Name of form element. Required. No more than 300 characters.
+            label: Label displayed to user. Required. No more than 48 characters.
+            optional: Provide true when the form element is not required. By
+                default, form elements are required.
+            value: Provide a default selected value.
+            placeholder: A string displayed as needed to help guide users in
+                completing the element. 150 character maximum.
+        """
+        self._elements.append(
+            DialogConversationSelector(
+                name=name,
+                label=label,
+                optional=optional,
+                value=value,
+                placeholder=placeholder,
+            )
+        )
+        return self
+
+    @JsonValidator("title attribute is required")
+    def title_present(self) -> bool:
+        return self._title is not None
+
+    @JsonValidator(f"title attribute cannot exceed {title_max_length} characters")
+    def title_length(self) -> bool:
+        return self._title is not None and len(self._title) <= self.title_max_length
+
+    @JsonValidator("callback_id attribute is required")
+    def callback_id_present(self) -> bool:
+        return self._callback_id is not None
+
+    @JsonValidator(f"dialogs must contain between 1 and {elements_max_length} elements")
+    def elements_length(self) -> bool:
+        return 0 < len(self._elements) <= self.elements_max_length
+
+    @JsonValidator(f"submit_label cannot exceed {submit_label_max_length} characters")
+    def submit_label_length(self) -> bool:
+        return self._submit_label is None or len(self._submit_label) <= self.submit_label_max_length
+
+    @JsonValidator("submit_label can only be one word")
+    def submit_label_valid(self) -> bool:
+        return self._submit_label is None or " " not in self._submit_label
+
+    @JsonValidator(f"state cannot exceed {state_max_length} characters")
+    def state_length(self) -> bool:
+        return not self._state or len(self._state) <= self.state_max_length
+
+    def to_dict(self) -> dict:
+        self.validate_json()
+        json = {
+            "title": self._title,
+            "callback_id": self._callback_id,
+            "elements": extract_json(self._elements),
+            "notify_on_cancel": self._notify_on_cancel,
+        }
+        if self._submit_label is not None:
+            json["submit_label"] = self._submit_label
+        if self._state is not None:
+            json["state"] = self._state
+        return json
+
+

The base class for JSON serializable class objects

+

Create a DialogBuilder to more easily construct the JSON required to submit a +dialog to Slack

+

Ancestors

+ +

Class variables

+
+
var attributes : Set[str]
+
+

The type of the None singleton.

+
+
var elements_max_length
+
+

The type of the None singleton.

+
+
var state_max_length
+
+

The type of the None singleton.

+
+
var submit_label_max_length
+
+

The type of the None singleton.

+
+
var title_max_length
+
+

The type of the None singleton.

+
+
+

Methods

+
+
+def callback_id(self, callback_id: str) ‑> DialogBuilder +
+
+
+ +Expand source code + +
def callback_id(self, callback_id: str) -> "DialogBuilder":
+    """
+    Specify a callback ID for this dialog, which your application will then
+    receive upon dialog submission
+
+    Args:
+      callback_id: a string identifying this particular dialog
+    """
+    self._callback_id = callback_id
+    return self
+
+

Specify a callback ID for this dialog, which your application will then +receive upon dialog submission

+

Args

+
+
callback_id
+
a string identifying this particular dialog
+
+
+
+def callback_id_present(self) ‑> bool +
+
+
+ +Expand source code + +
@JsonValidator("callback_id attribute is required")
+def callback_id_present(self) -> bool:
+    return self._callback_id is not None
+
+
+
+
+def channel_selector(self,
*,
name: str,
label: str,
optional: bool = False,
value: str | None = None,
placeholder: str | None = None) ‑> DialogBuilder
+
+
+
+ +Expand source code + +
def channel_selector(
+    self,
+    *,
+    name: str,
+    label: str,
+    optional: bool = False,
+    value: Optional[str] = None,
+    placeholder: Optional[str] = None,
+) -> "DialogBuilder":
+    """
+    You can also provide a select menu with a list of channels. Specify your
+    data_source as channels to limit only to public channels
+
+    https://docs.slack.dev/legacy/legacy-dialogs/#dynamic_select_elements_channels_conversations
+
+    Args:
+        name: Name of form element. Required. No more than 300 characters.
+        label: Label displayed to user. Required. No more than 48 characters.
+        optional: Provide true when the form element is not required. By
+            default, form elements are required.
+        value: Provide a default selected value.
+        placeholder: A string displayed as needed to help guide users in
+            completing the element. 150 character maximum.
+    """
+    self._elements.append(
+        DialogChannelSelector(
+            name=name,
+            label=label,
+            optional=optional,
+            value=value,
+            placeholder=placeholder,
+        )
+    )
+    return self
+
+

You can also provide a select menu with a list of channels. Specify your +data_source as channels to limit only to public channels

+

https://docs.slack.dev/legacy/legacy-dialogs/#dynamic_select_elements_channels_conversations

+

Args

+
+
name
+
Name of form element. Required. No more than 300 characters.
+
label
+
Label displayed to user. Required. No more than 48 characters.
+
optional
+
Provide true when the form element is not required. By +default, form elements are required.
+
value
+
Provide a default selected value.
+
placeholder
+
A string displayed as needed to help guide users in +completing the element. 150 character maximum.
+
+
+
+def conversation_selector(self,
*,
name: str,
label: str,
optional: bool = False,
value: str | None = None,
placeholder: str | None = None) ‑> DialogBuilder
+
+
+
+ +Expand source code + +
def conversation_selector(
+    self,
+    *,
+    name: str,
+    label: str,
+    optional: bool = False,
+    value: Optional[str] = None,
+    placeholder: Optional[str] = None,
+) -> "DialogBuilder":
+    """
+    You can also provide a select menu with a list of conversations - including
+    private channels, direct messages, MPIMs, and whatever else we consider a
+    conversation-like thing.
+
+    https://docs.slack.dev/legacy/legacy-dialogs/#dynamic_select_elements_channels_conversations
+
+    Args:
+        name: Name of form element. Required. No more than 300 characters.
+        label: Label displayed to user. Required. No more than 48 characters.
+        optional: Provide true when the form element is not required. By
+            default, form elements are required.
+        value: Provide a default selected value.
+        placeholder: A string displayed as needed to help guide users in
+            completing the element. 150 character maximum.
+    """
+    self._elements.append(
+        DialogConversationSelector(
+            name=name,
+            label=label,
+            optional=optional,
+            value=value,
+            placeholder=placeholder,
+        )
+    )
+    return self
+
+

You can also provide a select menu with a list of conversations - including +private channels, direct messages, MPIMs, and whatever else we consider a +conversation-like thing.

+

https://docs.slack.dev/legacy/legacy-dialogs/#dynamic_select_elements_channels_conversations

+

Args

+
+
name
+
Name of form element. Required. No more than 300 characters.
+
label
+
Label displayed to user. Required. No more than 48 characters.
+
optional
+
Provide true when the form element is not required. By +default, form elements are required.
+
value
+
Provide a default selected value.
+
placeholder
+
A string displayed as needed to help guide users in +completing the element. 150 character maximum.
+
+
+
+def elements_length(self) ‑> bool +
+
+
+ +Expand source code + +
@JsonValidator(f"dialogs must contain between 1 and {elements_max_length} elements")
+def elements_length(self) -> bool:
+    return 0 < len(self._elements) <= self.elements_max_length
+
+
+
+
+def external_selector(self,
*,
name: str,
label: str,
optional: bool = False,
value: Option | None = None,
placeholder: str | None = None,
min_query_length: int | None = None) ‑> DialogBuilder
+
+
+
+ +Expand source code + +
def external_selector(
+    self,
+    *,
+    name: str,
+    label: str,
+    optional: bool = False,
+    value: Optional[Option] = None,
+    placeholder: Optional[str] = None,
+    min_query_length: Optional[int] = None,
+) -> "DialogBuilder":
+    """
+    Use the select element for multiple choice selections allowing users to pick
+    a single item from a list. True to web roots, this selection is displayed as
+    a dropdown menu.
+
+    A list of options can be loaded from an external URL and used in your dialog
+    menus.
+
+    https://docs.slack.dev/legacy/legacy-dialogs/#dynamic_select_elements_external
+
+    Args:
+        name: Name of form element. Required. No more than 300 characters.
+        label: Label displayed to user. Required. No more than 48 characters.
+        min_query_length: Specify the number of characters that must be
+            typed by a user into a dynamic select menu before dispatching to your
+            application.
+        optional: Provide true when the form element is not required. By
+            default, form elements are required.
+        value: Provide a default selected value. This should be a single
+            Option or OptionGroup that exactly matches one that will be returned
+            from your external endpoint.
+        placeholder: A string displayed as needed to help guide users in
+            completing the element. 150 character maximum.
+    """
+    self._elements.append(
+        DialogExternalSelector(
+            name=name,
+            label=label,
+            optional=optional,
+            value=value,
+            placeholder=placeholder,
+            min_query_length=min_query_length,
+        )
+    )
+    return self
+
+

Use the select element for multiple choice selections allowing users to pick +a single item from a list. True to web roots, this selection is displayed as +a dropdown menu.

+

A list of options can be loaded from an external URL and used in your dialog +menus.

+

https://docs.slack.dev/legacy/legacy-dialogs/#dynamic_select_elements_external

+

Args

+
+
name
+
Name of form element. Required. No more than 300 characters.
+
label
+
Label displayed to user. Required. No more than 48 characters.
+
min_query_length
+
Specify the number of characters that must be +typed by a user into a dynamic select menu before dispatching to your +application.
+
optional
+
Provide true when the form element is not required. By +default, form elements are required.
+
value
+
Provide a default selected value. This should be a single +Option or OptionGroup that exactly matches one that will be returned +from your external endpoint.
+
placeholder
+
A string displayed as needed to help guide users in +completing the element. 150 character maximum.
+
+
+
+def notify_on_cancel(self, notify: bool) ‑> DialogBuilder +
+
+
+ +Expand source code + +
def notify_on_cancel(self, notify: bool) -> "DialogBuilder":
+    """
+    Whether this dialog should send a request to your application even if the
+    user cancels their interaction. Defaults to False.
+
+    Args:
+        notify: Set to True to indicate that your application should receive a
+            request even if the user cancels interaction with the dialog.
+    """
+    self._notify_on_cancel = notify
+    return self
+
+

Whether this dialog should send a request to your application even if the +user cancels their interaction. Defaults to False.

+

Args

+
+
notify
+
Set to True to indicate that your application should receive a +request even if the user cancels interaction with the dialog.
+
+
+
+def state(self, state: dict | str) ‑> DialogBuilder +
+
+
+ +Expand source code + +
def state(self, state: Union[dict, str]) -> "DialogBuilder":
+    """
+    Pass state into this dialog - dictionaries will be automatically formatted to
+    JSON
+
+    Args:
+        state: Extra state information that you need to pass from this dialog
+            back to your application on submission
+    """
+    if isinstance(state, dict):
+        self._state = dumps(state)
+    else:
+        self._state = state
+    return self
+
+

Pass state into this dialog - dictionaries will be automatically formatted to +JSON

+

Args

+
+
state
+
Extra state information that you need to pass from this dialog +back to your application on submission
+
+
+
+def state_length(self) ‑> bool +
+
+
+ +Expand source code + +
@JsonValidator(f"state cannot exceed {state_max_length} characters")
+def state_length(self) -> bool:
+    return not self._state or len(self._state) <= self.state_max_length
+
+
+
+
+def static_selector(self,
*,
name: str,
label: str,
options: Sequence[Option] | Sequence[OptionGroup],
optional: bool = False,
value: str | None = None,
placeholder: str | None = None) ‑> DialogBuilder
+
+
+
+ +Expand source code + +
def static_selector(
+    self,
+    *,
+    name: str,
+    label: str,
+    options: Union[Sequence[Option], Sequence[OptionGroup]],
+    optional: bool = False,
+    value: Optional[str] = None,
+    placeholder: Optional[str] = None,
+) -> "DialogBuilder":
+    """
+    Use the select element for multiple choice selections allowing users to pick
+    a single item from a list. True to web roots, this selection is displayed as
+    a dropdown menu.
+
+    A select element may contain up to 100 selections, provided as a list of
+    Option or OptionGroup objects
+
+    https://docs.slack.dev/legacy/legacy-dialogs/#attributes_select_elements
+
+    Args:
+        name: Name of form element. Required. No more than 300 characters.
+        label: Label displayed to user. Required. No more than 48 characters.
+        options: A list of up to 100 Option or OptionGroup objects. Object
+            types cannot be mixed.
+        optional: Provide true when the form element is not required. By
+            default, form elements are required.
+        value: Provide a default selected value.
+        placeholder: A string displayed as needed to help guide users in
+            completing the element. 150 character maximum.
+    """
+    self._elements.append(
+        DialogStaticSelector(
+            name=name,
+            label=label,
+            options=options,
+            optional=optional,
+            value=value,
+            placeholder=placeholder,
+        )
+    )
+    return self
+
+

Use the select element for multiple choice selections allowing users to pick +a single item from a list. True to web roots, this selection is displayed as +a dropdown menu.

+

A select element may contain up to 100 selections, provided as a list of +Option or OptionGroup objects

+

https://docs.slack.dev/legacy/legacy-dialogs/#attributes_select_elements

+

Args

+
+
name
+
Name of form element. Required. No more than 300 characters.
+
label
+
Label displayed to user. Required. No more than 48 characters.
+
options
+
A list of up to 100 Option or OptionGroup objects. Object +types cannot be mixed.
+
optional
+
Provide true when the form element is not required. By +default, form elements are required.
+
value
+
Provide a default selected value.
+
placeholder
+
A string displayed as needed to help guide users in +completing the element. 150 character maximum.
+
+
+
+def submit_label(self, label: str) ‑> DialogBuilder +
+
+
+ +Expand source code + +
def submit_label(self, label: str) -> "DialogBuilder":
+    """
+    The label to use on the 'Submit' button on the dialog. Defaults to 'Submit'
+    if not specified.
+
+    Args:
+        label: must not exceed 24 characters, and must be a single word (no
+            spaces)
+    """
+    self._submit_label = label
+    return self
+
+

The label to use on the 'Submit' button on the dialog. Defaults to 'Submit' +if not specified.

+

Args

+
+
label
+
must not exceed 24 characters, and must be a single word (no +spaces)
+
+
+
+def submit_label_length(self) ‑> bool +
+
+
+ +Expand source code + +
@JsonValidator(f"submit_label cannot exceed {submit_label_max_length} characters")
+def submit_label_length(self) -> bool:
+    return self._submit_label is None or len(self._submit_label) <= self.submit_label_max_length
+
+
+
+
+def submit_label_valid(self) ‑> bool +
+
+
+ +Expand source code + +
@JsonValidator("submit_label can only be one word")
+def submit_label_valid(self) -> bool:
+    return self._submit_label is None or " " not in self._submit_label
+
+
+
+
+def text_area(self,
*,
name: str,
label: str,
optional: bool = False,
placeholder: str | None = None,
hint: str | None = None,
value: str | None = None,
min_length: int = 0,
max_length: int = 3000,
subtype: str | None = None) ‑> DialogBuilder
+
+
+
+ +Expand source code + +
def text_area(
+    self,
+    *,
+    name: str,
+    label: str,
+    optional: bool = False,
+    placeholder: Optional[str] = None,
+    hint: Optional[str] = None,
+    value: Optional[str] = None,
+    min_length: int = 0,
+    max_length: int = 3000,
+    subtype: Optional[str] = None,
+) -> "DialogBuilder":
+    """
+    A textarea is a multi-line plain text editing control. You've likely
+    encountered these on the world wide web. Use this element if you want a
+    relatively long answer from users. The element UI provides a remaining
+    character count to the max_length you have set or the default,
+    3000.
+
+    https://docs.slack.dev/legacy/legacy-dialogs/#attributes_textarea_elements
+
+    Args:
+        name: Name of form element. Required. No more than 300 characters.
+        label: Label displayed to user. Required. 48 character maximum.
+        optional: Provide true when the form element is not required. By
+            default, form elements are required.
+        placeholder: A string displayed as needed to help guide users in
+            completing the element. 150 character maximum.
+        hint: Helpful text provided to assist users in answering a question.
+            Up to 150 characters.
+        value: A default value for this field. Up to 3000 characters.
+        min_length: Minimum input length allowed for element. 1-3000
+            characters. Defaults to 0.
+        max_length: Maximum input length allowed for element. 0-3000
+            characters. Defaults to 3000.
+        subtype: A subtype for this text input. Accepts email, number, tel,
+            or url. In some form factors, optimized input is provided for this
+            subtype.
+    """
+    self._elements.append(
+        DialogTextArea(
+            name=name,
+            label=label,
+            optional=optional,
+            placeholder=placeholder,
+            hint=hint,
+            value=value,
+            min_length=min_length,
+            max_length=max_length,
+            subtype=subtype,
+        )
+    )
+    return self
+
+

A textarea is a multi-line plain text editing control. You've likely +encountered these on the world wide web. Use this element if you want a +relatively long answer from users. The element UI provides a remaining +character count to the max_length you have set or the default, +3000.

+

https://docs.slack.dev/legacy/legacy-dialogs/#attributes_textarea_elements

+

Args

+
+
name
+
Name of form element. Required. No more than 300 characters.
+
label
+
Label displayed to user. Required. 48 character maximum.
+
optional
+
Provide true when the form element is not required. By +default, form elements are required.
+
placeholder
+
A string displayed as needed to help guide users in +completing the element. 150 character maximum.
+
hint
+
Helpful text provided to assist users in answering a question. +Up to 150 characters.
+
value
+
A default value for this field. Up to 3000 characters.
+
min_length
+
Minimum input length allowed for element. 1-3000 +characters. Defaults to 0.
+
max_length
+
Maximum input length allowed for element. 0-3000 +characters. Defaults to 3000.
+
subtype
+
A subtype for this text input. Accepts email, number, tel, +or url. In some form factors, optimized input is provided for this +subtype.
+
+
+
+def text_field(self,
*,
name: str,
label: str,
optional: bool = False,
placeholder: str | None = None,
hint: str | None = None,
value: str | None = None,
min_length: int = 0,
max_length: int = 150,
subtype: str | None = None) ‑> DialogBuilder
+
+
+
+ +Expand source code + +
def text_field(
+    self,
+    *,
+    name: str,
+    label: str,
+    optional: bool = False,
+    placeholder: Optional[str] = None,
+    hint: Optional[str] = None,
+    value: Optional[str] = None,
+    min_length: int = 0,
+    max_length: int = 150,
+    subtype: Optional[str] = None,
+) -> "DialogBuilder":
+    """
+    Text elements are single-line plain text fields.
+
+    https://docs.slack.dev/legacy/legacy-dialogs/#attributes_text_elements
+
+    Args:
+        name: Name of form element. Required. No more than 300 characters.
+        label: Label displayed to user. Required. 48 character maximum.
+        optional: Provide true when the form element is not required. By
+            default, form elements are required.
+        placeholder: A string displayed as needed to help guide users in
+            completing the element. 150 character maximum.
+        hint: Helpful text provided to assist users in answering a question.
+            Up to 150 characters.
+        value: A default value for this field. Up to 150 characters.
+        min_length: Minimum input length allowed for element. Up to 150
+            characters. Defaults to 0.
+        max_length: Maximum input length allowed for element. Up to 150
+            characters. Defaults to 150.
+        subtype: A subtype for this text input. Accepts email, number, tel,
+                or url. In some form factors, optimized input is provided for this
+                subtype.
+    """
+    self._elements.append(
+        DialogTextField(
+            name=name,
+            label=label,
+            optional=optional,
+            placeholder=placeholder,
+            hint=hint,
+            value=value,
+            min_length=min_length,
+            max_length=max_length,
+            subtype=subtype,
+        )
+    )
+    return self
+
+

Text elements are single-line plain text fields.

+

https://docs.slack.dev/legacy/legacy-dialogs/#attributes_text_elements

+

Args

+
+
name
+
Name of form element. Required. No more than 300 characters.
+
label
+
Label displayed to user. Required. 48 character maximum.
+
optional
+
Provide true when the form element is not required. By +default, form elements are required.
+
placeholder
+
A string displayed as needed to help guide users in +completing the element. 150 character maximum.
+
hint
+
Helpful text provided to assist users in answering a question. +Up to 150 characters.
+
value
+
A default value for this field. Up to 150 characters.
+
min_length
+
Minimum input length allowed for element. Up to 150 +characters. Defaults to 0.
+
max_length
+
Maximum input length allowed for element. Up to 150 +characters. Defaults to 150.
+
subtype
+
A subtype for this text input. Accepts email, number, tel, +or url. In some form factors, optimized input is provided for this +subtype.
+
+
+
+def title(self, title: str) ‑> DialogBuilder +
+
+
+ +Expand source code + +
def title(self, title: str) -> "DialogBuilder":
+    """
+    Specify a title for this dialog
+
+    Args:
+      title: must not exceed 24 characters
+    """
+    self._title = title
+    return self
+
+

Specify a title for this dialog

+

Args

+
+
title
+
must not exceed 24 characters
+
+
+
+def title_length(self) ‑> bool +
+
+
+ +Expand source code + +
@JsonValidator(f"title attribute cannot exceed {title_max_length} characters")
+def title_length(self) -> bool:
+    return self._title is not None and len(self._title) <= self.title_max_length
+
+
+
+
+def title_present(self) ‑> bool +
+
+
+ +Expand source code + +
@JsonValidator("title attribute is required")
+def title_present(self) -> bool:
+    return self._title is not None
+
+
+
+
+def user_selector(self,
*,
name: str,
label: str,
optional: bool = False,
value: str | None = None,
placeholder: str | None = None) ‑> DialogBuilder
+
+
+
+ +Expand source code + +
def user_selector(
+    self,
+    *,
+    name: str,
+    label: str,
+    optional: bool = False,
+    value: Optional[str] = None,
+    placeholder: Optional[str] = None,
+) -> "DialogBuilder":
+    """
+    Now you can easily populate a select menu with a list of users. For example,
+    when you are creating a bug tracking app, you want to include a field for an
+    assignee. Slack pre-populates the user list in client-side, so your app
+    doesn't need access to a related OAuth scope.
+
+    https://docs.slack.dev/legacy/legacy-dialogs/#dynamic_select_elements_users
+
+    Args:
+        name: Name of form element. Required. No more than 300 characters.
+        label: Label displayed to user. Required. No more than 48 characters.
+        optional: Provide true when the form element is not required. By
+            default, form elements are required.
+        value: Provide a default selected value.
+        placeholder: A string displayed as needed to help guide users in
+            completing the element. 150 character maximum.
+    """
+    self._elements.append(
+        DialogUserSelector(
+            name=name,
+            label=label,
+            optional=optional,
+            value=value,
+            placeholder=placeholder,
+        )
+    )
+    return self
+
+

Now you can easily populate a select menu with a list of users. For example, +when you are creating a bug tracking app, you want to include a field for an +assignee. Slack pre-populates the user list in client-side, so your app +doesn't need access to a related OAuth scope.

+

https://docs.slack.dev/legacy/legacy-dialogs/#dynamic_select_elements_users

+

Args

+
+
name
+
Name of form element. Required. No more than 300 characters.
+
label
+
Label displayed to user. Required. No more than 48 characters.
+
optional
+
Provide true when the form element is not required. By +default, form elements are required.
+
value
+
Provide a default selected value.
+
placeholder
+
A string displayed as needed to help guide users in +completing the element. 150 character maximum.
+
+
+
+

Inherited members

+ +
+
+class DialogChannelSelector +(*,
name: str,
label: str,
optional: bool = False,
value: str | None = None,
placeholder: str | None = None)
+
+
+
+ +Expand source code + +
class DialogChannelSelector(AbstractDialogSelector):
+    data_source = "channels"
+
+    def __init__(
+        self,
+        *,
+        name: str,
+        label: str,
+        optional: bool = False,
+        value: Optional[str] = None,
+        placeholder: Optional[str] = None,
+    ):
+        """
+        You can also provide a select menu with a list of channels. Specify your
+        data_source as channels to limit only to public channels
+
+        https://docs.slack.dev/legacy/legacy-dialogs/#dynamic_select_elements_channels_conversations
+
+        Args:
+            name: Name of form element. Required. No more than 300 characters.
+            label: Label displayed to user. Required. No more than 48 characters.
+            optional: Provide true when the form element is not required. By
+                default, form elements are required.
+            value: Provide a default selected value.
+            placeholder: A string displayed as needed to help guide users in
+                completing the element. 150 character maximum.
+        """
+        super().__init__(
+            name=name,
+            label=label,
+            optional=optional,
+            value=value,
+            placeholder=placeholder,
+        )
+
+

The base class for JSON serializable class objects

+

You can also provide a select menu with a list of channels. Specify your +data_source as channels to limit only to public channels

+

https://docs.slack.dev/legacy/legacy-dialogs/#dynamic_select_elements_channels_conversations

+

Args

+
+
name
+
Name of form element. Required. No more than 300 characters.
+
label
+
Label displayed to user. Required. No more than 48 characters.
+
optional
+
Provide true when the form element is not required. By +default, form elements are required.
+
value
+
Provide a default selected value.
+
placeholder
+
A string displayed as needed to help guide users in +completing the element. 150 character maximum.
+
+

Ancestors

+ +

Class variables

+
+
var data_source
+
+

The type of the None singleton.

+
+
+

Inherited members

+ +
+
+class DialogConversationSelector +(*,
name: str,
label: str,
optional: bool = False,
value: str | None = None,
placeholder: str | None = None)
+
+
+
+ +Expand source code + +
class DialogConversationSelector(AbstractDialogSelector):
+    data_source = "conversations"
+
+    def __init__(
+        self,
+        *,
+        name: str,
+        label: str,
+        optional: bool = False,
+        value: Optional[str] = None,
+        placeholder: Optional[str] = None,
+    ):
+        """
+        You can also provide a select menu with a list of conversations - including
+        private channels, direct messages, MPIMs, and whatever else we consider a
+        conversation-like thing.
+
+        https://docs.slack.dev/legacy/legacy-dialogs/#dynamic_select_elements_channels_conversations
+
+        Args:
+            name: Name of form element. Required. No more than 300 characters.
+            label: Label displayed to user. Required. No more than 48 characters.
+            optional: Provide true when the form element is not required. By
+                default, form elements are required.
+            value: Provide a default selected value.
+            placeholder: A string displayed as needed to help guide users in
+                completing the element. 150 character maximum.
+        """
+        super().__init__(
+            name=name,
+            label=label,
+            optional=optional,
+            value=value,
+            placeholder=placeholder,
+        )
+
+

The base class for JSON serializable class objects

+

You can also provide a select menu with a list of conversations - including +private channels, direct messages, MPIMs, and whatever else we consider a +conversation-like thing.

+

https://docs.slack.dev/legacy/legacy-dialogs/#dynamic_select_elements_channels_conversations

+

Args

+
+
name
+
Name of form element. Required. No more than 300 characters.
+
label
+
Label displayed to user. Required. No more than 48 characters.
+
optional
+
Provide true when the form element is not required. By +default, form elements are required.
+
value
+
Provide a default selected value.
+
placeholder
+
A string displayed as needed to help guide users in +completing the element. 150 character maximum.
+
+

Ancestors

+ +

Class variables

+
+
var data_source
+
+

The type of the None singleton.

+
+
+

Inherited members

+ +
+
+class DialogExternalSelector +(*,
name: str,
label: str,
value: Option | None = None,
min_query_length: int | None = None,
optional: bool | None = False,
placeholder: str | None = None)
+
+
+
+ +Expand source code + +
class DialogExternalSelector(AbstractDialogSelector):
+    data_source = "external"
+
+    @property
+    def attributes(self) -> Set[str]:  # type: ignore[override]
+        return super().attributes.union({"min_query_length"})
+
+    def __init__(
+        self,
+        *,
+        name: str,
+        label: str,
+        value: Optional[Option] = None,
+        min_query_length: Optional[int] = None,
+        optional: Optional[bool] = False,
+        placeholder: Optional[str] = None,
+    ):
+        """
+        Use the select element for multiple choice selections allowing users to pick
+        a single item from a list. True to web roots, this selection is displayed as
+        a dropdown menu.
+
+        A list of options can be loaded from an external URL and used in your dialog
+        menus.
+
+        https://docs.slack.dev/legacy/legacy-dialogs/#dynamic_select_elements_external
+
+        Args:
+            name: Name of form element. Required. No more than 300 characters.
+            label: Label displayed to user. Required. No more than 48 characters.
+            min_query_length: Specify the number of characters that must be typed
+                by a user into a dynamic select menu before dispatching to the app.
+            optional: Provide true when the form element is not required. By
+                default, form elements are required.
+            value: Provide a default selected value. This should be a single
+                Option or OptionGroup that exactly matches one that will be returned
+                from your external endpoint.
+            placeholder: A string displayed as needed to help guide users in
+                completing the element. 150 character maximum.
+        """
+        super().__init__(
+            name=name,
+            label=label,
+            value=value,
+            optional=optional,  # type: ignore[arg-type]
+            placeholder=placeholder,
+        )
+        self.min_query_length = min_query_length
+
+

The base class for JSON serializable class objects

+

Use the select element for multiple choice selections allowing users to pick +a single item from a list. True to web roots, this selection is displayed as +a dropdown menu.

+

A list of options can be loaded from an external URL and used in your dialog +menus.

+

https://docs.slack.dev/legacy/legacy-dialogs/#dynamic_select_elements_external

+

Args

+
+
name
+
Name of form element. Required. No more than 300 characters.
+
label
+
Label displayed to user. Required. No more than 48 characters.
+
min_query_length
+
Specify the number of characters that must be typed +by a user into a dynamic select menu before dispatching to the app.
+
optional
+
Provide true when the form element is not required. By +default, form elements are required.
+
value
+
Provide a default selected value. This should be a single +Option or OptionGroup that exactly matches one that will be returned +from your external endpoint.
+
placeholder
+
A string displayed as needed to help guide users in +completing the element. 150 character maximum.
+
+

Ancestors

+ +

Class variables

+
+
var data_source
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
prop attributes : Set[str]
+
+
+ +Expand source code + +
@property
+def attributes(self) -> Set[str]:  # type: ignore[override]
+    return super().attributes.union({"min_query_length"})
+
+

Build an unordered collection of unique elements.

+
+
+

Inherited members

+ +
+
+class DialogStaticSelector +(*,
name: str,
label: str,
options: Sequence[Option] | Sequence[OptionGroup],
optional: bool = False,
value: Option | str | None = None,
placeholder: str | None = None)
+
+
+
+ +Expand source code + +
class DialogStaticSelector(AbstractDialogSelector):
+    """
+    Use the select element for multiple choice selections allowing users to pick a
+    single item from a list. True to web roots, this selection is displayed as a
+    dropdown menu.
+
+    https://docs.slack.dev/legacy/legacy-dialogs/#select_elements
+    """
+
+    data_source = "static"
+
+    options_max_length = 100
+
+    def __init__(
+        self,
+        *,
+        name: str,
+        label: str,
+        options: Union[Sequence[Option], Sequence[OptionGroup]],
+        optional: bool = False,
+        value: Optional[Union[Option, str]] = None,
+        placeholder: Optional[str] = None,
+    ):
+        """
+        Use the select element for multiple choice selections allowing users to pick
+        a single item from a list. True to web roots, this selection is displayed as
+        a dropdown menu.
+
+        A select element may contain up to 100 selections, provided as a list of
+        Option or OptionGroup objects
+
+        https://docs.slack.dev/legacy/legacy-dialogs/#attributes_select_elements
+
+        Args:
+            name: Name of form element. Required. No more than 300 characters.
+            label: Label displayed to user. Required. No more than 48 characters.
+            options: A list of up to 100 Option or OptionGroup objects. Object
+                types cannot be mixed.
+            optional: Provide true when the form element is not required. By
+                default, form elements are required.
+            value: Provide a default selected value.
+            placeholder: A string displayed as needed to help guide users in
+                completing the element. 150 character maximum.
+        """
+        super().__init__(
+            name=name,
+            label=label,
+            optional=optional,
+            value=value,
+            placeholder=placeholder,
+        )
+        self.options = options
+
+    @JsonValidator(f"options attribute cannot exceed {options_max_length} items")
+    def options_length(self) -> bool:
+        return len(self.options) < self.options_max_length
+
+    def to_dict(self) -> dict:
+        json = super().to_dict()
+        if isinstance(self.options[0], OptionGroup):
+            json["option_groups"] = extract_json(self.options, "dialog")
+        else:
+            json["options"] = extract_json(self.options, "dialog")
+        return json
+
+

Use the select element for multiple choice selections allowing users to pick a +single item from a list. True to web roots, this selection is displayed as a +dropdown menu.

+

https://docs.slack.dev/legacy/legacy-dialogs/#select_elements

+

Use the select element for multiple choice selections allowing users to pick +a single item from a list. True to web roots, this selection is displayed as +a dropdown menu.

+

A select element may contain up to 100 selections, provided as a list of +Option or OptionGroup objects

+

https://docs.slack.dev/legacy/legacy-dialogs/#attributes_select_elements

+

Args

+
+
name
+
Name of form element. Required. No more than 300 characters.
+
label
+
Label displayed to user. Required. No more than 48 characters.
+
options
+
A list of up to 100 Option or OptionGroup objects. Object +types cannot be mixed.
+
optional
+
Provide true when the form element is not required. By +default, form elements are required.
+
value
+
Provide a default selected value.
+
placeholder
+
A string displayed as needed to help guide users in +completing the element. 150 character maximum.
+
+

Ancestors

+ +

Class variables

+
+
var data_source
+
+

The type of the None singleton.

+
+
var options_max_length
+
+

The type of the None singleton.

+
+
+

Methods

+
+
+def options_length(self) ‑> bool +
+
+
+ +Expand source code + +
@JsonValidator(f"options attribute cannot exceed {options_max_length} items")
+def options_length(self) -> bool:
+    return len(self.options) < self.options_max_length
+
+
+
+
+

Inherited members

+ +
+
+class DialogTextArea +(*,
name: str,
label: str,
optional: bool = False,
placeholder: str | None = None,
hint: str | None = None,
value: str | None = None,
min_length: int = 0,
max_length: int | None = None,
subtype: str | None = None)
+
+
+
+ +Expand source code + +
class DialogTextArea(DialogTextComponent):
+    """
+    A textarea is a multi-line plain text editing control. You've likely encountered
+    these on the world wide web. Use this element if you want a relatively long
+    answer from users. The element UI provides a remaining character count to the
+    max_length you have set or the default, 3000.
+
+    https://docs.slack.dev/legacy/legacy-dialogs/#textarea_elements
+    """
+
+    type = "textarea"
+    max_value_length = 3000
+
+

A textarea is a multi-line plain text editing control. You've likely encountered +these on the world wide web. Use this element if you want a relatively long +answer from users. The element UI provides a remaining character count to the +max_length you have set or the default, 3000.

+

https://docs.slack.dev/legacy/legacy-dialogs/#textarea_elements

+

Ancestors

+ +

Class variables

+
+
var max_value_length
+
+

The type of the None singleton.

+
+
var type
+
+

The type of the None singleton.

+
+
+

Inherited members

+ +
+
+class DialogTextComponent +(*,
name: str,
label: str,
optional: bool = False,
placeholder: str | None = None,
hint: str | None = None,
value: str | None = None,
min_length: int = 0,
max_length: int | None = None,
subtype: str | None = None)
+
+
+
+ +Expand source code + +
class DialogTextComponent(JsonObject, metaclass=ABCMeta):
+    attributes = {
+        "hint",
+        "label",
+        "max_length",
+        "min_length",
+        "name",
+        "optional",
+        "placeholder",
+        "subtype",
+        "type",
+        "value",
+    }
+
+    name_max_length = 300
+    label_max_length = 48
+    placeholder_max_length = 150
+    hint_max_length = 150
+
+    @property
+    @abstractmethod
+    def type(self):
+        pass
+
+    @property
+    @abstractmethod
+    def max_value_length(self):
+        pass
+
+    def __init__(
+        self,
+        *,
+        name: str,
+        label: str,
+        optional: bool = False,
+        placeholder: Optional[str] = None,
+        hint: Optional[str] = None,
+        value: Optional[str] = None,
+        min_length: int = 0,
+        max_length: Optional[int] = None,
+        subtype: Optional[str] = None,
+    ):
+        self.name = name
+        self.label = label
+        self.optional = optional
+        self.placeholder = placeholder
+        self.hint = hint
+        self.value = value
+        self.min_length = min_length
+        self.max_length = max_length or self.max_value_length
+        self.subtype = subtype
+
+    @JsonValidator(f"name attribute cannot exceed {name_max_length} characters")
+    def name_length(self) -> bool:
+        return len(self.name) < self.name_max_length
+
+    @JsonValidator(f"label attribute cannot exceed {label_max_length} characters")
+    def label_length(self) -> bool:
+        return len(self.label) < self.label_max_length
+
+    @JsonValidator(f"placeholder attribute cannot exceed {placeholder_max_length} characters")
+    def placeholder_length(self) -> bool:
+        return self.placeholder is None or len(self.placeholder) < self.placeholder_max_length
+
+    @JsonValidator(f"hint attribute cannot exceed {hint_max_length} characters")
+    def hint_length(self) -> bool:
+        return self.hint is None or len(self.hint) < self.hint_max_length
+
+    @JsonValidator("value attribute exceeded bounds")
+    def value_length(self) -> bool:
+        return self.value is None or len(self.value) < self.max_value_length
+
+    @JsonValidator("min_length attribute must be greater than or equal to 0")
+    def min_length_above_zero(self) -> bool:
+        return self.min_length is None or self.min_length >= 0
+
+    @JsonValidator("min_length attribute exceed bounds")
+    def min_length_length(self) -> bool:
+        return self.min_length is None or self.min_length <= self.max_value_length
+
+    @JsonValidator("min_length attribute must be less than max value attribute")
+    def min_length_below_max_length(self) -> bool:
+        return self.min_length is None or self.min_length < self.max_length
+
+    @JsonValidator("max_length attribute must be greater than or equal to 0")
+    def max_length_above_zero(self) -> bool:
+        return self.max_length is None or self.max_length > 0
+
+    @JsonValidator("max_length attribute exceeded bounds")
+    def max_length_length(self) -> bool:
+        return self.max_length is None or self.max_length <= self.max_value_length
+
+    @EnumValidator("subtype", TextElementSubtypes)
+    def subtype_valid(self) -> bool:
+        return self.subtype is None or self.subtype in TextElementSubtypes
+
+

The base class for JSON serializable class objects

+

Ancestors

+ +

Subclasses

+ +

Class variables

+
+
var attributes
+
+

The type of the None singleton.

+
+
var hint_max_length
+
+

The type of the None singleton.

+
+
var label_max_length
+
+

The type of the None singleton.

+
+
var name_max_length
+
+

The type of the None singleton.

+
+
var placeholder_max_length
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
prop max_value_length
+
+
+ +Expand source code + +
@property
+@abstractmethod
+def max_value_length(self):
+    pass
+
+
+
+
prop type
+
+
+ +Expand source code + +
@property
+@abstractmethod
+def type(self):
+    pass
+
+
+
+
+

Methods

+
+
+def hint_length(self) ‑> bool +
+
+
+ +Expand source code + +
@JsonValidator(f"hint attribute cannot exceed {hint_max_length} characters")
+def hint_length(self) -> bool:
+    return self.hint is None or len(self.hint) < self.hint_max_length
+
+
+
+
+def label_length(self) ‑> bool +
+
+
+ +Expand source code + +
@JsonValidator(f"label attribute cannot exceed {label_max_length} characters")
+def label_length(self) -> bool:
+    return len(self.label) < self.label_max_length
+
+
+
+
+def max_length_above_zero(self) ‑> bool +
+
+
+ +Expand source code + +
@JsonValidator("max_length attribute must be greater than or equal to 0")
+def max_length_above_zero(self) -> bool:
+    return self.max_length is None or self.max_length > 0
+
+
+
+
+def max_length_length(self) ‑> bool +
+
+
+ +Expand source code + +
@JsonValidator("max_length attribute exceeded bounds")
+def max_length_length(self) -> bool:
+    return self.max_length is None or self.max_length <= self.max_value_length
+
+
+
+
+def min_length_above_zero(self) ‑> bool +
+
+
+ +Expand source code + +
@JsonValidator("min_length attribute must be greater than or equal to 0")
+def min_length_above_zero(self) -> bool:
+    return self.min_length is None or self.min_length >= 0
+
+
+
+
+def min_length_below_max_length(self) ‑> bool +
+
+
+ +Expand source code + +
@JsonValidator("min_length attribute must be less than max value attribute")
+def min_length_below_max_length(self) -> bool:
+    return self.min_length is None or self.min_length < self.max_length
+
+
+
+
+def min_length_length(self) ‑> bool +
+
+
+ +Expand source code + +
@JsonValidator("min_length attribute exceed bounds")
+def min_length_length(self) -> bool:
+    return self.min_length is None or self.min_length <= self.max_value_length
+
+
+
+
+def name_length(self) ‑> bool +
+
+
+ +Expand source code + +
@JsonValidator(f"name attribute cannot exceed {name_max_length} characters")
+def name_length(self) -> bool:
+    return len(self.name) < self.name_max_length
+
+
+
+
+def placeholder_length(self) ‑> bool +
+
+
+ +Expand source code + +
@JsonValidator(f"placeholder attribute cannot exceed {placeholder_max_length} characters")
+def placeholder_length(self) -> bool:
+    return self.placeholder is None or len(self.placeholder) < self.placeholder_max_length
+
+
+
+
+def subtype_valid(self) ‑> bool +
+
+
+ +Expand source code + +
@EnumValidator("subtype", TextElementSubtypes)
+def subtype_valid(self) -> bool:
+    return self.subtype is None or self.subtype in TextElementSubtypes
+
+
+
+
+def value_length(self) ‑> bool +
+
+
+ +Expand source code + +
@JsonValidator("value attribute exceeded bounds")
+def value_length(self) -> bool:
+    return self.value is None or len(self.value) < self.max_value_length
+
+
+
+
+

Inherited members

+ +
+
+class DialogTextField +(*,
name: str,
label: str,
optional: bool = False,
placeholder: str | None = None,
hint: str | None = None,
value: str | None = None,
min_length: int = 0,
max_length: int | None = None,
subtype: str | None = None)
+
+
+
+ +Expand source code + +
class DialogTextField(DialogTextComponent):
+    """
+    Text elements are single-line plain text fields.
+
+    https://docs.slack.dev/legacy/legacy-dialogs/#text_elements
+    """
+
+    type = "text"
+    max_value_length = 150
+
+

Text elements are single-line plain text fields.

+

https://docs.slack.dev/legacy/legacy-dialogs/#text_elements

+

Ancestors

+ +

Class variables

+
+
var max_value_length
+
+

The type of the None singleton.

+
+
var type
+
+

The type of the None singleton.

+
+
+

Inherited members

+ +
+
+class DialogUserSelector +(*,
name: str,
label: str,
optional: bool = False,
value: str | None = None,
placeholder: str | None = None)
+
+
+
+ +Expand source code + +
class DialogUserSelector(AbstractDialogSelector):
+    data_source = "users"
+
+    def __init__(
+        self,
+        *,
+        name: str,
+        label: str,
+        optional: bool = False,
+        value: Optional[str] = None,
+        placeholder: Optional[str] = None,
+    ):
+        """
+        Now you can easily populate a select menu with a list of users. For example,
+        when you are creating a bug tracking app, you want to include a field for an
+        assignee. Slack pre-populates the user list in client-side, so your app
+        doesn't need access to a related OAuth scope.
+
+        https://docs.slack.dev/legacy/legacy-dialogs/#dynamic_select_elements_users
+
+        Args:
+            name: Name of form element. Required. No more than 300 characters.
+            label: Label displayed to user. Required. No more than 48 characters.
+            optional: Provide true when the form element is not required. By
+                default, form elements are required.
+            value: Provide a default selected value.
+            placeholder: A string displayed as needed to help guide users in
+                completing the element. 150 character maximum.
+        """
+        super().__init__(
+            name=name,
+            label=label,
+            optional=optional,
+            value=value,
+            placeholder=placeholder,
+        )
+
+

The base class for JSON serializable class objects

+

Now you can easily populate a select menu with a list of users. For example, +when you are creating a bug tracking app, you want to include a field for an +assignee. Slack pre-populates the user list in client-side, so your app +doesn't need access to a related OAuth scope.

+

https://docs.slack.dev/legacy/legacy-dialogs/#dynamic_select_elements_users

+

Args

+
+
name
+
Name of form element. Required. No more than 300 characters.
+
label
+
Label displayed to user. Required. No more than 48 characters.
+
optional
+
Provide true when the form element is not required. By +default, form elements are required.
+
value
+
Provide a default selected value.
+
placeholder
+
A string displayed as needed to help guide users in +completing the element. 150 character maximum.
+
+

Ancestors

+ +

Class variables

+
+
var data_source
+
+

The type of the None singleton.

+
+
+

Inherited members

+ +
+
+
+
+ +
+ + + diff --git a/docs/reference/models/dialogs/index.html b/docs/reference/models/dialogs/index.html new file mode 100644 index 000000000..e0c6fe457 --- /dev/null +++ b/docs/reference/models/dialogs/index.html @@ -0,0 +1,2653 @@ + + + + + + +slack_sdk.models.dialogs API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.models.dialogs

+
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class AbstractDialogSelector +(*,
name: str,
label: str,
optional: bool = False,
value: Option | str | None = None,
placeholder: str | None = None)
+
+
+
+ +Expand source code + +
class AbstractDialogSelector(JsonObject, metaclass=ABCMeta):
+    DataSourceTypes = DynamicSelectElementTypes.union({"external", "static"})
+
+    attributes = {"data_source", "label", "name", "optional", "placeholder", "type"}
+
+    name_max_length = 300
+    label_max_length = 48
+    placeholder_max_length = 150
+
+    @property
+    @abstractmethod
+    def data_source(self) -> str:
+        pass
+
+    def __init__(
+        self,
+        *,
+        name: str,
+        label: str,
+        optional: bool = False,
+        value: Optional[Union[Option, str]] = None,
+        placeholder: Optional[str] = None,
+    ):
+        self.name = name
+        self.label = label
+        self.optional = optional
+        self.value = value
+        self.placeholder = placeholder
+        self.type = "select"
+
+    @JsonValidator(f"name attribute cannot exceed {name_max_length} characters")
+    def name_length(self) -> bool:
+        return len(self.name) < self.name_max_length
+
+    @JsonValidator(f"label attribute cannot exceed {label_max_length} characters")
+    def label_length(self) -> bool:
+        return len(self.label) < self.label_max_length
+
+    @JsonValidator(f"placeholder attribute cannot exceed {placeholder_max_length} characters")
+    def placeholder_length(self) -> bool:
+        return self.placeholder is None or len(self.placeholder) < self.placeholder_max_length
+
+    @EnumValidator("data_source", DataSourceTypes)
+    def data_source_valid(self) -> bool:
+        return self.data_source in self.DataSourceTypes
+
+    def to_dict(self) -> dict:
+        json = super().to_dict()
+        if self.data_source == "external":
+            if isinstance(self.value, Option):
+                json["selected_options"] = extract_json([self.value], "dialog")
+            elif self.value is not None:
+                json["selected_options"] = Option.from_single_value(self.value)
+        else:
+            if isinstance(self.value, Option):
+                json["value"] = self.value.value
+            elif self.value is not None:
+                json["value"] = self.value
+        return json
+
+

The base class for JSON serializable class objects

+

Ancestors

+ +

Subclasses

+ +

Class variables

+
+
var DataSourceTypes
+
+

The type of the None singleton.

+
+
var attributes
+
+

The type of the None singleton.

+
+
var label_max_length
+
+

The type of the None singleton.

+
+
var name_max_length
+
+

The type of the None singleton.

+
+
var placeholder_max_length
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
prop data_source : str
+
+
+ +Expand source code + +
@property
+@abstractmethod
+def data_source(self) -> str:
+    pass
+
+
+
+
+

Methods

+
+
+def data_source_valid(self) ‑> bool +
+
+
+ +Expand source code + +
@EnumValidator("data_source", DataSourceTypes)
+def data_source_valid(self) -> bool:
+    return self.data_source in self.DataSourceTypes
+
+
+
+
+def label_length(self) ‑> bool +
+
+
+ +Expand source code + +
@JsonValidator(f"label attribute cannot exceed {label_max_length} characters")
+def label_length(self) -> bool:
+    return len(self.label) < self.label_max_length
+
+
+
+
+def name_length(self) ‑> bool +
+
+
+ +Expand source code + +
@JsonValidator(f"name attribute cannot exceed {name_max_length} characters")
+def name_length(self) -> bool:
+    return len(self.name) < self.name_max_length
+
+
+
+
+def placeholder_length(self) ‑> bool +
+
+
+ +Expand source code + +
@JsonValidator(f"placeholder attribute cannot exceed {placeholder_max_length} characters")
+def placeholder_length(self) -> bool:
+    return self.placeholder is None or len(self.placeholder) < self.placeholder_max_length
+
+
+
+
+

Inherited members

+ +
+
+class DialogBuilder +
+
+
+ +Expand source code + +
class DialogBuilder(JsonObject):
+    attributes: Set[str] = set()
+
+    _callback_id: Optional[str]
+    _elements: List[Union[DialogTextComponent, AbstractDialogSelector]]
+    _submit_label: Optional[str]
+    _notify_on_cancel: bool
+    _state: Optional[str]
+
+    title_max_length = 24
+    submit_label_max_length = 24
+    elements_max_length = 10
+    state_max_length = 3000
+
+    def __init__(self):
+        """
+        Create a DialogBuilder to more easily construct the JSON required to submit a
+        dialog to Slack
+        """
+        self._title = None
+        self._callback_id = None
+        self._elements = []
+        self._submit_label = None
+        self._notify_on_cancel = False
+        self._state = None
+
+    def title(self, title: str) -> "DialogBuilder":
+        """
+        Specify a title for this dialog
+
+        Args:
+          title: must not exceed 24 characters
+        """
+        self._title = title
+        return self
+
+    def state(self, state: Union[dict, str]) -> "DialogBuilder":
+        """
+        Pass state into this dialog - dictionaries will be automatically formatted to
+        JSON
+
+        Args:
+            state: Extra state information that you need to pass from this dialog
+                back to your application on submission
+        """
+        if isinstance(state, dict):
+            self._state = dumps(state)
+        else:
+            self._state = state
+        return self
+
+    def callback_id(self, callback_id: str) -> "DialogBuilder":
+        """
+        Specify a callback ID for this dialog, which your application will then
+        receive upon dialog submission
+
+        Args:
+          callback_id: a string identifying this particular dialog
+        """
+        self._callback_id = callback_id
+        return self
+
+    def submit_label(self, label: str) -> "DialogBuilder":
+        """
+        The label to use on the 'Submit' button on the dialog. Defaults to 'Submit'
+        if not specified.
+
+        Args:
+            label: must not exceed 24 characters, and must be a single word (no
+                spaces)
+        """
+        self._submit_label = label
+        return self
+
+    def notify_on_cancel(self, notify: bool) -> "DialogBuilder":
+        """
+        Whether this dialog should send a request to your application even if the
+        user cancels their interaction. Defaults to False.
+
+        Args:
+            notify: Set to True to indicate that your application should receive a
+                request even if the user cancels interaction with the dialog.
+        """
+        self._notify_on_cancel = notify
+        return self
+
+    def text_field(
+        self,
+        *,
+        name: str,
+        label: str,
+        optional: bool = False,
+        placeholder: Optional[str] = None,
+        hint: Optional[str] = None,
+        value: Optional[str] = None,
+        min_length: int = 0,
+        max_length: int = 150,
+        subtype: Optional[str] = None,
+    ) -> "DialogBuilder":
+        """
+        Text elements are single-line plain text fields.
+
+        https://docs.slack.dev/legacy/legacy-dialogs/#attributes_text_elements
+
+        Args:
+            name: Name of form element. Required. No more than 300 characters.
+            label: Label displayed to user. Required. 48 character maximum.
+            optional: Provide true when the form element is not required. By
+                default, form elements are required.
+            placeholder: A string displayed as needed to help guide users in
+                completing the element. 150 character maximum.
+            hint: Helpful text provided to assist users in answering a question.
+                Up to 150 characters.
+            value: A default value for this field. Up to 150 characters.
+            min_length: Minimum input length allowed for element. Up to 150
+                characters. Defaults to 0.
+            max_length: Maximum input length allowed for element. Up to 150
+                characters. Defaults to 150.
+            subtype: A subtype for this text input. Accepts email, number, tel,
+                    or url. In some form factors, optimized input is provided for this
+                    subtype.
+        """
+        self._elements.append(
+            DialogTextField(
+                name=name,
+                label=label,
+                optional=optional,
+                placeholder=placeholder,
+                hint=hint,
+                value=value,
+                min_length=min_length,
+                max_length=max_length,
+                subtype=subtype,
+            )
+        )
+        return self
+
+    def text_area(
+        self,
+        *,
+        name: str,
+        label: str,
+        optional: bool = False,
+        placeholder: Optional[str] = None,
+        hint: Optional[str] = None,
+        value: Optional[str] = None,
+        min_length: int = 0,
+        max_length: int = 3000,
+        subtype: Optional[str] = None,
+    ) -> "DialogBuilder":
+        """
+        A textarea is a multi-line plain text editing control. You've likely
+        encountered these on the world wide web. Use this element if you want a
+        relatively long answer from users. The element UI provides a remaining
+        character count to the max_length you have set or the default,
+        3000.
+
+        https://docs.slack.dev/legacy/legacy-dialogs/#attributes_textarea_elements
+
+        Args:
+            name: Name of form element. Required. No more than 300 characters.
+            label: Label displayed to user. Required. 48 character maximum.
+            optional: Provide true when the form element is not required. By
+                default, form elements are required.
+            placeholder: A string displayed as needed to help guide users in
+                completing the element. 150 character maximum.
+            hint: Helpful text provided to assist users in answering a question.
+                Up to 150 characters.
+            value: A default value for this field. Up to 3000 characters.
+            min_length: Minimum input length allowed for element. 1-3000
+                characters. Defaults to 0.
+            max_length: Maximum input length allowed for element. 0-3000
+                characters. Defaults to 3000.
+            subtype: A subtype for this text input. Accepts email, number, tel,
+                or url. In some form factors, optimized input is provided for this
+                subtype.
+        """
+        self._elements.append(
+            DialogTextArea(
+                name=name,
+                label=label,
+                optional=optional,
+                placeholder=placeholder,
+                hint=hint,
+                value=value,
+                min_length=min_length,
+                max_length=max_length,
+                subtype=subtype,
+            )
+        )
+        return self
+
+    def static_selector(
+        self,
+        *,
+        name: str,
+        label: str,
+        options: Union[Sequence[Option], Sequence[OptionGroup]],
+        optional: bool = False,
+        value: Optional[str] = None,
+        placeholder: Optional[str] = None,
+    ) -> "DialogBuilder":
+        """
+        Use the select element for multiple choice selections allowing users to pick
+        a single item from a list. True to web roots, this selection is displayed as
+        a dropdown menu.
+
+        A select element may contain up to 100 selections, provided as a list of
+        Option or OptionGroup objects
+
+        https://docs.slack.dev/legacy/legacy-dialogs/#attributes_select_elements
+
+        Args:
+            name: Name of form element. Required. No more than 300 characters.
+            label: Label displayed to user. Required. No more than 48 characters.
+            options: A list of up to 100 Option or OptionGroup objects. Object
+                types cannot be mixed.
+            optional: Provide true when the form element is not required. By
+                default, form elements are required.
+            value: Provide a default selected value.
+            placeholder: A string displayed as needed to help guide users in
+                completing the element. 150 character maximum.
+        """
+        self._elements.append(
+            DialogStaticSelector(
+                name=name,
+                label=label,
+                options=options,
+                optional=optional,
+                value=value,
+                placeholder=placeholder,
+            )
+        )
+        return self
+
+    def external_selector(
+        self,
+        *,
+        name: str,
+        label: str,
+        optional: bool = False,
+        value: Optional[Option] = None,
+        placeholder: Optional[str] = None,
+        min_query_length: Optional[int] = None,
+    ) -> "DialogBuilder":
+        """
+        Use the select element for multiple choice selections allowing users to pick
+        a single item from a list. True to web roots, this selection is displayed as
+        a dropdown menu.
+
+        A list of options can be loaded from an external URL and used in your dialog
+        menus.
+
+        https://docs.slack.dev/legacy/legacy-dialogs/#dynamic_select_elements_external
+
+        Args:
+            name: Name of form element. Required. No more than 300 characters.
+            label: Label displayed to user. Required. No more than 48 characters.
+            min_query_length: Specify the number of characters that must be
+                typed by a user into a dynamic select menu before dispatching to your
+                application.
+            optional: Provide true when the form element is not required. By
+                default, form elements are required.
+            value: Provide a default selected value. This should be a single
+                Option or OptionGroup that exactly matches one that will be returned
+                from your external endpoint.
+            placeholder: A string displayed as needed to help guide users in
+                completing the element. 150 character maximum.
+        """
+        self._elements.append(
+            DialogExternalSelector(
+                name=name,
+                label=label,
+                optional=optional,
+                value=value,
+                placeholder=placeholder,
+                min_query_length=min_query_length,
+            )
+        )
+        return self
+
+    def user_selector(
+        self,
+        *,
+        name: str,
+        label: str,
+        optional: bool = False,
+        value: Optional[str] = None,
+        placeholder: Optional[str] = None,
+    ) -> "DialogBuilder":
+        """
+        Now you can easily populate a select menu with a list of users. For example,
+        when you are creating a bug tracking app, you want to include a field for an
+        assignee. Slack pre-populates the user list in client-side, so your app
+        doesn't need access to a related OAuth scope.
+
+        https://docs.slack.dev/legacy/legacy-dialogs/#dynamic_select_elements_users
+
+        Args:
+            name: Name of form element. Required. No more than 300 characters.
+            label: Label displayed to user. Required. No more than 48 characters.
+            optional: Provide true when the form element is not required. By
+                default, form elements are required.
+            value: Provide a default selected value.
+            placeholder: A string displayed as needed to help guide users in
+                completing the element. 150 character maximum.
+        """
+        self._elements.append(
+            DialogUserSelector(
+                name=name,
+                label=label,
+                optional=optional,
+                value=value,
+                placeholder=placeholder,
+            )
+        )
+        return self
+
+    def channel_selector(
+        self,
+        *,
+        name: str,
+        label: str,
+        optional: bool = False,
+        value: Optional[str] = None,
+        placeholder: Optional[str] = None,
+    ) -> "DialogBuilder":
+        """
+        You can also provide a select menu with a list of channels. Specify your
+        data_source as channels to limit only to public channels
+
+        https://docs.slack.dev/legacy/legacy-dialogs/#dynamic_select_elements_channels_conversations
+
+        Args:
+            name: Name of form element. Required. No more than 300 characters.
+            label: Label displayed to user. Required. No more than 48 characters.
+            optional: Provide true when the form element is not required. By
+                default, form elements are required.
+            value: Provide a default selected value.
+            placeholder: A string displayed as needed to help guide users in
+                completing the element. 150 character maximum.
+        """
+        self._elements.append(
+            DialogChannelSelector(
+                name=name,
+                label=label,
+                optional=optional,
+                value=value,
+                placeholder=placeholder,
+            )
+        )
+        return self
+
+    def conversation_selector(
+        self,
+        *,
+        name: str,
+        label: str,
+        optional: bool = False,
+        value: Optional[str] = None,
+        placeholder: Optional[str] = None,
+    ) -> "DialogBuilder":
+        """
+        You can also provide a select menu with a list of conversations - including
+        private channels, direct messages, MPIMs, and whatever else we consider a
+        conversation-like thing.
+
+        https://docs.slack.dev/legacy/legacy-dialogs/#dynamic_select_elements_channels_conversations
+
+        Args:
+            name: Name of form element. Required. No more than 300 characters.
+            label: Label displayed to user. Required. No more than 48 characters.
+            optional: Provide true when the form element is not required. By
+                default, form elements are required.
+            value: Provide a default selected value.
+            placeholder: A string displayed as needed to help guide users in
+                completing the element. 150 character maximum.
+        """
+        self._elements.append(
+            DialogConversationSelector(
+                name=name,
+                label=label,
+                optional=optional,
+                value=value,
+                placeholder=placeholder,
+            )
+        )
+        return self
+
+    @JsonValidator("title attribute is required")
+    def title_present(self) -> bool:
+        return self._title is not None
+
+    @JsonValidator(f"title attribute cannot exceed {title_max_length} characters")
+    def title_length(self) -> bool:
+        return self._title is not None and len(self._title) <= self.title_max_length
+
+    @JsonValidator("callback_id attribute is required")
+    def callback_id_present(self) -> bool:
+        return self._callback_id is not None
+
+    @JsonValidator(f"dialogs must contain between 1 and {elements_max_length} elements")
+    def elements_length(self) -> bool:
+        return 0 < len(self._elements) <= self.elements_max_length
+
+    @JsonValidator(f"submit_label cannot exceed {submit_label_max_length} characters")
+    def submit_label_length(self) -> bool:
+        return self._submit_label is None or len(self._submit_label) <= self.submit_label_max_length
+
+    @JsonValidator("submit_label can only be one word")
+    def submit_label_valid(self) -> bool:
+        return self._submit_label is None or " " not in self._submit_label
+
+    @JsonValidator(f"state cannot exceed {state_max_length} characters")
+    def state_length(self) -> bool:
+        return not self._state or len(self._state) <= self.state_max_length
+
+    def to_dict(self) -> dict:
+        self.validate_json()
+        json = {
+            "title": self._title,
+            "callback_id": self._callback_id,
+            "elements": extract_json(self._elements),
+            "notify_on_cancel": self._notify_on_cancel,
+        }
+        if self._submit_label is not None:
+            json["submit_label"] = self._submit_label
+        if self._state is not None:
+            json["state"] = self._state
+        return json
+
+

The base class for JSON serializable class objects

+

Create a DialogBuilder to more easily construct the JSON required to submit a +dialog to Slack

+

Ancestors

+ +

Class variables

+
+
var attributes : Set[str]
+
+

The type of the None singleton.

+
+
var elements_max_length
+
+

The type of the None singleton.

+
+
var state_max_length
+
+

The type of the None singleton.

+
+
var submit_label_max_length
+
+

The type of the None singleton.

+
+
var title_max_length
+
+

The type of the None singleton.

+
+
+

Methods

+
+
+def callback_id(self, callback_id: str) ‑> DialogBuilder +
+
+
+ +Expand source code + +
def callback_id(self, callback_id: str) -> "DialogBuilder":
+    """
+    Specify a callback ID for this dialog, which your application will then
+    receive upon dialog submission
+
+    Args:
+      callback_id: a string identifying this particular dialog
+    """
+    self._callback_id = callback_id
+    return self
+
+

Specify a callback ID for this dialog, which your application will then +receive upon dialog submission

+

Args

+
+
callback_id
+
a string identifying this particular dialog
+
+
+
+def callback_id_present(self) ‑> bool +
+
+
+ +Expand source code + +
@JsonValidator("callback_id attribute is required")
+def callback_id_present(self) -> bool:
+    return self._callback_id is not None
+
+
+
+
+def channel_selector(self,
*,
name: str,
label: str,
optional: bool = False,
value: str | None = None,
placeholder: str | None = None) ‑> DialogBuilder
+
+
+
+ +Expand source code + +
def channel_selector(
+    self,
+    *,
+    name: str,
+    label: str,
+    optional: bool = False,
+    value: Optional[str] = None,
+    placeholder: Optional[str] = None,
+) -> "DialogBuilder":
+    """
+    You can also provide a select menu with a list of channels. Specify your
+    data_source as channels to limit only to public channels
+
+    https://docs.slack.dev/legacy/legacy-dialogs/#dynamic_select_elements_channels_conversations
+
+    Args:
+        name: Name of form element. Required. No more than 300 characters.
+        label: Label displayed to user. Required. No more than 48 characters.
+        optional: Provide true when the form element is not required. By
+            default, form elements are required.
+        value: Provide a default selected value.
+        placeholder: A string displayed as needed to help guide users in
+            completing the element. 150 character maximum.
+    """
+    self._elements.append(
+        DialogChannelSelector(
+            name=name,
+            label=label,
+            optional=optional,
+            value=value,
+            placeholder=placeholder,
+        )
+    )
+    return self
+
+

You can also provide a select menu with a list of channels. Specify your +data_source as channels to limit only to public channels

+

https://docs.slack.dev/legacy/legacy-dialogs/#dynamic_select_elements_channels_conversations

+

Args

+
+
name
+
Name of form element. Required. No more than 300 characters.
+
label
+
Label displayed to user. Required. No more than 48 characters.
+
optional
+
Provide true when the form element is not required. By +default, form elements are required.
+
value
+
Provide a default selected value.
+
placeholder
+
A string displayed as needed to help guide users in +completing the element. 150 character maximum.
+
+
+
+def conversation_selector(self,
*,
name: str,
label: str,
optional: bool = False,
value: str | None = None,
placeholder: str | None = None) ‑> DialogBuilder
+
+
+
+ +Expand source code + +
def conversation_selector(
+    self,
+    *,
+    name: str,
+    label: str,
+    optional: bool = False,
+    value: Optional[str] = None,
+    placeholder: Optional[str] = None,
+) -> "DialogBuilder":
+    """
+    You can also provide a select menu with a list of conversations - including
+    private channels, direct messages, MPIMs, and whatever else we consider a
+    conversation-like thing.
+
+    https://docs.slack.dev/legacy/legacy-dialogs/#dynamic_select_elements_channels_conversations
+
+    Args:
+        name: Name of form element. Required. No more than 300 characters.
+        label: Label displayed to user. Required. No more than 48 characters.
+        optional: Provide true when the form element is not required. By
+            default, form elements are required.
+        value: Provide a default selected value.
+        placeholder: A string displayed as needed to help guide users in
+            completing the element. 150 character maximum.
+    """
+    self._elements.append(
+        DialogConversationSelector(
+            name=name,
+            label=label,
+            optional=optional,
+            value=value,
+            placeholder=placeholder,
+        )
+    )
+    return self
+
+

You can also provide a select menu with a list of conversations - including +private channels, direct messages, MPIMs, and whatever else we consider a +conversation-like thing.

+

https://docs.slack.dev/legacy/legacy-dialogs/#dynamic_select_elements_channels_conversations

+

Args

+
+
name
+
Name of form element. Required. No more than 300 characters.
+
label
+
Label displayed to user. Required. No more than 48 characters.
+
optional
+
Provide true when the form element is not required. By +default, form elements are required.
+
value
+
Provide a default selected value.
+
placeholder
+
A string displayed as needed to help guide users in +completing the element. 150 character maximum.
+
+
+
+def elements_length(self) ‑> bool +
+
+
+ +Expand source code + +
@JsonValidator(f"dialogs must contain between 1 and {elements_max_length} elements")
+def elements_length(self) -> bool:
+    return 0 < len(self._elements) <= self.elements_max_length
+
+
+
+
+def external_selector(self,
*,
name: str,
label: str,
optional: bool = False,
value: Option | None = None,
placeholder: str | None = None,
min_query_length: int | None = None) ‑> DialogBuilder
+
+
+
+ +Expand source code + +
def external_selector(
+    self,
+    *,
+    name: str,
+    label: str,
+    optional: bool = False,
+    value: Optional[Option] = None,
+    placeholder: Optional[str] = None,
+    min_query_length: Optional[int] = None,
+) -> "DialogBuilder":
+    """
+    Use the select element for multiple choice selections allowing users to pick
+    a single item from a list. True to web roots, this selection is displayed as
+    a dropdown menu.
+
+    A list of options can be loaded from an external URL and used in your dialog
+    menus.
+
+    https://docs.slack.dev/legacy/legacy-dialogs/#dynamic_select_elements_external
+
+    Args:
+        name: Name of form element. Required. No more than 300 characters.
+        label: Label displayed to user. Required. No more than 48 characters.
+        min_query_length: Specify the number of characters that must be
+            typed by a user into a dynamic select menu before dispatching to your
+            application.
+        optional: Provide true when the form element is not required. By
+            default, form elements are required.
+        value: Provide a default selected value. This should be a single
+            Option or OptionGroup that exactly matches one that will be returned
+            from your external endpoint.
+        placeholder: A string displayed as needed to help guide users in
+            completing the element. 150 character maximum.
+    """
+    self._elements.append(
+        DialogExternalSelector(
+            name=name,
+            label=label,
+            optional=optional,
+            value=value,
+            placeholder=placeholder,
+            min_query_length=min_query_length,
+        )
+    )
+    return self
+
+

Use the select element for multiple choice selections allowing users to pick +a single item from a list. True to web roots, this selection is displayed as +a dropdown menu.

+

A list of options can be loaded from an external URL and used in your dialog +menus.

+

https://docs.slack.dev/legacy/legacy-dialogs/#dynamic_select_elements_external

+

Args

+
+
name
+
Name of form element. Required. No more than 300 characters.
+
label
+
Label displayed to user. Required. No more than 48 characters.
+
min_query_length
+
Specify the number of characters that must be +typed by a user into a dynamic select menu before dispatching to your +application.
+
optional
+
Provide true when the form element is not required. By +default, form elements are required.
+
value
+
Provide a default selected value. This should be a single +Option or OptionGroup that exactly matches one that will be returned +from your external endpoint.
+
placeholder
+
A string displayed as needed to help guide users in +completing the element. 150 character maximum.
+
+
+
+def notify_on_cancel(self, notify: bool) ‑> DialogBuilder +
+
+
+ +Expand source code + +
def notify_on_cancel(self, notify: bool) -> "DialogBuilder":
+    """
+    Whether this dialog should send a request to your application even if the
+    user cancels their interaction. Defaults to False.
+
+    Args:
+        notify: Set to True to indicate that your application should receive a
+            request even if the user cancels interaction with the dialog.
+    """
+    self._notify_on_cancel = notify
+    return self
+
+

Whether this dialog should send a request to your application even if the +user cancels their interaction. Defaults to False.

+

Args

+
+
notify
+
Set to True to indicate that your application should receive a +request even if the user cancels interaction with the dialog.
+
+
+
+def state(self, state: dict | str) ‑> DialogBuilder +
+
+
+ +Expand source code + +
def state(self, state: Union[dict, str]) -> "DialogBuilder":
+    """
+    Pass state into this dialog - dictionaries will be automatically formatted to
+    JSON
+
+    Args:
+        state: Extra state information that you need to pass from this dialog
+            back to your application on submission
+    """
+    if isinstance(state, dict):
+        self._state = dumps(state)
+    else:
+        self._state = state
+    return self
+
+

Pass state into this dialog - dictionaries will be automatically formatted to +JSON

+

Args

+
+
state
+
Extra state information that you need to pass from this dialog +back to your application on submission
+
+
+
+def state_length(self) ‑> bool +
+
+
+ +Expand source code + +
@JsonValidator(f"state cannot exceed {state_max_length} characters")
+def state_length(self) -> bool:
+    return not self._state or len(self._state) <= self.state_max_length
+
+
+
+
+def static_selector(self,
*,
name: str,
label: str,
options: Sequence[Option] | Sequence[OptionGroup],
optional: bool = False,
value: str | None = None,
placeholder: str | None = None) ‑> DialogBuilder
+
+
+
+ +Expand source code + +
def static_selector(
+    self,
+    *,
+    name: str,
+    label: str,
+    options: Union[Sequence[Option], Sequence[OptionGroup]],
+    optional: bool = False,
+    value: Optional[str] = None,
+    placeholder: Optional[str] = None,
+) -> "DialogBuilder":
+    """
+    Use the select element for multiple choice selections allowing users to pick
+    a single item from a list. True to web roots, this selection is displayed as
+    a dropdown menu.
+
+    A select element may contain up to 100 selections, provided as a list of
+    Option or OptionGroup objects
+
+    https://docs.slack.dev/legacy/legacy-dialogs/#attributes_select_elements
+
+    Args:
+        name: Name of form element. Required. No more than 300 characters.
+        label: Label displayed to user. Required. No more than 48 characters.
+        options: A list of up to 100 Option or OptionGroup objects. Object
+            types cannot be mixed.
+        optional: Provide true when the form element is not required. By
+            default, form elements are required.
+        value: Provide a default selected value.
+        placeholder: A string displayed as needed to help guide users in
+            completing the element. 150 character maximum.
+    """
+    self._elements.append(
+        DialogStaticSelector(
+            name=name,
+            label=label,
+            options=options,
+            optional=optional,
+            value=value,
+            placeholder=placeholder,
+        )
+    )
+    return self
+
+

Use the select element for multiple choice selections allowing users to pick +a single item from a list. True to web roots, this selection is displayed as +a dropdown menu.

+

A select element may contain up to 100 selections, provided as a list of +Option or OptionGroup objects

+

https://docs.slack.dev/legacy/legacy-dialogs/#attributes_select_elements

+

Args

+
+
name
+
Name of form element. Required. No more than 300 characters.
+
label
+
Label displayed to user. Required. No more than 48 characters.
+
options
+
A list of up to 100 Option or OptionGroup objects. Object +types cannot be mixed.
+
optional
+
Provide true when the form element is not required. By +default, form elements are required.
+
value
+
Provide a default selected value.
+
placeholder
+
A string displayed as needed to help guide users in +completing the element. 150 character maximum.
+
+
+
+def submit_label(self, label: str) ‑> DialogBuilder +
+
+
+ +Expand source code + +
def submit_label(self, label: str) -> "DialogBuilder":
+    """
+    The label to use on the 'Submit' button on the dialog. Defaults to 'Submit'
+    if not specified.
+
+    Args:
+        label: must not exceed 24 characters, and must be a single word (no
+            spaces)
+    """
+    self._submit_label = label
+    return self
+
+

The label to use on the 'Submit' button on the dialog. Defaults to 'Submit' +if not specified.

+

Args

+
+
label
+
must not exceed 24 characters, and must be a single word (no +spaces)
+
+
+
+def submit_label_length(self) ‑> bool +
+
+
+ +Expand source code + +
@JsonValidator(f"submit_label cannot exceed {submit_label_max_length} characters")
+def submit_label_length(self) -> bool:
+    return self._submit_label is None or len(self._submit_label) <= self.submit_label_max_length
+
+
+
+
+def submit_label_valid(self) ‑> bool +
+
+
+ +Expand source code + +
@JsonValidator("submit_label can only be one word")
+def submit_label_valid(self) -> bool:
+    return self._submit_label is None or " " not in self._submit_label
+
+
+
+
+def text_area(self,
*,
name: str,
label: str,
optional: bool = False,
placeholder: str | None = None,
hint: str | None = None,
value: str | None = None,
min_length: int = 0,
max_length: int = 3000,
subtype: str | None = None) ‑> DialogBuilder
+
+
+
+ +Expand source code + +
def text_area(
+    self,
+    *,
+    name: str,
+    label: str,
+    optional: bool = False,
+    placeholder: Optional[str] = None,
+    hint: Optional[str] = None,
+    value: Optional[str] = None,
+    min_length: int = 0,
+    max_length: int = 3000,
+    subtype: Optional[str] = None,
+) -> "DialogBuilder":
+    """
+    A textarea is a multi-line plain text editing control. You've likely
+    encountered these on the world wide web. Use this element if you want a
+    relatively long answer from users. The element UI provides a remaining
+    character count to the max_length you have set or the default,
+    3000.
+
+    https://docs.slack.dev/legacy/legacy-dialogs/#attributes_textarea_elements
+
+    Args:
+        name: Name of form element. Required. No more than 300 characters.
+        label: Label displayed to user. Required. 48 character maximum.
+        optional: Provide true when the form element is not required. By
+            default, form elements are required.
+        placeholder: A string displayed as needed to help guide users in
+            completing the element. 150 character maximum.
+        hint: Helpful text provided to assist users in answering a question.
+            Up to 150 characters.
+        value: A default value for this field. Up to 3000 characters.
+        min_length: Minimum input length allowed for element. 1-3000
+            characters. Defaults to 0.
+        max_length: Maximum input length allowed for element. 0-3000
+            characters. Defaults to 3000.
+        subtype: A subtype for this text input. Accepts email, number, tel,
+            or url. In some form factors, optimized input is provided for this
+            subtype.
+    """
+    self._elements.append(
+        DialogTextArea(
+            name=name,
+            label=label,
+            optional=optional,
+            placeholder=placeholder,
+            hint=hint,
+            value=value,
+            min_length=min_length,
+            max_length=max_length,
+            subtype=subtype,
+        )
+    )
+    return self
+
+

A textarea is a multi-line plain text editing control. You've likely +encountered these on the world wide web. Use this element if you want a +relatively long answer from users. The element UI provides a remaining +character count to the max_length you have set or the default, +3000.

+

https://docs.slack.dev/legacy/legacy-dialogs/#attributes_textarea_elements

+

Args

+
+
name
+
Name of form element. Required. No more than 300 characters.
+
label
+
Label displayed to user. Required. 48 character maximum.
+
optional
+
Provide true when the form element is not required. By +default, form elements are required.
+
placeholder
+
A string displayed as needed to help guide users in +completing the element. 150 character maximum.
+
hint
+
Helpful text provided to assist users in answering a question. +Up to 150 characters.
+
value
+
A default value for this field. Up to 3000 characters.
+
min_length
+
Minimum input length allowed for element. 1-3000 +characters. Defaults to 0.
+
max_length
+
Maximum input length allowed for element. 0-3000 +characters. Defaults to 3000.
+
subtype
+
A subtype for this text input. Accepts email, number, tel, +or url. In some form factors, optimized input is provided for this +subtype.
+
+
+
+def text_field(self,
*,
name: str,
label: str,
optional: bool = False,
placeholder: str | None = None,
hint: str | None = None,
value: str | None = None,
min_length: int = 0,
max_length: int = 150,
subtype: str | None = None) ‑> DialogBuilder
+
+
+
+ +Expand source code + +
def text_field(
+    self,
+    *,
+    name: str,
+    label: str,
+    optional: bool = False,
+    placeholder: Optional[str] = None,
+    hint: Optional[str] = None,
+    value: Optional[str] = None,
+    min_length: int = 0,
+    max_length: int = 150,
+    subtype: Optional[str] = None,
+) -> "DialogBuilder":
+    """
+    Text elements are single-line plain text fields.
+
+    https://docs.slack.dev/legacy/legacy-dialogs/#attributes_text_elements
+
+    Args:
+        name: Name of form element. Required. No more than 300 characters.
+        label: Label displayed to user. Required. 48 character maximum.
+        optional: Provide true when the form element is not required. By
+            default, form elements are required.
+        placeholder: A string displayed as needed to help guide users in
+            completing the element. 150 character maximum.
+        hint: Helpful text provided to assist users in answering a question.
+            Up to 150 characters.
+        value: A default value for this field. Up to 150 characters.
+        min_length: Minimum input length allowed for element. Up to 150
+            characters. Defaults to 0.
+        max_length: Maximum input length allowed for element. Up to 150
+            characters. Defaults to 150.
+        subtype: A subtype for this text input. Accepts email, number, tel,
+                or url. In some form factors, optimized input is provided for this
+                subtype.
+    """
+    self._elements.append(
+        DialogTextField(
+            name=name,
+            label=label,
+            optional=optional,
+            placeholder=placeholder,
+            hint=hint,
+            value=value,
+            min_length=min_length,
+            max_length=max_length,
+            subtype=subtype,
+        )
+    )
+    return self
+
+

Text elements are single-line plain text fields.

+

https://docs.slack.dev/legacy/legacy-dialogs/#attributes_text_elements

+

Args

+
+
name
+
Name of form element. Required. No more than 300 characters.
+
label
+
Label displayed to user. Required. 48 character maximum.
+
optional
+
Provide true when the form element is not required. By +default, form elements are required.
+
placeholder
+
A string displayed as needed to help guide users in +completing the element. 150 character maximum.
+
hint
+
Helpful text provided to assist users in answering a question. +Up to 150 characters.
+
value
+
A default value for this field. Up to 150 characters.
+
min_length
+
Minimum input length allowed for element. Up to 150 +characters. Defaults to 0.
+
max_length
+
Maximum input length allowed for element. Up to 150 +characters. Defaults to 150.
+
subtype
+
A subtype for this text input. Accepts email, number, tel, +or url. In some form factors, optimized input is provided for this +subtype.
+
+
+
+def title(self, title: str) ‑> DialogBuilder +
+
+
+ +Expand source code + +
def title(self, title: str) -> "DialogBuilder":
+    """
+    Specify a title for this dialog
+
+    Args:
+      title: must not exceed 24 characters
+    """
+    self._title = title
+    return self
+
+

Specify a title for this dialog

+

Args

+
+
title
+
must not exceed 24 characters
+
+
+
+def title_length(self) ‑> bool +
+
+
+ +Expand source code + +
@JsonValidator(f"title attribute cannot exceed {title_max_length} characters")
+def title_length(self) -> bool:
+    return self._title is not None and len(self._title) <= self.title_max_length
+
+
+
+
+def title_present(self) ‑> bool +
+
+
+ +Expand source code + +
@JsonValidator("title attribute is required")
+def title_present(self) -> bool:
+    return self._title is not None
+
+
+
+
+def user_selector(self,
*,
name: str,
label: str,
optional: bool = False,
value: str | None = None,
placeholder: str | None = None) ‑> DialogBuilder
+
+
+
+ +Expand source code + +
def user_selector(
+    self,
+    *,
+    name: str,
+    label: str,
+    optional: bool = False,
+    value: Optional[str] = None,
+    placeholder: Optional[str] = None,
+) -> "DialogBuilder":
+    """
+    Now you can easily populate a select menu with a list of users. For example,
+    when you are creating a bug tracking app, you want to include a field for an
+    assignee. Slack pre-populates the user list in client-side, so your app
+    doesn't need access to a related OAuth scope.
+
+    https://docs.slack.dev/legacy/legacy-dialogs/#dynamic_select_elements_users
+
+    Args:
+        name: Name of form element. Required. No more than 300 characters.
+        label: Label displayed to user. Required. No more than 48 characters.
+        optional: Provide true when the form element is not required. By
+            default, form elements are required.
+        value: Provide a default selected value.
+        placeholder: A string displayed as needed to help guide users in
+            completing the element. 150 character maximum.
+    """
+    self._elements.append(
+        DialogUserSelector(
+            name=name,
+            label=label,
+            optional=optional,
+            value=value,
+            placeholder=placeholder,
+        )
+    )
+    return self
+
+

Now you can easily populate a select menu with a list of users. For example, +when you are creating a bug tracking app, you want to include a field for an +assignee. Slack pre-populates the user list in client-side, so your app +doesn't need access to a related OAuth scope.

+

https://docs.slack.dev/legacy/legacy-dialogs/#dynamic_select_elements_users

+

Args

+
+
name
+
Name of form element. Required. No more than 300 characters.
+
label
+
Label displayed to user. Required. No more than 48 characters.
+
optional
+
Provide true when the form element is not required. By +default, form elements are required.
+
value
+
Provide a default selected value.
+
placeholder
+
A string displayed as needed to help guide users in +completing the element. 150 character maximum.
+
+
+
+

Inherited members

+ +
+
+class DialogChannelSelector +(*,
name: str,
label: str,
optional: bool = False,
value: str | None = None,
placeholder: str | None = None)
+
+
+
+ +Expand source code + +
class DialogChannelSelector(AbstractDialogSelector):
+    data_source = "channels"
+
+    def __init__(
+        self,
+        *,
+        name: str,
+        label: str,
+        optional: bool = False,
+        value: Optional[str] = None,
+        placeholder: Optional[str] = None,
+    ):
+        """
+        You can also provide a select menu with a list of channels. Specify your
+        data_source as channels to limit only to public channels
+
+        https://docs.slack.dev/legacy/legacy-dialogs/#dynamic_select_elements_channels_conversations
+
+        Args:
+            name: Name of form element. Required. No more than 300 characters.
+            label: Label displayed to user. Required. No more than 48 characters.
+            optional: Provide true when the form element is not required. By
+                default, form elements are required.
+            value: Provide a default selected value.
+            placeholder: A string displayed as needed to help guide users in
+                completing the element. 150 character maximum.
+        """
+        super().__init__(
+            name=name,
+            label=label,
+            optional=optional,
+            value=value,
+            placeholder=placeholder,
+        )
+
+

The base class for JSON serializable class objects

+

You can also provide a select menu with a list of channels. Specify your +data_source as channels to limit only to public channels

+

https://docs.slack.dev/legacy/legacy-dialogs/#dynamic_select_elements_channels_conversations

+

Args

+
+
name
+
Name of form element. Required. No more than 300 characters.
+
label
+
Label displayed to user. Required. No more than 48 characters.
+
optional
+
Provide true when the form element is not required. By +default, form elements are required.
+
value
+
Provide a default selected value.
+
placeholder
+
A string displayed as needed to help guide users in +completing the element. 150 character maximum.
+
+

Ancestors

+ +

Class variables

+
+
var data_source
+
+

The type of the None singleton.

+
+
+

Inherited members

+ +
+
+class DialogConversationSelector +(*,
name: str,
label: str,
optional: bool = False,
value: str | None = None,
placeholder: str | None = None)
+
+
+
+ +Expand source code + +
class DialogConversationSelector(AbstractDialogSelector):
+    data_source = "conversations"
+
+    def __init__(
+        self,
+        *,
+        name: str,
+        label: str,
+        optional: bool = False,
+        value: Optional[str] = None,
+        placeholder: Optional[str] = None,
+    ):
+        """
+        You can also provide a select menu with a list of conversations - including
+        private channels, direct messages, MPIMs, and whatever else we consider a
+        conversation-like thing.
+
+        https://docs.slack.dev/legacy/legacy-dialogs/#dynamic_select_elements_channels_conversations
+
+        Args:
+            name: Name of form element. Required. No more than 300 characters.
+            label: Label displayed to user. Required. No more than 48 characters.
+            optional: Provide true when the form element is not required. By
+                default, form elements are required.
+            value: Provide a default selected value.
+            placeholder: A string displayed as needed to help guide users in
+                completing the element. 150 character maximum.
+        """
+        super().__init__(
+            name=name,
+            label=label,
+            optional=optional,
+            value=value,
+            placeholder=placeholder,
+        )
+
+

The base class for JSON serializable class objects

+

You can also provide a select menu with a list of conversations - including +private channels, direct messages, MPIMs, and whatever else we consider a +conversation-like thing.

+

https://docs.slack.dev/legacy/legacy-dialogs/#dynamic_select_elements_channels_conversations

+

Args

+
+
name
+
Name of form element. Required. No more than 300 characters.
+
label
+
Label displayed to user. Required. No more than 48 characters.
+
optional
+
Provide true when the form element is not required. By +default, form elements are required.
+
value
+
Provide a default selected value.
+
placeholder
+
A string displayed as needed to help guide users in +completing the element. 150 character maximum.
+
+

Ancestors

+ +

Class variables

+
+
var data_source
+
+

The type of the None singleton.

+
+
+

Inherited members

+ +
+
+class DialogExternalSelector +(*,
name: str,
label: str,
value: Option | None = None,
min_query_length: int | None = None,
optional: bool | None = False,
placeholder: str | None = None)
+
+
+
+ +Expand source code + +
class DialogExternalSelector(AbstractDialogSelector):
+    data_source = "external"
+
+    @property
+    def attributes(self) -> Set[str]:  # type: ignore[override]
+        return super().attributes.union({"min_query_length"})
+
+    def __init__(
+        self,
+        *,
+        name: str,
+        label: str,
+        value: Optional[Option] = None,
+        min_query_length: Optional[int] = None,
+        optional: Optional[bool] = False,
+        placeholder: Optional[str] = None,
+    ):
+        """
+        Use the select element for multiple choice selections allowing users to pick
+        a single item from a list. True to web roots, this selection is displayed as
+        a dropdown menu.
+
+        A list of options can be loaded from an external URL and used in your dialog
+        menus.
+
+        https://docs.slack.dev/legacy/legacy-dialogs/#dynamic_select_elements_external
+
+        Args:
+            name: Name of form element. Required. No more than 300 characters.
+            label: Label displayed to user. Required. No more than 48 characters.
+            min_query_length: Specify the number of characters that must be typed
+                by a user into a dynamic select menu before dispatching to the app.
+            optional: Provide true when the form element is not required. By
+                default, form elements are required.
+            value: Provide a default selected value. This should be a single
+                Option or OptionGroup that exactly matches one that will be returned
+                from your external endpoint.
+            placeholder: A string displayed as needed to help guide users in
+                completing the element. 150 character maximum.
+        """
+        super().__init__(
+            name=name,
+            label=label,
+            value=value,
+            optional=optional,  # type: ignore[arg-type]
+            placeholder=placeholder,
+        )
+        self.min_query_length = min_query_length
+
+

The base class for JSON serializable class objects

+

Use the select element for multiple choice selections allowing users to pick +a single item from a list. True to web roots, this selection is displayed as +a dropdown menu.

+

A list of options can be loaded from an external URL and used in your dialog +menus.

+

https://docs.slack.dev/legacy/legacy-dialogs/#dynamic_select_elements_external

+

Args

+
+
name
+
Name of form element. Required. No more than 300 characters.
+
label
+
Label displayed to user. Required. No more than 48 characters.
+
min_query_length
+
Specify the number of characters that must be typed +by a user into a dynamic select menu before dispatching to the app.
+
optional
+
Provide true when the form element is not required. By +default, form elements are required.
+
value
+
Provide a default selected value. This should be a single +Option or OptionGroup that exactly matches one that will be returned +from your external endpoint.
+
placeholder
+
A string displayed as needed to help guide users in +completing the element. 150 character maximum.
+
+

Ancestors

+ +

Class variables

+
+
var data_source
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
prop attributes : Set[str]
+
+
+ +Expand source code + +
@property
+def attributes(self) -> Set[str]:  # type: ignore[override]
+    return super().attributes.union({"min_query_length"})
+
+

Build an unordered collection of unique elements.

+
+
+

Inherited members

+ +
+
+class DialogStaticSelector +(*,
name: str,
label: str,
options: Sequence[Option] | Sequence[OptionGroup],
optional: bool = False,
value: Option | str | None = None,
placeholder: str | None = None)
+
+
+
+ +Expand source code + +
class DialogStaticSelector(AbstractDialogSelector):
+    """
+    Use the select element for multiple choice selections allowing users to pick a
+    single item from a list. True to web roots, this selection is displayed as a
+    dropdown menu.
+
+    https://docs.slack.dev/legacy/legacy-dialogs/#select_elements
+    """
+
+    data_source = "static"
+
+    options_max_length = 100
+
+    def __init__(
+        self,
+        *,
+        name: str,
+        label: str,
+        options: Union[Sequence[Option], Sequence[OptionGroup]],
+        optional: bool = False,
+        value: Optional[Union[Option, str]] = None,
+        placeholder: Optional[str] = None,
+    ):
+        """
+        Use the select element for multiple choice selections allowing users to pick
+        a single item from a list. True to web roots, this selection is displayed as
+        a dropdown menu.
+
+        A select element may contain up to 100 selections, provided as a list of
+        Option or OptionGroup objects
+
+        https://docs.slack.dev/legacy/legacy-dialogs/#attributes_select_elements
+
+        Args:
+            name: Name of form element. Required. No more than 300 characters.
+            label: Label displayed to user. Required. No more than 48 characters.
+            options: A list of up to 100 Option or OptionGroup objects. Object
+                types cannot be mixed.
+            optional: Provide true when the form element is not required. By
+                default, form elements are required.
+            value: Provide a default selected value.
+            placeholder: A string displayed as needed to help guide users in
+                completing the element. 150 character maximum.
+        """
+        super().__init__(
+            name=name,
+            label=label,
+            optional=optional,
+            value=value,
+            placeholder=placeholder,
+        )
+        self.options = options
+
+    @JsonValidator(f"options attribute cannot exceed {options_max_length} items")
+    def options_length(self) -> bool:
+        return len(self.options) < self.options_max_length
+
+    def to_dict(self) -> dict:
+        json = super().to_dict()
+        if isinstance(self.options[0], OptionGroup):
+            json["option_groups"] = extract_json(self.options, "dialog")
+        else:
+            json["options"] = extract_json(self.options, "dialog")
+        return json
+
+

Use the select element for multiple choice selections allowing users to pick a +single item from a list. True to web roots, this selection is displayed as a +dropdown menu.

+

https://docs.slack.dev/legacy/legacy-dialogs/#select_elements

+

Use the select element for multiple choice selections allowing users to pick +a single item from a list. True to web roots, this selection is displayed as +a dropdown menu.

+

A select element may contain up to 100 selections, provided as a list of +Option or OptionGroup objects

+

https://docs.slack.dev/legacy/legacy-dialogs/#attributes_select_elements

+

Args

+
+
name
+
Name of form element. Required. No more than 300 characters.
+
label
+
Label displayed to user. Required. No more than 48 characters.
+
options
+
A list of up to 100 Option or OptionGroup objects. Object +types cannot be mixed.
+
optional
+
Provide true when the form element is not required. By +default, form elements are required.
+
value
+
Provide a default selected value.
+
placeholder
+
A string displayed as needed to help guide users in +completing the element. 150 character maximum.
+
+

Ancestors

+ +

Class variables

+
+
var data_source
+
+

The type of the None singleton.

+
+
var options_max_length
+
+

The type of the None singleton.

+
+
+

Methods

+
+
+def options_length(self) ‑> bool +
+
+
+ +Expand source code + +
@JsonValidator(f"options attribute cannot exceed {options_max_length} items")
+def options_length(self) -> bool:
+    return len(self.options) < self.options_max_length
+
+
+
+
+

Inherited members

+ +
+
+class DialogTextArea +(*,
name: str,
label: str,
optional: bool = False,
placeholder: str | None = None,
hint: str | None = None,
value: str | None = None,
min_length: int = 0,
max_length: int | None = None,
subtype: str | None = None)
+
+
+
+ +Expand source code + +
class DialogTextArea(DialogTextComponent):
+    """
+    A textarea is a multi-line plain text editing control. You've likely encountered
+    these on the world wide web. Use this element if you want a relatively long
+    answer from users. The element UI provides a remaining character count to the
+    max_length you have set or the default, 3000.
+
+    https://docs.slack.dev/legacy/legacy-dialogs/#textarea_elements
+    """
+
+    type = "textarea"
+    max_value_length = 3000
+
+

A textarea is a multi-line plain text editing control. You've likely encountered +these on the world wide web. Use this element if you want a relatively long +answer from users. The element UI provides a remaining character count to the +max_length you have set or the default, 3000.

+

https://docs.slack.dev/legacy/legacy-dialogs/#textarea_elements

+

Ancestors

+ +

Class variables

+
+
var max_value_length
+
+

The type of the None singleton.

+
+
var type
+
+

The type of the None singleton.

+
+
+

Inherited members

+ +
+
+class DialogTextComponent +(*,
name: str,
label: str,
optional: bool = False,
placeholder: str | None = None,
hint: str | None = None,
value: str | None = None,
min_length: int = 0,
max_length: int | None = None,
subtype: str | None = None)
+
+
+
+ +Expand source code + +
class DialogTextComponent(JsonObject, metaclass=ABCMeta):
+    attributes = {
+        "hint",
+        "label",
+        "max_length",
+        "min_length",
+        "name",
+        "optional",
+        "placeholder",
+        "subtype",
+        "type",
+        "value",
+    }
+
+    name_max_length = 300
+    label_max_length = 48
+    placeholder_max_length = 150
+    hint_max_length = 150
+
+    @property
+    @abstractmethod
+    def type(self):
+        pass
+
+    @property
+    @abstractmethod
+    def max_value_length(self):
+        pass
+
+    def __init__(
+        self,
+        *,
+        name: str,
+        label: str,
+        optional: bool = False,
+        placeholder: Optional[str] = None,
+        hint: Optional[str] = None,
+        value: Optional[str] = None,
+        min_length: int = 0,
+        max_length: Optional[int] = None,
+        subtype: Optional[str] = None,
+    ):
+        self.name = name
+        self.label = label
+        self.optional = optional
+        self.placeholder = placeholder
+        self.hint = hint
+        self.value = value
+        self.min_length = min_length
+        self.max_length = max_length or self.max_value_length
+        self.subtype = subtype
+
+    @JsonValidator(f"name attribute cannot exceed {name_max_length} characters")
+    def name_length(self) -> bool:
+        return len(self.name) < self.name_max_length
+
+    @JsonValidator(f"label attribute cannot exceed {label_max_length} characters")
+    def label_length(self) -> bool:
+        return len(self.label) < self.label_max_length
+
+    @JsonValidator(f"placeholder attribute cannot exceed {placeholder_max_length} characters")
+    def placeholder_length(self) -> bool:
+        return self.placeholder is None or len(self.placeholder) < self.placeholder_max_length
+
+    @JsonValidator(f"hint attribute cannot exceed {hint_max_length} characters")
+    def hint_length(self) -> bool:
+        return self.hint is None or len(self.hint) < self.hint_max_length
+
+    @JsonValidator("value attribute exceeded bounds")
+    def value_length(self) -> bool:
+        return self.value is None or len(self.value) < self.max_value_length
+
+    @JsonValidator("min_length attribute must be greater than or equal to 0")
+    def min_length_above_zero(self) -> bool:
+        return self.min_length is None or self.min_length >= 0
+
+    @JsonValidator("min_length attribute exceed bounds")
+    def min_length_length(self) -> bool:
+        return self.min_length is None or self.min_length <= self.max_value_length
+
+    @JsonValidator("min_length attribute must be less than max value attribute")
+    def min_length_below_max_length(self) -> bool:
+        return self.min_length is None or self.min_length < self.max_length
+
+    @JsonValidator("max_length attribute must be greater than or equal to 0")
+    def max_length_above_zero(self) -> bool:
+        return self.max_length is None or self.max_length > 0
+
+    @JsonValidator("max_length attribute exceeded bounds")
+    def max_length_length(self) -> bool:
+        return self.max_length is None or self.max_length <= self.max_value_length
+
+    @EnumValidator("subtype", TextElementSubtypes)
+    def subtype_valid(self) -> bool:
+        return self.subtype is None or self.subtype in TextElementSubtypes
+
+

The base class for JSON serializable class objects

+

Ancestors

+ +

Subclasses

+ +

Class variables

+
+
var attributes
+
+

The type of the None singleton.

+
+
var hint_max_length
+
+

The type of the None singleton.

+
+
var label_max_length
+
+

The type of the None singleton.

+
+
var name_max_length
+
+

The type of the None singleton.

+
+
var placeholder_max_length
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
prop max_value_length
+
+
+ +Expand source code + +
@property
+@abstractmethod
+def max_value_length(self):
+    pass
+
+
+
+
prop type
+
+
+ +Expand source code + +
@property
+@abstractmethod
+def type(self):
+    pass
+
+
+
+
+

Methods

+
+
+def hint_length(self) ‑> bool +
+
+
+ +Expand source code + +
@JsonValidator(f"hint attribute cannot exceed {hint_max_length} characters")
+def hint_length(self) -> bool:
+    return self.hint is None or len(self.hint) < self.hint_max_length
+
+
+
+
+def label_length(self) ‑> bool +
+
+
+ +Expand source code + +
@JsonValidator(f"label attribute cannot exceed {label_max_length} characters")
+def label_length(self) -> bool:
+    return len(self.label) < self.label_max_length
+
+
+
+
+def max_length_above_zero(self) ‑> bool +
+
+
+ +Expand source code + +
@JsonValidator("max_length attribute must be greater than or equal to 0")
+def max_length_above_zero(self) -> bool:
+    return self.max_length is None or self.max_length > 0
+
+
+
+
+def max_length_length(self) ‑> bool +
+
+
+ +Expand source code + +
@JsonValidator("max_length attribute exceeded bounds")
+def max_length_length(self) -> bool:
+    return self.max_length is None or self.max_length <= self.max_value_length
+
+
+
+
+def min_length_above_zero(self) ‑> bool +
+
+
+ +Expand source code + +
@JsonValidator("min_length attribute must be greater than or equal to 0")
+def min_length_above_zero(self) -> bool:
+    return self.min_length is None or self.min_length >= 0
+
+
+
+
+def min_length_below_max_length(self) ‑> bool +
+
+
+ +Expand source code + +
@JsonValidator("min_length attribute must be less than max value attribute")
+def min_length_below_max_length(self) -> bool:
+    return self.min_length is None or self.min_length < self.max_length
+
+
+
+
+def min_length_length(self) ‑> bool +
+
+
+ +Expand source code + +
@JsonValidator("min_length attribute exceed bounds")
+def min_length_length(self) -> bool:
+    return self.min_length is None or self.min_length <= self.max_value_length
+
+
+
+
+def name_length(self) ‑> bool +
+
+
+ +Expand source code + +
@JsonValidator(f"name attribute cannot exceed {name_max_length} characters")
+def name_length(self) -> bool:
+    return len(self.name) < self.name_max_length
+
+
+
+
+def placeholder_length(self) ‑> bool +
+
+
+ +Expand source code + +
@JsonValidator(f"placeholder attribute cannot exceed {placeholder_max_length} characters")
+def placeholder_length(self) -> bool:
+    return self.placeholder is None or len(self.placeholder) < self.placeholder_max_length
+
+
+
+
+def subtype_valid(self) ‑> bool +
+
+
+ +Expand source code + +
@EnumValidator("subtype", TextElementSubtypes)
+def subtype_valid(self) -> bool:
+    return self.subtype is None or self.subtype in TextElementSubtypes
+
+
+
+
+def value_length(self) ‑> bool +
+
+
+ +Expand source code + +
@JsonValidator("value attribute exceeded bounds")
+def value_length(self) -> bool:
+    return self.value is None or len(self.value) < self.max_value_length
+
+
+
+
+

Inherited members

+ +
+
+class DialogTextField +(*,
name: str,
label: str,
optional: bool = False,
placeholder: str | None = None,
hint: str | None = None,
value: str | None = None,
min_length: int = 0,
max_length: int | None = None,
subtype: str | None = None)
+
+
+
+ +Expand source code + +
class DialogTextField(DialogTextComponent):
+    """
+    Text elements are single-line plain text fields.
+
+    https://docs.slack.dev/legacy/legacy-dialogs/#text_elements
+    """
+
+    type = "text"
+    max_value_length = 150
+
+

Text elements are single-line plain text fields.

+

https://docs.slack.dev/legacy/legacy-dialogs/#text_elements

+

Ancestors

+ +

Class variables

+
+
var max_value_length
+
+

The type of the None singleton.

+
+
var type
+
+

The type of the None singleton.

+
+
+

Inherited members

+ +
+
+class DialogUserSelector +(*,
name: str,
label: str,
optional: bool = False,
value: str | None = None,
placeholder: str | None = None)
+
+
+
+ +Expand source code + +
class DialogUserSelector(AbstractDialogSelector):
+    data_source = "users"
+
+    def __init__(
+        self,
+        *,
+        name: str,
+        label: str,
+        optional: bool = False,
+        value: Optional[str] = None,
+        placeholder: Optional[str] = None,
+    ):
+        """
+        Now you can easily populate a select menu with a list of users. For example,
+        when you are creating a bug tracking app, you want to include a field for an
+        assignee. Slack pre-populates the user list in client-side, so your app
+        doesn't need access to a related OAuth scope.
+
+        https://docs.slack.dev/legacy/legacy-dialogs/#dynamic_select_elements_users
+
+        Args:
+            name: Name of form element. Required. No more than 300 characters.
+            label: Label displayed to user. Required. No more than 48 characters.
+            optional: Provide true when the form element is not required. By
+                default, form elements are required.
+            value: Provide a default selected value.
+            placeholder: A string displayed as needed to help guide users in
+                completing the element. 150 character maximum.
+        """
+        super().__init__(
+            name=name,
+            label=label,
+            optional=optional,
+            value=value,
+            placeholder=placeholder,
+        )
+
+

The base class for JSON serializable class objects

+

Now you can easily populate a select menu with a list of users. For example, +when you are creating a bug tracking app, you want to include a field for an +assignee. Slack pre-populates the user list in client-side, so your app +doesn't need access to a related OAuth scope.

+

https://docs.slack.dev/legacy/legacy-dialogs/#dynamic_select_elements_users

+

Args

+
+
name
+
Name of form element. Required. No more than 300 characters.
+
label
+
Label displayed to user. Required. No more than 48 characters.
+
optional
+
Provide true when the form element is not required. By +default, form elements are required.
+
value
+
Provide a default selected value.
+
placeholder
+
A string displayed as needed to help guide users in +completing the element. 150 character maximum.
+
+

Ancestors

+ +

Class variables

+
+
var data_source
+
+

The type of the None singleton.

+
+
+

Inherited members

+ +
+
+
+
+ +
+ + + diff --git a/docs/reference/models/index.html b/docs/reference/models/index.html new file mode 100644 index 000000000..d3670bfde --- /dev/null +++ b/docs/reference/models/index.html @@ -0,0 +1,596 @@ + + + + + + +slack_sdk.models API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.models

+
+
+

Classes for constructing Slack-specific data structure

+
+
+

Sub-modules

+
+
slack_sdk.models.attachments
+
+
+
+
slack_sdk.models.basic_objects
+
+
+
+
slack_sdk.models.blocks
+
+

Block Kit data model objects …

+
+
slack_sdk.models.dialoags
+
+
+
+
slack_sdk.models.dialogs
+
+
+
+
slack_sdk.models.messages
+
+
+
+
slack_sdk.models.metadata
+
+
+
+
slack_sdk.models.views
+
+
+
+
+
+
+
+
+

Functions

+
+
+def extract_json(item_or_items: JsonObject | Sequence[JsonObject],
*format_args) ‑> Dict[Any, Any] | List[Dict[Any, Any]] | Sequence[JsonObject]
+
+
+
+ +Expand source code + +
def extract_json(
+    item_or_items: Union[JsonObject, Sequence[JsonObject]], *format_args
+) -> Union[Dict[Any, Any], List[Dict[Any, Any]], Sequence[JsonObject]]:
+    """
+    Given a sequence (or single item), attempt to call the to_dict() method on each
+    item and return a plain list. If item is not the expected type, return it
+    unmodified, in case it's already a plain dict or some other user created class.
+
+    Args:
+      item_or_items: item(s) to go through
+      format_args: Any formatting specifiers to pass into the object's to_dict
+            method
+    """
+    try:
+        return [
+            elem.to_dict(*format_args) if isinstance(elem, JsonObject) else elem
+            for elem in item_or_items  # type: ignore[union-attr]
+        ]
+    except TypeError:  # not iterable, so try returning it as a single item
+        return item_or_items.to_dict(*format_args) if isinstance(item_or_items, JsonObject) else item_or_items
+
+

Given a sequence (or single item), attempt to call the to_dict() method on each +item and return a plain list. If item is not the expected type, return it +unmodified, in case it's already a plain dict or some other user created class.

+

Args

+
+
item_or_items
+
item(s) to go through
+
format_args
+
Any formatting specifiers to pass into the object's to_dict +method
+
+
+
+def show_unknown_key_warning(name: str | object, others: dict) +
+
+
+ +Expand source code + +
def show_unknown_key_warning(name: Union[str, object], others: dict):
+    if "type" in others:
+        others.pop("type")
+    if len(others) > 0:
+        keys = ", ".join(others.keys())
+        logger = logging.getLogger(__name__)
+        if isinstance(name, object):
+            name = name.__class__.__name__
+        logger.debug(
+            f"!!! {name}'s constructor args ({keys}) were ignored."
+            f"If they should be supported by this library, report this issue to the project :bow: "
+            f"https://github.com/slackapi/python-slack-sdk/issues"
+        )
+
+
+
+
+
+
+

Classes

+
+
+class BaseObject +
+
+
+ +Expand source code + +
class BaseObject:
+    """The base class for all model objects in this module"""
+
+    def __str__(self):
+        return f"<slack_sdk.{self.__class__.__name__}>"
+
+

The base class for all model objects in this module

+

Subclasses

+ +
+
+class EnumValidator +(attribute: str, enum: Iterable[str]) +
+
+
+ +Expand source code + +
class EnumValidator(JsonValidator):
+    def __init__(self, attribute: str, enum: Iterable[str]):
+        super().__init__(f"{attribute} attribute must be one of the following values: " f"{', '.join(enum)}")
+
+

Decorate a method on a class to mark it as a JSON validator. Validation +functions should return true if valid, false if not.

+

Args

+
+
message
+
Message to be attached to the thrown SlackObjectFormationError
+
+

Ancestors

+ +
+
+class JsonObject +
+
+
+ +Expand source code + +
class JsonObject(BaseObject, metaclass=ABCMeta):
+    """The base class for JSON serializable class objects"""
+
+    @property
+    @abstractmethod
+    def attributes(self) -> Set[str]:
+        """Provide a set of attributes of this object that will make up its JSON structure"""
+        return set()
+
+    def validate_json(self) -> None:
+        """
+        Raises:
+          SlackObjectFormationError if the object was not valid
+        """
+        for attribute in (func for func in dir(self) if not func.startswith("__")):
+            method = getattr(self, attribute, None)
+            if callable(method) and hasattr(method, "validator"):
+                method()
+
+    def get_object_attribute(self, key: str):
+        return getattr(self, key, None)
+
+    def get_non_null_attributes(self) -> dict:
+        """
+        Construct a dictionary out of non-null keys (from attributes property)
+        present on this object
+        """
+
+        def to_dict_compatible(value: Union[dict, list, object, tuple]) -> Union[dict, list, Any]:
+            if isinstance(value, (list, tuple)):
+                return [to_dict_compatible(v) for v in value]
+            else:
+                to_dict = getattr(value, "to_dict", None)
+                if to_dict and callable(to_dict):
+                    return {k: to_dict_compatible(v) for k, v in value.to_dict().items()}  # type: ignore[attr-defined]
+                else:
+                    return value
+
+        def is_not_empty(self, key: str) -> bool:
+            value = self.get_object_attribute(key)
+            if value is None:
+                return False
+
+            # Usually, Block Kit components do not allow an empty array for a property value, but there are some exceptions.
+            # The following code deals with these exceptions:
+            type_value = getattr(self, "type", None)
+            for empty_allowed in EMPTY_ALLOWED_TYPE_AND_PROPERTY_LIST:
+                if type_value == empty_allowed["type"] and key == empty_allowed["property"]:
+                    return True
+
+            has_len = getattr(value, "__len__", None) is not None
+            if has_len:
+                return len(value) > 0
+            else:
+                return value is not None
+
+        return {
+            key: to_dict_compatible(value=self.get_object_attribute(key))
+            for key in sorted(self.attributes)
+            if is_not_empty(self, key)
+        }
+
+    def to_dict(self, *args) -> dict:
+        """
+        Extract this object as a JSON-compatible, Slack-API-valid dictionary
+
+        Args:
+          *args: Any specific formatting args (rare; generally not required)
+
+        Raises:
+          SlackObjectFormationError if the object was not valid
+        """
+        self.validate_json()
+        return self.get_non_null_attributes()
+
+    def __repr__(self):
+        dict_value = self.get_non_null_attributes()
+        if dict_value:
+            return f"<slack_sdk.{self.__class__.__name__}: {dict_value}>"
+        else:
+            return self.__str__()
+
+    def __eq__(self, other: Any) -> bool:
+        if not isinstance(other, JsonObject):
+            return False
+        return self.to_dict() == other.to_dict()
+
+

The base class for JSON serializable class objects

+

Ancestors

+ +

Subclasses

+ +

Instance variables

+
+
prop attributes : Set[str]
+
+
+ +Expand source code + +
@property
+@abstractmethod
+def attributes(self) -> Set[str]:
+    """Provide a set of attributes of this object that will make up its JSON structure"""
+    return set()
+
+

Provide a set of attributes of this object that will make up its JSON structure

+
+
+

Methods

+
+
+def get_non_null_attributes(self) ‑> dict +
+
+
+ +Expand source code + +
def get_non_null_attributes(self) -> dict:
+    """
+    Construct a dictionary out of non-null keys (from attributes property)
+    present on this object
+    """
+
+    def to_dict_compatible(value: Union[dict, list, object, tuple]) -> Union[dict, list, Any]:
+        if isinstance(value, (list, tuple)):
+            return [to_dict_compatible(v) for v in value]
+        else:
+            to_dict = getattr(value, "to_dict", None)
+            if to_dict and callable(to_dict):
+                return {k: to_dict_compatible(v) for k, v in value.to_dict().items()}  # type: ignore[attr-defined]
+            else:
+                return value
+
+    def is_not_empty(self, key: str) -> bool:
+        value = self.get_object_attribute(key)
+        if value is None:
+            return False
+
+        # Usually, Block Kit components do not allow an empty array for a property value, but there are some exceptions.
+        # The following code deals with these exceptions:
+        type_value = getattr(self, "type", None)
+        for empty_allowed in EMPTY_ALLOWED_TYPE_AND_PROPERTY_LIST:
+            if type_value == empty_allowed["type"] and key == empty_allowed["property"]:
+                return True
+
+        has_len = getattr(value, "__len__", None) is not None
+        if has_len:
+            return len(value) > 0
+        else:
+            return value is not None
+
+    return {
+        key: to_dict_compatible(value=self.get_object_attribute(key))
+        for key in sorted(self.attributes)
+        if is_not_empty(self, key)
+    }
+
+

Construct a dictionary out of non-null keys (from attributes property) +present on this object

+
+
+def get_object_attribute(self, key: str) +
+
+
+ +Expand source code + +
def get_object_attribute(self, key: str):
+    return getattr(self, key, None)
+
+
+
+
+def to_dict(self, *args) ‑> dict +
+
+
+ +Expand source code + +
def to_dict(self, *args) -> dict:
+    """
+    Extract this object as a JSON-compatible, Slack-API-valid dictionary
+
+    Args:
+      *args: Any specific formatting args (rare; generally not required)
+
+    Raises:
+      SlackObjectFormationError if the object was not valid
+    """
+    self.validate_json()
+    return self.get_non_null_attributes()
+
+

Extract this object as a JSON-compatible, Slack-API-valid dictionary

+

Args

+
+
*args
+
Any specific formatting args (rare; generally not required)
+
+

Raises

+

SlackObjectFormationError if the object was not valid

+
+
+def validate_json(self) ‑> None +
+
+
+ +Expand source code + +
def validate_json(self) -> None:
+    """
+    Raises:
+      SlackObjectFormationError if the object was not valid
+    """
+    for attribute in (func for func in dir(self) if not func.startswith("__")):
+        method = getattr(self, attribute, None)
+        if callable(method) and hasattr(method, "validator"):
+            method()
+
+

Raises

+

SlackObjectFormationError if the object was not valid

+
+
+
+
+class JsonValidator +(message: str) +
+
+
+ +Expand source code + +
class JsonValidator:
+    def __init__(self, message: str):
+        """
+        Decorate a method on a class to mark it as a JSON validator. Validation
+            functions should return true if valid, false if not.
+
+        Args:
+            message: Message to be attached to the thrown SlackObjectFormationError
+        """
+        self.message = message
+
+    def __call__(self, func: Callable) -> Callable[..., None]:
+        @wraps(func)
+        def wrapped_f(*args, **kwargs):
+            if not func(*args, **kwargs):
+                raise SlackObjectFormationError(self.message)
+
+        wrapped_f.validator = True  # type: ignore[attr-defined]
+        return wrapped_f
+
+

Decorate a method on a class to mark it as a JSON validator. Validation +functions should return true if valid, false if not.

+

Args

+
+
message
+
Message to be attached to the thrown SlackObjectFormationError
+
+

Subclasses

+ +
+
+
+
+ +
+ + + diff --git a/docs/reference/models/messages/index.html b/docs/reference/models/messages/index.html new file mode 100644 index 000000000..5d10ced1c --- /dev/null +++ b/docs/reference/models/messages/index.html @@ -0,0 +1,300 @@ + + + + + + +slack_sdk.models.messages API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.models.messages

+
+
+
+
+

Sub-modules

+
+
slack_sdk.models.messages.message
+
+
+
+
+
+
+
+
+
+
+

Classes

+
+ +
+
+ +Expand source code + +
class ChannelLink(Link):
+    def __init__(self):
+        """Represents an @channel link, which notifies everyone present in this channel.
+        https://docs.slack.dev/messaging/formatting-message-text/
+        """
+        super().__init__(url="!channel", text="channel")
+
+

The base class for all model objects in this module

+

Represents an @channel link, which notifies everyone present in this channel. +https://docs.slack.dev/messaging/formatting-message-text/

+

Ancestors

+ +
+ +
+
+ +Expand source code + +
class DateLink(Link):
+    def __init__(
+        self,
+        *,
+        date: Union[datetime, int],
+        date_format: str,
+        fallback: str,
+        link: Optional[str] = None,
+    ):
+        """Text containing a date or time should display that date in the local timezone of the person seeing the text.
+        https://docs.slack.dev/messaging/formatting-message-text/#date-formatting
+        """
+        if isinstance(date, datetime):
+            epoch = int(date.timestamp())
+        else:
+            epoch = date
+        if link is not None:
+            link = f"^{link}"
+        else:
+            link = ""
+        super().__init__(url=f"!date^{epoch}^{date_format}{link}", text=fallback)
+
+

The base class for all model objects in this module

+

Text containing a date or time should display that date in the local timezone of the person seeing the text. +https://docs.slack.dev/messaging/formatting-message-text/#date-formatting

+

Ancestors

+ +
+ +
+
+ +Expand source code + +
class EveryoneLink(Link):
+    def __init__(self):
+        """Represents an @everyone link, which notifies all users of this workspace.
+        https://docs.slack.dev/messaging/formatting-message-text/
+        """
+        super().__init__(url="!everyone", text="everyone")
+
+

The base class for all model objects in this module

+

Represents an @everyone link, which notifies all users of this workspace. +https://docs.slack.dev/messaging/formatting-message-text/

+

Ancestors

+ +
+ +
+
+ +Expand source code + +
class HereLink(Link):
+    def __init__(self):
+        """Represents an @here link, which notifies all online users of this channel.
+        https://docs.slack.dev/messaging/formatting-message-text/
+        """
+        super().__init__(url="!here", text="here")
+
+

The base class for all model objects in this module

+

Represents an @here link, which notifies all online users of this channel. +https://docs.slack.dev/messaging/formatting-message-text/

+

Ancestors

+ +
+ +
+
+ +Expand source code + +
class Link(BaseObject):
+    def __init__(self, *, url: str, text: str):
+        """Base class used to generate links in Slack's not-quite Markdown, not quite HTML syntax
+        https://docs.slack.dev/messaging/formatting-message-text/#linking_to_urls
+        """
+        self.url = url
+        self.text = text
+
+    def __str__(self):
+        if self.text:
+            separator = "|"
+        else:
+            separator = ""
+        return f"<{self.url}{separator}{self.text}>"
+
+

The base class for all model objects in this module

+

Base class used to generate links in Slack's not-quite Markdown, not quite HTML syntax +https://docs.slack.dev/messaging/formatting-message-text/#linking_to_urls

+

Ancestors

+ +

Subclasses

+ +
+ +
+
+ +Expand source code + +
class ObjectLink(Link):
+    prefix_mapping = {
+        "C": "#",  # channel
+        "G": "#",  # group message
+        "U": "@",  # user
+        "W": "@",  # workspace user (enterprise)
+        "B": "@",  # bot user
+        "S": "!subteam^",  # user groups, originally known as subteams
+    }
+
+    def __init__(self, *, object_id: str, text: str = ""):
+        """Convenience class to create links to specific object types
+        https://docs.slack.dev/messaging/formatting-message-text/#linking-channels
+        """
+        prefix = self.prefix_mapping.get(object_id[0].upper(), "@")
+        super().__init__(url=f"{prefix}{object_id}", text=text)
+
+

The base class for all model objects in this module

+

Convenience class to create links to specific object types +https://docs.slack.dev/messaging/formatting-message-text/#linking-channels

+

Ancestors

+ +

Class variables

+
+
var prefix_mapping
+
+

The type of the None singleton.

+
+
+
+
+
+
+ +
+ + + diff --git a/docs/reference/models/messages/message.html b/docs/reference/models/messages/message.html new file mode 100644 index 000000000..6a7e7a85a --- /dev/null +++ b/docs/reference/models/messages/message.html @@ -0,0 +1,210 @@ + + + + + + +slack_sdk.models.messages.message API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.models.messages.message

+
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class Message +(*,
text: str,
attachments: Sequence[Attachment] | None = None,
blocks: Sequence[Block] | None = None,
markdown: bool = True)
+
+
+
+ +Expand source code + +
class Message(JsonObject):
+    attributes = {"text"}
+
+    attachments_max_length = 100
+
+    def __init__(
+        self,
+        *,
+        text: str,
+        attachments: Optional[Sequence[Attachment]] = None,
+        blocks: Optional[Sequence[Block]] = None,
+        markdown: bool = True,
+    ):
+        """
+        Create a message.
+
+        https://docs.slack.dev/messaging/#message-structure
+
+        Args:
+            text: Plain or Slack Markdown-like text to display in the message.
+            attachments: A list of Attachment objects to display after the rest of
+                the message's content. More than 20 is not recommended, but the actual
+                limit is 100
+            blocks: A list of Block objects to attach to this message. If
+                specified, the 'text' property is ignored (more specifically, it's used
+                as a fallback on clients that can't render blocks)
+            markdown: Whether to parse markdown into formatting such as
+                bold/italics, or leave text completely unmodified.
+        """
+        self.text = text
+        self.attachments = attachments or []
+        self.blocks = blocks or []
+        self.markdown = markdown
+
+    @JsonValidator(f"attachments attribute cannot exceed {attachments_max_length} items")
+    def attachments_length(self):
+        return self.attachments is None or len(self.attachments) <= self.attachments_max_length
+
+    def to_dict(self) -> dict:
+        json = super().to_dict()
+        if len(self.text) > 40000:
+            LOGGER.error("Messages over 40,000 characters are automatically truncated by Slack")
+        # The following limitation used to be true in the past.
+        # As of Feb 2021, having both is recommended
+        # -----------------
+        # if self.text and self.blocks:
+        #     #  Slack doesn't render the text property if there are blocks, so:
+        #     LOGGER.info(q
+        #         "text attribute is treated as fallback text if blocks are attached to "
+        #         "a message - insert text as a new SectionBlock if you want it to be "
+        #         "displayed "
+        #     )
+        json["attachments"] = extract_json(self.attachments)
+        json["blocks"] = extract_json(self.blocks)
+        json["mrkdwn"] = self.markdown
+        return json
+
+

The base class for JSON serializable class objects

+

Create a message.

+

https://docs.slack.dev/messaging/#message-structure

+

Args

+
+
text
+
Plain or Slack Markdown-like text to display in the message.
+
attachments
+
A list of Attachment objects to display after the rest of +the message's content. More than 20 is not recommended, but the actual +limit is 100
+
blocks
+
A list of Block objects to attach to this message. If +specified, the 'text' property is ignored (more specifically, it's used +as a fallback on clients that can't render blocks)
+
markdown
+
Whether to parse markdown into formatting such as +bold/italics, or leave text completely unmodified.
+
+

Ancestors

+ +

Class variables

+
+
var attachments_max_length
+
+

The type of the None singleton.

+
+
var attributes
+
+

The type of the None singleton.

+
+
+

Methods

+
+
+def attachments_length(self) +
+
+
+ +Expand source code + +
@JsonValidator(f"attachments attribute cannot exceed {attachments_max_length} items")
+def attachments_length(self):
+    return self.attachments is None or len(self.attachments) <= self.attachments_max_length
+
+
+
+
+

Inherited members

+ +
+
+
+
+ +
+ + + diff --git a/docs/reference/models/metadata/index.html b/docs/reference/models/metadata/index.html new file mode 100644 index 000000000..1c4d7a69c --- /dev/null +++ b/docs/reference/models/metadata/index.html @@ -0,0 +1,2680 @@ + + + + + + +slack_sdk.models.metadata API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.models.metadata

+
+
+
+
+
+
+

Global variables

+
+
var EntityType
+
+

Custom field types

+
+
+
+
+
+
+

Classes

+
+
+class ContentItemEntityFields +(preview: Dict[str, Any] | EntityImageField | None = None,
description: Dict[str, Any] | EntityStringField | None = None,
created_by: Dict[str, Any] | EntityTypedField | None = None,
date_created: Dict[str, Any] | EntityTimestampField | None = None,
date_updated: Dict[str, Any] | EntityTimestampField | None = None,
last_modified_by: Dict[str, Any] | EntityTypedField | None = None,
**kwargs)
+
+
+
+ +Expand source code + +
class ContentItemEntityFields(JsonObject):
+    """Fields specific to content item entities"""
+
+    attributes = {
+        "preview",
+        "description",
+        "created_by",
+        "date_created",
+        "date_updated",
+        "last_modified_by",
+    }
+
+    def __init__(
+        self,
+        preview: Optional[Union[Dict[str, Any], EntityImageField]] = None,
+        description: Optional[Union[Dict[str, Any], EntityStringField]] = None,
+        created_by: Optional[Union[Dict[str, Any], EntityTypedField]] = None,
+        date_created: Optional[Union[Dict[str, Any], EntityTimestampField]] = None,
+        date_updated: Optional[Union[Dict[str, Any], EntityTimestampField]] = None,
+        last_modified_by: Optional[Union[Dict[str, Any], EntityTypedField]] = None,
+        **kwargs,
+    ):
+        self.preview = preview
+        self.description = description
+        self.created_by = created_by
+        self.date_created = date_created
+        self.date_updated = date_updated
+        self.last_modified_by = last_modified_by
+        self.additional_attributes = kwargs
+
+    def __str__(self):
+        return str(self.get_non_null_attributes())
+
+    def __repr__(self):
+        return self.__str__()
+
+

Fields specific to content item entities

+

Ancestors

+ +

Class variables

+
+
var attributes
+
+

The type of the None singleton.

+
+
+

Inherited members

+ +
+
+class EntityActionButton +(text: str,
action_id: str,
value: str | None = None,
style: str | None = None,
url: str | None = None,
accessibility_label: str | None = None,
processing_state: Dict[str, Any] | EntityActionProcessingState | None = None,
**kwargs)
+
+
+
+ +Expand source code + +
class EntityActionButton(JsonObject):
+    """Action button for entity"""
+
+    attributes = {
+        "text",
+        "action_id",
+        "value",
+        "style",
+        "url",
+        "accessibility_label",
+        "processing_state",
+    }
+
+    def __init__(
+        self,
+        text: str,
+        action_id: str,
+        value: Optional[str] = None,
+        style: Optional[str] = None,
+        url: Optional[str] = None,
+        accessibility_label: Optional[str] = None,
+        processing_state: Optional[Union[Dict[str, Any], EntityActionProcessingState]] = None,
+        **kwargs,
+    ):
+        self.text = text
+        self.action_id = action_id
+        self.value = value
+        self.style = style
+        self.url = url
+        self.accessibility_label = accessibility_label
+        self.processing_state = processing_state
+        self.additional_attributes = kwargs
+
+    def __str__(self):
+        return str(self.get_non_null_attributes())
+
+    def __repr__(self):
+        return self.__str__()
+
+

Action button for entity

+

Ancestors

+ +

Class variables

+
+
var attributes
+
+

The type of the None singleton.

+
+
+

Inherited members

+ +
+
+class EntityActionProcessingState +(enabled: bool, interstitial_text: str | None = None, **kwargs) +
+
+
+ +Expand source code + +
class EntityActionProcessingState(JsonObject):
+    """Processing state configuration for entity action button"""
+
+    attributes = {
+        "enabled",
+        "interstitial_text",
+    }
+
+    def __init__(
+        self,
+        enabled: bool,
+        interstitial_text: Optional[str] = None,
+        **kwargs,
+    ):
+        self.enabled = enabled
+        self.interstitial_text = interstitial_text
+        self.additional_attributes = kwargs
+
+    def __str__(self):
+        return str(self.get_non_null_attributes())
+
+    def __repr__(self):
+        return self.__str__()
+
+

Processing state configuration for entity action button

+

Ancestors

+ +

Class variables

+
+
var attributes
+
+

The type of the None singleton.

+
+
+

Inherited members

+ +
+
+class EntityActions +(primary_actions: List[Dict[str, Any] | EntityActionButton] | None = None,
overflow_actions: List[Dict[str, Any] | EntityActionButton] | None = None,
**kwargs)
+
+
+
+ +Expand source code + +
class EntityActions(JsonObject):
+    """Actions configuration for entity"""
+
+    attributes = {
+        "primary_actions",
+        "overflow_actions",
+    }
+
+    def __init__(
+        self,
+        primary_actions: Optional[List[Union[Dict[str, Any], EntityActionButton]]] = None,
+        overflow_actions: Optional[List[Union[Dict[str, Any], EntityActionButton]]] = None,
+        **kwargs,
+    ):
+        self.primary_actions = primary_actions
+        self.overflow_actions = overflow_actions
+        self.additional_attributes = kwargs
+
+    def __str__(self):
+        return str(self.get_non_null_attributes())
+
+    def __repr__(self):
+        return self.__str__()
+
+

Actions configuration for entity

+

Ancestors

+ +

Class variables

+
+
var attributes
+
+

The type of the None singleton.

+
+
+

Inherited members

+ +
+
+class EntityArrayItemField +(type: str | None = None,
label: str | None = None,
value: str | int | None = None,
link: str | None = None,
icon: Dict[str, Any] | EntityIconField | None = None,
long: bool | None = None,
format: str | None = None,
image_url: str | None = None,
slack_file: Dict[str, Any] | None = None,
alt_text: str | None = None,
edit: Dict[str, Any] | EntityEditSupport | None = None,
tag_color: str | None = None,
user: Dict[str, Any] | EntityUserIDField | EntityUserField | None = None,
entity_ref: Dict[str, Any] | EntityRefField | None = None,
**kwargs)
+
+
+
+ +Expand source code + +
class EntityArrayItemField(JsonObject):
+    """Array item field for entity (similar to EntityTypedField but with optional type)"""
+
+    attributes = {
+        "type",
+        "label",
+        "value",
+        "link",
+        "icon",
+        "long",
+        "format",
+        "image_url",
+        "slack_file",
+        "alt_text",
+        "edit",
+        "tag_color",
+        "user",
+        "entity_ref",
+    }
+
+    def __init__(
+        self,
+        type: Optional[str] = None,
+        label: Optional[str] = None,
+        value: Optional[Union[str, int]] = None,
+        link: Optional[str] = None,
+        icon: Optional[Union[Dict[str, Any], EntityIconField]] = None,
+        long: Optional[bool] = None,
+        format: Optional[str] = None,
+        image_url: Optional[str] = None,
+        slack_file: Optional[Dict[str, Any]] = None,
+        alt_text: Optional[str] = None,
+        edit: Optional[Union[Dict[str, Any], EntityEditSupport]] = None,
+        tag_color: Optional[str] = None,
+        user: Optional[Union[Dict[str, Any], EntityUserIDField, EntityUserField]] = None,
+        entity_ref: Optional[Union[Dict[str, Any], EntityRefField]] = None,
+        **kwargs,
+    ):
+        self.type = type
+        self.label = label
+        self.value = value
+        self.link = link
+        self.icon = icon
+        self.long = long
+        self.format = format
+        self.image_url = image_url
+        self.slack_file = slack_file
+        self.alt_text = alt_text
+        self.edit = edit
+        self.tag_color = tag_color
+        self.user = user
+        self.entity_ref = entity_ref
+        self.additional_attributes = kwargs
+
+    def __str__(self):
+        return str(self.get_non_null_attributes())
+
+    def __repr__(self):
+        return self.__str__()
+
+

Array item field for entity (similar to EntityTypedField but with optional type)

+

Ancestors

+ +

Class variables

+
+
var attributes
+
+

The type of the None singleton.

+
+
+

Inherited members

+ +
+
+class EntityAttributes +(title: Dict[str, Any] | EntityTitle,
display_type: str | None = None,
display_id: str | None = None,
product_icon: Dict[str, Any] | EntityIconField | None = None,
product_name: str | None = None,
locale: str | None = None,
full_size_preview: Dict[str, Any] | EntityFullSizePreview | None = None,
metadata_last_modified: int | None = None,
**kwargs)
+
+
+
+ +Expand source code + +
class EntityAttributes(JsonObject):
+    """Attributes for an entity"""
+
+    attributes = {
+        "title",
+        "display_type",
+        "display_id",
+        "product_icon",
+        "product_name",
+        "locale",
+        "full_size_preview",
+        "metadata_last_modified",
+    }
+
+    def __init__(
+        self,
+        title: Union[Dict[str, Any], EntityTitle],
+        display_type: Optional[str] = None,
+        display_id: Optional[str] = None,
+        product_icon: Optional[Union[Dict[str, Any], EntityIconField]] = None,
+        product_name: Optional[str] = None,
+        locale: Optional[str] = None,
+        full_size_preview: Optional[Union[Dict[str, Any], EntityFullSizePreview]] = None,
+        metadata_last_modified: Optional[int] = None,
+        **kwargs,
+    ):
+        self.title = title
+        self.display_type = display_type
+        self.display_id = display_id
+        self.product_icon = product_icon
+        self.product_name = product_name
+        self.locale = locale
+        self.full_size_preview = full_size_preview
+        self.metadata_last_modified = metadata_last_modified
+        self.additional_attributes = kwargs
+
+    def __str__(self):
+        return str(self.get_non_null_attributes())
+
+    def __repr__(self):
+        return self.__str__()
+
+

Attributes for an entity

+

Ancestors

+ +

Class variables

+
+
var attributes
+
+

The type of the None singleton.

+
+
+

Inherited members

+ +
+
+class EntityBooleanCheckboxField +(type: str, text: str, description: str | None, **kwargs) +
+
+
+ +Expand source code + +
class EntityBooleanCheckboxField(JsonObject):
+    """Boolean checkbox properties"""
+
+    attributes = {"type", "text", "description"}
+
+    def __init__(
+        self,
+        type: str,
+        text: str,
+        description: Optional[str],
+        **kwargs,
+    ):
+        self.type = type
+        self.text = text
+        self.description = description
+        self.additional_attributes = kwargs
+
+    def __str__(self):
+        return str(self.get_non_null_attributes())
+
+    def __repr__(self):
+        return self.__str__()
+
+

Boolean checkbox properties

+

Ancestors

+ +

Class variables

+
+
var attributes
+
+

The type of the None singleton.

+
+
+

Inherited members

+ +
+
+class EntityBooleanTextField +(type: str,
true_text: str,
false_text: str,
true_description: str | None,
false_description: str | None,
**kwargs)
+
+
+
+ +Expand source code + +
class EntityBooleanTextField(JsonObject):
+    """Boolean text properties"""
+
+    attributes = {"type", "true_text", "false_text", "true_description", "false_description"}
+
+    def __init__(
+        self,
+        type: str,
+        true_text: str,
+        false_text: str,
+        true_description: Optional[str],
+        false_description: Optional[str],
+        **kwargs,
+    ):
+        self.type = type
+        self.true_text = (true_text,)
+        self.false_text = (false_text,)
+        self.true_description = (true_description,)
+        self.false_description = (false_description,)
+        self.additional_attributes = kwargs
+
+    def __str__(self):
+        return str(self.get_non_null_attributes())
+
+    def __repr__(self):
+        return self.__str__()
+
+

Boolean text properties

+

Ancestors

+ +

Class variables

+
+
var attributes
+
+

The type of the None singleton.

+
+
+

Inherited members

+ +
+
+class EntityCustomField +(label: str,
key: str,
type: str,
value: str | int | List[Dict[str, Any] | EntityArrayItemField] | None = None,
link: str | None = None,
icon: Dict[str, Any] | EntityIconField | None = None,
long: bool | None = None,
format: str | None = None,
image_url: str | None = None,
slack_file: Dict[str, Any] | None = None,
alt_text: str | None = None,
tag_color: str | None = None,
edit: Dict[str, Any] | EntityEditSupport | None = None,
item_type: str | None = None,
user: Dict[str, Any] | EntityUserIDField | EntityUserField | None = None,
entity_ref: Dict[str, Any] | EntityRefField | None = None,
boolean: Dict[str, Any] | EntityBooleanCheckboxField | EntityBooleanTextField | None = None,
**kwargs)
+
+
+
+ +Expand source code + +
class EntityCustomField(JsonObject):
+    """Custom field for entity with flexible types"""
+
+    attributes = {
+        "label",
+        "key",
+        "type",
+        "value",
+        "link",
+        "icon",
+        "long",
+        "format",
+        "image_url",
+        "slack_file",
+        "alt_text",
+        "tag_color",
+        "edit",
+        "item_type",
+        "user",
+        "entity_ref",
+        "boolean",
+    }
+
+    def __init__(
+        self,
+        label: str,
+        key: str,
+        type: str,
+        value: Optional[Union[str, int, List[Union[Dict[str, Any], EntityArrayItemField]]]] = None,
+        link: Optional[str] = None,
+        icon: Optional[Union[Dict[str, Any], EntityIconField]] = None,
+        long: Optional[bool] = None,
+        format: Optional[str] = None,
+        image_url: Optional[str] = None,
+        slack_file: Optional[Dict[str, Any]] = None,
+        alt_text: Optional[str] = None,
+        tag_color: Optional[str] = None,
+        edit: Optional[Union[Dict[str, Any], EntityEditSupport]] = None,
+        item_type: Optional[str] = None,
+        user: Optional[Union[Dict[str, Any], EntityUserIDField, EntityUserField]] = None,
+        entity_ref: Optional[Union[Dict[str, Any], EntityRefField]] = None,
+        boolean: Optional[Union[Dict[str, Any], EntityBooleanCheckboxField, EntityBooleanTextField]] = None,
+        **kwargs,
+    ):
+        self.label = label
+        self.key = key
+        self.type = type
+        self.value = value
+        self.link = link
+        self.icon = icon
+        self.long = long
+        self.format = format
+        self.image_url = image_url
+        self.slack_file = slack_file
+        self.alt_text = alt_text
+        self.tag_color = tag_color
+        self.edit = edit
+        self.item_type = item_type
+        self.user = user
+        self.entity_ref = entity_ref
+        self.boolean = boolean
+        self.additional_attributes = kwargs
+
+    def __str__(self):
+        return str(self.get_non_null_attributes())
+
+    def __repr__(self):
+        return self.__str__()
+
+    @EnumValidator("type", CustomFieldType)
+    def type_valid(self):
+        return self.type is None or self.type in CustomFieldType
+
+

Custom field for entity with flexible types

+

Ancestors

+ +

Class variables

+
+
var attributes
+
+

The type of the None singleton.

+
+
+

Methods

+
+
+def type_valid(self) +
+
+
+ +Expand source code + +
@EnumValidator("type", CustomFieldType)
+def type_valid(self):
+    return self.type is None or self.type in CustomFieldType
+
+
+
+
+

Inherited members

+ +
+
+class EntityEditNumberConfig +(is_decimal_allowed: bool | None = None,
min_value: int | float | None = None,
max_value: int | float | None = None,
**kwargs)
+
+
+
+ +Expand source code + +
class EntityEditNumberConfig(JsonObject):
+    """Number configuration for entity edit support"""
+
+    attributes = {
+        "is_decimal_allowed",
+        "min_value",
+        "max_value",
+    }
+
+    def __init__(
+        self,
+        is_decimal_allowed: Optional[bool] = None,
+        min_value: Optional[Union[int, float]] = None,
+        max_value: Optional[Union[int, float]] = None,
+        **kwargs,
+    ):
+        self.is_decimal_allowed = is_decimal_allowed
+        self.min_value = min_value
+        self.max_value = max_value
+        self.additional_attributes = kwargs
+
+    def __str__(self):
+        return str(self.get_non_null_attributes())
+
+    def __repr__(self):
+        return self.__str__()
+
+

Number configuration for entity edit support

+

Ancestors

+ +

Class variables

+
+
var attributes
+
+

The type of the None singleton.

+
+
+

Inherited members

+ +
+
+class EntityEditSelectConfig +(current_value: str | None = None,
current_values: List[str] | None = None,
static_options: List[Dict[str, Any]] | None = None,
fetch_options_dynamically: bool | None = None,
min_query_length: int | None = None,
**kwargs)
+
+
+
+ +Expand source code + +
class EntityEditSelectConfig(JsonObject):
+    """Select configuration for entity edit support"""
+
+    attributes = {
+        "current_value",
+        "current_values",
+        "static_options",
+        "fetch_options_dynamically",
+        "min_query_length",
+    }
+
+    def __init__(
+        self,
+        current_value: Optional[str] = None,
+        current_values: Optional[List[str]] = None,
+        static_options: Optional[List[Dict[str, Any]]] = None,  # Option[]
+        fetch_options_dynamically: Optional[bool] = None,
+        min_query_length: Optional[int] = None,
+        **kwargs,
+    ):
+        self.current_value = current_value
+        self.current_values = current_values
+        self.static_options = static_options
+        self.fetch_options_dynamically = fetch_options_dynamically
+        self.min_query_length = min_query_length
+        self.additional_attributes = kwargs
+
+    def __str__(self):
+        return str(self.get_non_null_attributes())
+
+    def __repr__(self):
+        return self.__str__()
+
+

Select configuration for entity edit support

+

Ancestors

+ +

Class variables

+
+
var attributes
+
+

The type of the None singleton.

+
+
+

Inherited members

+ +
+
+class EntityEditSupport +(enabled: bool,
placeholder: Dict[str, Any] | None = None,
hint: Dict[str, Any] | None = None,
optional: bool | None = None,
select: Dict[str, Any] | EntityEditSelectConfig | None = None,
number: Dict[str, Any] | EntityEditNumberConfig | None = None,
text: Dict[str, Any] | EntityEditTextConfig | None = None,
**kwargs)
+
+
+
+ +Expand source code + +
class EntityEditSupport(JsonObject):
+    """Edit support configuration for entity fields"""
+
+    attributes = {
+        "enabled",
+        "placeholder",
+        "hint",
+        "optional",
+        "select",
+        "number",
+        "text",
+    }
+
+    def __init__(
+        self,
+        enabled: bool,
+        placeholder: Optional[Dict[str, Any]] = None,  # PlainTextElement
+        hint: Optional[Dict[str, Any]] = None,  # PlainTextElement
+        optional: Optional[bool] = None,
+        select: Optional[Union[Dict[str, Any], EntityEditSelectConfig]] = None,
+        number: Optional[Union[Dict[str, Any], EntityEditNumberConfig]] = None,
+        text: Optional[Union[Dict[str, Any], EntityEditTextConfig]] = None,
+        **kwargs,
+    ):
+        self.enabled = enabled
+        self.placeholder = placeholder
+        self.hint = hint
+        self.optional = optional
+        self.select = select
+        self.number = number
+        self.text = text
+        self.additional_attributes = kwargs
+
+    def __str__(self):
+        return str(self.get_non_null_attributes())
+
+    def __repr__(self):
+        return self.__str__()
+
+

Edit support configuration for entity fields

+

Ancestors

+ +

Class variables

+
+
var attributes
+
+

The type of the None singleton.

+
+
+

Inherited members

+ +
+
+class EntityEditTextConfig +(min_length: int | None = None, max_length: int | None = None, **kwargs) +
+
+
+ +Expand source code + +
class EntityEditTextConfig(JsonObject):
+    """Text configuration for entity edit support"""
+
+    attributes = {
+        "min_length",
+        "max_length",
+    }
+
+    def __init__(
+        self,
+        min_length: Optional[int] = None,
+        max_length: Optional[int] = None,
+        **kwargs,
+    ):
+        self.min_length = min_length
+        self.max_length = max_length
+        self.additional_attributes = kwargs
+
+    def __str__(self):
+        return str(self.get_non_null_attributes())
+
+    def __repr__(self):
+        return self.__str__()
+
+

Text configuration for entity edit support

+

Ancestors

+ +

Class variables

+
+
var attributes
+
+

The type of the None singleton.

+
+
+

Inherited members

+ +
+
+class EntityFullSizePreview +(is_supported: bool,
preview_url: str | None = None,
mime_type: str | None = None,
error: Dict[str, Any] | EntityFullSizePreviewError | None = None,
**kwargs)
+
+
+
+ +Expand source code + +
class EntityFullSizePreview(JsonObject):
+    """Full-size preview configuration for entity"""
+
+    attributes = {
+        "is_supported",
+        "preview_url",
+        "mime_type",
+        "error",
+    }
+
+    def __init__(
+        self,
+        is_supported: bool,
+        preview_url: Optional[str] = None,
+        mime_type: Optional[str] = None,
+        error: Optional[Union[Dict[str, Any], EntityFullSizePreviewError]] = None,
+        **kwargs,
+    ):
+        self.is_supported = is_supported
+        self.preview_url = preview_url
+        self.mime_type = mime_type
+        self.error = error
+        self.additional_attributes = kwargs
+
+    def __str__(self):
+        return str(self.get_non_null_attributes())
+
+    def __repr__(self):
+        return self.__str__()
+
+

Full-size preview configuration for entity

+

Ancestors

+ +

Class variables

+
+
var attributes
+
+

The type of the None singleton.

+
+
+

Inherited members

+ +
+
+class EntityFullSizePreviewError +(code: str, message: str | None = None, **kwargs) +
+
+
+ +Expand source code + +
class EntityFullSizePreviewError(JsonObject):
+    """Error information for full-size preview"""
+
+    attributes = {
+        "code",
+        "message",
+    }
+
+    def __init__(
+        self,
+        code: str,
+        message: Optional[str] = None,
+        **kwargs,
+    ):
+        self.code = code
+        self.message = message
+        self.additional_attributes = kwargs
+
+    def __str__(self):
+        return str(self.get_non_null_attributes())
+
+    def __repr__(self):
+        return self.__str__()
+
+

Error information for full-size preview

+

Ancestors

+ +

Class variables

+
+
var attributes
+
+

The type of the None singleton.

+
+
+

Inherited members

+ +
+
+class EntityIconField +(alt_text: str,
url: str | None = None,
slack_file: Dict[str, Any] | EntityIconSlackFile | None = None,
**kwargs)
+
+
+
+ +Expand source code + +
class EntityIconField(JsonObject):
+    """Icon field for entity attributes"""
+
+    attributes = {
+        "alt_text",
+        "url",
+        "slack_file",
+    }
+
+    def __init__(
+        self,
+        alt_text: str,
+        url: Optional[str] = None,
+        slack_file: Optional[Union[Dict[str, Any], EntityIconSlackFile]] = None,
+        **kwargs,
+    ):
+        self.alt_text = alt_text
+        self.url = url
+        self.slack_file = slack_file
+        self.additional_attributes = kwargs
+
+    def __str__(self):
+        return str(self.get_non_null_attributes())
+
+    def __repr__(self):
+        return self.__str__()
+
+

Icon field for entity attributes

+

Ancestors

+ +

Class variables

+
+
var attributes
+
+

The type of the None singleton.

+
+
+

Inherited members

+ +
+
+class EntityIconSlackFile +(id: str | None = None, url: str | None = None, **kwargs) +
+
+
+ +Expand source code + +
class EntityIconSlackFile(JsonObject):
+    """Slack file reference for entity icon"""
+
+    attributes = {
+        "id",
+        "url",
+    }
+
+    def __init__(
+        self,
+        id: Optional[str] = None,
+        url: Optional[str] = None,
+        **kwargs,
+    ):
+        self.id = id
+        self.url = url
+        self.additional_attributes = kwargs
+
+    def __str__(self):
+        return str(self.get_non_null_attributes())
+
+    def __repr__(self):
+        return self.__str__()
+
+

Slack file reference for entity icon

+

Ancestors

+ +

Class variables

+
+
var attributes
+
+

The type of the None singleton.

+
+
+

Inherited members

+ +
+
+class EntityImageField +(alt_text: str,
label: str | None = None,
image_url: str | None = None,
slack_file: Dict[str, Any] | None = None,
title: str | None = None,
type: str | None = None,
**kwargs)
+
+
+
+ +Expand source code + +
class EntityImageField(JsonObject):
+    """Image field for entity"""
+
+    attributes = {
+        "alt_text",
+        "label",
+        "image_url",
+        "slack_file",
+        "title",
+        "type",
+    }
+
+    def __init__(
+        self,
+        alt_text: str,
+        label: Optional[str] = None,
+        image_url: Optional[str] = None,
+        slack_file: Optional[Dict[str, Any]] = None,
+        title: Optional[str] = None,
+        type: Optional[str] = None,
+        **kwargs,
+    ):
+        self.alt_text = alt_text
+        self.label = label
+        self.image_url = image_url
+        self.slack_file = slack_file
+        self.title = title
+        self.type = type
+        self.additional_attributes = kwargs
+
+    def __str__(self):
+        return str(self.get_non_null_attributes())
+
+    def __repr__(self):
+        return self.__str__()
+
+

Image field for entity

+

Ancestors

+ +

Class variables

+
+
var attributes
+
+

The type of the None singleton.

+
+
+

Inherited members

+ +
+
+class EntityMetadata +(entity_type: str,
entity_payload: Dict[str, Any] | EntityPayload,
external_ref: Dict[str, Any] | ExternalRef,
url: str,
app_unfurl_url: str | None = None,
**kwargs)
+
+
+
+ +Expand source code + +
class EntityMetadata(JsonObject):
+    """Work object entity metadata
+
+    https://docs.slack.dev/messaging/work-objects/
+    """
+
+    attributes = {
+        "entity_type",
+        "entity_payload",
+        "external_ref",
+        "url",
+        "app_unfurl_url",
+    }
+
+    def __init__(
+        self,
+        entity_type: str,
+        entity_payload: Union[Dict[str, Any], EntityPayload],
+        external_ref: Union[Dict[str, Any], ExternalRef],
+        url: str,
+        app_unfurl_url: Optional[str] = None,
+        **kwargs,
+    ):
+        self.entity_type = entity_type
+        self.entity_payload = entity_payload
+        self.external_ref = external_ref
+        self.url = url
+        self.app_unfurl_url = app_unfurl_url
+        self.additional_attributes = kwargs
+
+    def __str__(self):
+        return str(self.get_non_null_attributes())
+
+    def __repr__(self):
+        return self.__str__()
+
+    @EnumValidator("entity_type", EntityType)
+    def entity_type_valid(self):
+        return self.entity_type is None or self.entity_type in EntityType
+
+ +

Ancestors

+ +

Class variables

+
+
var attributes
+
+

The type of the None singleton.

+
+
+

Methods

+
+
+def entity_type_valid(self) +
+
+
+ +Expand source code + +
@EnumValidator("entity_type", EntityType)
+def entity_type_valid(self):
+    return self.entity_type is None or self.entity_type in EntityType
+
+
+
+
+

Inherited members

+ +
+
+class EntityPayload +(attributes: Dict[str, Any] | EntityAttributes,
fields: Dict[str, Any] | ContentItemEntityFields | FileEntityFields | IncidentEntityFields | TaskEntityFields | None = None,
custom_fields: List[Dict[str, Any] | EntityCustomField] | None = None,
slack_file: Dict[str, Any] | FileEntitySlackFile | None = None,
display_order: List[str] | None = None,
actions: Dict[str, Any] | EntityActions | None = None,
**kwargs)
+
+
+
+ +Expand source code + +
class EntityPayload(JsonObject):
+    """Payload schema for an entity"""
+
+    attributes = {
+        "attributes",
+        "fields",
+        "custom_fields",
+        "slack_file",
+        "display_order",
+        "actions",
+    }
+
+    def __init__(
+        self,
+        attributes: Union[Dict[str, Any], EntityAttributes],
+        fields: Optional[
+            Union[Dict[str, Any], ContentItemEntityFields, FileEntityFields, IncidentEntityFields, TaskEntityFields]
+        ] = None,
+        custom_fields: Optional[List[Union[Dict[str, Any], EntityCustomField]]] = None,
+        slack_file: Optional[Union[Dict[str, Any], FileEntitySlackFile]] = None,
+        display_order: Optional[List[str]] = None,
+        actions: Optional[Union[Dict[str, Any], EntityActions]] = None,
+        **kwargs,
+    ):
+        # Store entity attributes data with a different internal name to avoid
+        # shadowing the class-level 'attributes' set used for JSON serialization
+        self._entity_attributes = attributes
+        self.fields = fields
+        self.custom_fields = custom_fields
+        self.slack_file = slack_file
+        self.display_order = display_order
+        self.actions = actions
+        self.additional_attributes = kwargs
+
+    @property
+    def entity_attributes(self) -> Union[Dict[str, Any], EntityAttributes]:
+        """Get the entity attributes data.
+
+        Note: Use this property to access the attributes data. The class-level
+        'attributes' is reserved for the JSON serialization schema.
+        """
+        return self._entity_attributes
+
+    @entity_attributes.setter
+    def entity_attributes(self, value: Union[Dict[str, Any], EntityAttributes]):
+        """Set the entity attributes data."""
+        self._entity_attributes = value
+
+    def get_object_attribute(self, key: str):
+        if key == "attributes":
+            return self._entity_attributes
+        else:
+            return getattr(self, key, None)
+
+    def __str__(self):
+        return str(self.get_non_null_attributes())
+
+    def __repr__(self):
+        return self.__str__()
+
+

Payload schema for an entity

+

Ancestors

+ +

Class variables

+
+
var attributes
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
prop entity_attributes : Dict[str, Any] | EntityAttributes
+
+
+ +Expand source code + +
@property
+def entity_attributes(self) -> Union[Dict[str, Any], EntityAttributes]:
+    """Get the entity attributes data.
+
+    Note: Use this property to access the attributes data. The class-level
+    'attributes' is reserved for the JSON serialization schema.
+    """
+    return self._entity_attributes
+
+

Get the entity attributes data.

+

Note: Use this property to access the attributes data. The class-level +'attributes' is reserved for the JSON serialization schema.

+
+
+

Methods

+
+
+def get_object_attribute(self, key: str) +
+
+
+ +Expand source code + +
def get_object_attribute(self, key: str):
+    if key == "attributes":
+        return self._entity_attributes
+    else:
+        return getattr(self, key, None)
+
+
+
+
+

Inherited members

+ +
+
+class EntityRefField +(entity_url: str,
external_ref: Dict[str, Any] | ExternalRef,
title: str,
display_type: str | None = None,
icon: Dict[str, Any] | EntityIconField | None = None,
**kwargs)
+
+
+
+ +Expand source code + +
class EntityRefField(JsonObject):
+    """Entity reference field"""
+
+    attributes = {
+        "entity_url",
+        "external_ref",
+        "title",
+        "display_type",
+        "icon",
+    }
+
+    def __init__(
+        self,
+        entity_url: str,
+        external_ref: Union[Dict[str, Any], ExternalRef],
+        title: str,
+        display_type: Optional[str] = None,
+        icon: Optional[Union[Dict[str, Any], EntityIconField]] = None,
+        **kwargs,
+    ):
+        self.entity_url = entity_url
+        self.external_ref = external_ref
+        self.title = title
+        self.display_type = display_type
+        self.icon = icon
+        self.additional_attributes = kwargs
+
+    def __str__(self):
+        return str(self.get_non_null_attributes())
+
+    def __repr__(self):
+        return self.__str__()
+
+

Entity reference field

+

Ancestors

+ +

Class variables

+
+
var attributes
+
+

The type of the None singleton.

+
+
+

Inherited members

+ +
+
+class EntityStringField +(value: str,
label: str | None = None,
format: str | None = None,
link: str | None = None,
icon: Dict[str, Any] | EntityIconField | None = None,
long: bool | None = None,
type: str | None = None,
tag_color: str | None = None,
edit: Dict[str, Any] | EntityEditSupport | None = None,
**kwargs)
+
+
+
+ +Expand source code + +
class EntityStringField(JsonObject):
+    """String field for entity"""
+
+    attributes = {
+        "value",
+        "label",
+        "format",
+        "link",
+        "icon",
+        "long",
+        "type",
+        "tag_color",
+        "edit",
+    }
+
+    def __init__(
+        self,
+        value: str,
+        label: Optional[str] = None,
+        format: Optional[str] = None,
+        link: Optional[str] = None,
+        icon: Optional[Union[Dict[str, Any], EntityIconField]] = None,
+        long: Optional[bool] = None,
+        type: Optional[str] = None,
+        tag_color: Optional[str] = None,
+        edit: Optional[Union[Dict[str, Any], EntityEditSupport]] = None,
+        **kwargs,
+    ):
+        self.value = value
+        self.label = label
+        self.format = format
+        self.link = link
+        self.icon = icon
+        self.long = long
+        self.type = type
+        self.tag_color = tag_color
+        self.edit = edit
+        self.additional_attributes = kwargs
+
+    def __str__(self):
+        return str(self.get_non_null_attributes())
+
+    def __repr__(self):
+        return self.__str__()
+
+

String field for entity

+

Ancestors

+ +

Class variables

+
+
var attributes
+
+

The type of the None singleton.

+
+
+

Inherited members

+ +
+
+class EntityTimestampField +(value: int,
label: str | None = None,
type: str | None = None,
edit: Dict[str, Any] | EntityEditSupport | None = None,
**kwargs)
+
+
+
+ +Expand source code + +
class EntityTimestampField(JsonObject):
+    """Timestamp field for entity"""
+
+    attributes = {
+        "value",
+        "label",
+        "type",
+        "edit",
+    }
+
+    def __init__(
+        self,
+        value: int,
+        label: Optional[str] = None,
+        type: Optional[str] = None,
+        edit: Optional[Union[Dict[str, Any], EntityEditSupport]] = None,
+        **kwargs,
+    ):
+        self.value = value
+        self.label = label
+        self.type = type
+        self.edit = edit
+        self.additional_attributes = kwargs
+
+    def __str__(self):
+        return str(self.get_non_null_attributes())
+
+    def __repr__(self):
+        return self.__str__()
+
+

Timestamp field for entity

+

Ancestors

+ +

Class variables

+
+
var attributes
+
+

The type of the None singleton.

+
+
+

Inherited members

+ +
+
+class EntityTitle +(text: str,
edit: Dict[str, Any] | EntityEditSupport | None = None,
**kwargs)
+
+
+
+ +Expand source code + +
class EntityTitle(JsonObject):
+    """Title for entity attributes"""
+
+    attributes = {
+        "text",
+        "edit",
+    }
+
+    def __init__(
+        self,
+        text: str,
+        edit: Optional[Union[Dict[str, Any], EntityEditSupport]] = None,
+        **kwargs,
+    ):
+        self.text = text
+        self.edit = edit
+        self.additional_attributes = kwargs
+
+    def __str__(self):
+        return str(self.get_non_null_attributes())
+
+    def __repr__(self):
+        return self.__str__()
+
+

Title for entity attributes

+

Ancestors

+ +

Class variables

+
+
var attributes
+
+

The type of the None singleton.

+
+
+

Inherited members

+ +
+
+class EntityTypedField +(type: str,
label: str | None = None,
value: str | int | None = None,
link: str | None = None,
icon: Dict[str, Any] | EntityIconField | None = None,
long: bool | None = None,
format: str | None = None,
image_url: str | None = None,
slack_file: Dict[str, Any] | None = None,
alt_text: str | None = None,
edit: Dict[str, Any] | EntityEditSupport | None = None,
tag_color: str | None = None,
user: Dict[str, Any] | EntityUserIDField | EntityUserField | None = None,
entity_ref: Dict[str, Any] | EntityRefField | None = None,
**kwargs)
+
+
+
+ +Expand source code + +
class EntityTypedField(JsonObject):
+    """Typed field for entity with various display options"""
+
+    attributes = {
+        "type",
+        "label",
+        "value",
+        "link",
+        "icon",
+        "long",
+        "format",
+        "image_url",
+        "slack_file",
+        "alt_text",
+        "edit",
+        "tag_color",
+        "user",
+        "entity_ref",
+    }
+
+    def __init__(
+        self,
+        type: str,
+        label: Optional[str] = None,
+        value: Optional[Union[str, int]] = None,
+        link: Optional[str] = None,
+        icon: Optional[Union[Dict[str, Any], EntityIconField]] = None,
+        long: Optional[bool] = None,
+        format: Optional[str] = None,
+        image_url: Optional[str] = None,
+        slack_file: Optional[Dict[str, Any]] = None,
+        alt_text: Optional[str] = None,
+        edit: Optional[Union[Dict[str, Any], EntityEditSupport]] = None,
+        tag_color: Optional[str] = None,
+        user: Optional[Union[Dict[str, Any], EntityUserIDField, EntityUserField]] = None,
+        entity_ref: Optional[Union[Dict[str, Any], EntityRefField]] = None,
+        **kwargs,
+    ):
+        self.type = type
+        self.label = label
+        self.value = value
+        self.link = link
+        self.icon = icon
+        self.long = long
+        self.format = format
+        self.image_url = image_url
+        self.slack_file = slack_file
+        self.alt_text = alt_text
+        self.edit = edit
+        self.tag_color = tag_color
+        self.user = user
+        self.entity_ref = entity_ref
+        self.additional_attributes = kwargs
+
+    def __str__(self):
+        return str(self.get_non_null_attributes())
+
+    def __repr__(self):
+        return self.__str__()
+
+

Typed field for entity with various display options

+

Ancestors

+ +

Class variables

+
+
var attributes
+
+

The type of the None singleton.

+
+
+

Inherited members

+ +
+
+class EntityUserField +(text: str,
url: str | None = None,
email: str | None = None,
icon: Dict[str, Any] | EntityIconField | None = None,
**kwargs)
+
+
+
+ +Expand source code + +
class EntityUserField(JsonObject):
+    """User field for entity"""
+
+    attributes = {
+        "text",
+        "url",
+        "email",
+        "icon",
+    }
+
+    def __init__(
+        self,
+        text: str,
+        url: Optional[str] = None,
+        email: Optional[str] = None,
+        icon: Optional[Union[Dict[str, Any], EntityIconField]] = None,
+        **kwargs,
+    ):
+        self.text = text
+        self.url = url
+        self.email = email
+        self.icon = icon
+        self.additional_attributes = kwargs
+
+    def __str__(self):
+        return str(self.get_non_null_attributes())
+
+    def __repr__(self):
+        return self.__str__()
+
+

User field for entity

+

Ancestors

+ +

Class variables

+
+
var attributes
+
+

The type of the None singleton.

+
+
+

Inherited members

+ +
+
+class EntityUserIDField +(user_id: str, **kwargs) +
+
+
+ +Expand source code + +
class EntityUserIDField(JsonObject):
+    """User ID field for entity"""
+
+    attributes = {
+        "user_id",
+    }
+
+    def __init__(
+        self,
+        user_id: str,
+        **kwargs,
+    ):
+        self.user_id = user_id
+        self.additional_attributes = kwargs
+
+    def __str__(self):
+        return str(self.get_non_null_attributes())
+
+    def __repr__(self):
+        return self.__str__()
+
+

User ID field for entity

+

Ancestors

+ +

Class variables

+
+
var attributes
+
+

The type of the None singleton.

+
+
+

Inherited members

+ +
+
+class EventAndEntityMetadata +(event_type: str | None = None,
event_payload: Dict[str, Any] | None = None,
entities: List[Dict[str, Any] | EntityMetadata] | None = None,
**kwargs)
+
+
+
+ +Expand source code + +
class EventAndEntityMetadata(JsonObject):
+    """Message metadata with entities
+
+    https://docs.slack.dev/messaging/message-metadata/
+    https://docs.slack.dev/messaging/work-objects/
+    """
+
+    attributes = {"event_type", "event_payload", "entities"}
+
+    def __init__(
+        self,
+        event_type: Optional[str] = None,
+        event_payload: Optional[Dict[str, Any]] = None,
+        entities: Optional[List[Union[Dict[str, Any], EntityMetadata]]] = None,
+        **kwargs,
+    ):
+        self.event_type = event_type
+        self.event_payload = event_payload
+        self.entities = entities
+        self.additional_attributes = kwargs
+
+    def __str__(self):
+        return str(self.get_non_null_attributes())
+
+    def __repr__(self):
+        return self.__str__()
+
+ +

Ancestors

+ +

Class variables

+
+
var attributes
+
+

The type of the None singleton.

+
+
+

Inherited members

+ +
+
+class ExternalRef +(id: str, type: str | None = None, **kwargs) +
+
+
+ +Expand source code + +
class ExternalRef(JsonObject):
+    """Reference (and optional type) used to identify an entity within the developer's system"""
+
+    attributes = {
+        "id",
+        "type",
+    }
+
+    def __init__(
+        self,
+        id: str,
+        type: Optional[str] = None,
+        **kwargs,
+    ):
+        self.id = id
+        self.type = type
+        self.additional_attributes = kwargs
+
+    def __str__(self):
+        return str(self.get_non_null_attributes())
+
+    def __repr__(self):
+        return self.__str__()
+
+

Reference (and optional type) used to identify an entity within the developer's system

+

Ancestors

+ +

Class variables

+
+
var attributes
+
+

The type of the None singleton.

+
+
+

Inherited members

+ +
+
+class FileEntityFields +(preview: Dict[str, Any] | EntityImageField | None = None,
created_by: Dict[str, Any] | EntityTypedField | None = None,
date_created: Dict[str, Any] | EntityTimestampField | None = None,
date_updated: Dict[str, Any] | EntityTimestampField | None = None,
last_modified_by: Dict[str, Any] | EntityTypedField | None = None,
file_size: Dict[str, Any] | EntityStringField | None = None,
mime_type: Dict[str, Any] | EntityStringField | None = None,
full_size_preview: Dict[str, Any] | EntityFullSizePreview | None = None,
**kwargs)
+
+
+
+ +Expand source code + +
class FileEntityFields(JsonObject):
+    """Fields specific to file entities"""
+
+    attributes = {
+        "preview",
+        "created_by",
+        "date_created",
+        "date_updated",
+        "last_modified_by",
+        "file_size",
+        "mime_type",
+        "full_size_preview",
+    }
+
+    def __init__(
+        self,
+        preview: Optional[Union[Dict[str, Any], EntityImageField]] = None,
+        created_by: Optional[Union[Dict[str, Any], EntityTypedField]] = None,
+        date_created: Optional[Union[Dict[str, Any], EntityTimestampField]] = None,
+        date_updated: Optional[Union[Dict[str, Any], EntityTimestampField]] = None,
+        last_modified_by: Optional[Union[Dict[str, Any], EntityTypedField]] = None,
+        file_size: Optional[Union[Dict[str, Any], EntityStringField]] = None,
+        mime_type: Optional[Union[Dict[str, Any], EntityStringField]] = None,
+        full_size_preview: Optional[Union[Dict[str, Any], EntityFullSizePreview]] = None,
+        **kwargs,
+    ):
+        self.preview = preview
+        self.created_by = created_by
+        self.date_created = date_created
+        self.date_updated = date_updated
+        self.last_modified_by = last_modified_by
+        self.file_size = file_size
+        self.mime_type = mime_type
+        self.full_size_preview = full_size_preview
+        self.additional_attributes = kwargs
+
+    def __str__(self):
+        return str(self.get_non_null_attributes())
+
+    def __repr__(self):
+        return self.__str__()
+
+

Fields specific to file entities

+

Ancestors

+ +

Class variables

+
+
var attributes
+
+

The type of the None singleton.

+
+
+

Inherited members

+ +
+
+class FileEntitySlackFile +(id: str, type: str | None = None, **kwargs) +
+
+
+ +Expand source code + +
class FileEntitySlackFile(JsonObject):
+    """Slack file reference for file entities"""
+
+    attributes = {
+        "id",
+        "type",
+    }
+
+    def __init__(
+        self,
+        id: str,
+        type: Optional[str] = None,
+        **kwargs,
+    ):
+        self.id = id
+        self.type = type
+        self.additional_attributes = kwargs
+
+    def __str__(self):
+        return str(self.get_non_null_attributes())
+
+    def __repr__(self):
+        return self.__str__()
+
+

Slack file reference for file entities

+

Ancestors

+ +

Class variables

+
+
var attributes
+
+

The type of the None singleton.

+
+
+

Inherited members

+ +
+
+class IncidentEntityFields +(status: Dict[str, Any] | EntityStringField | None = None,
priority: Dict[str, Any] | EntityStringField | None = None,
urgency: Dict[str, Any] | EntityStringField | None = None,
created_by: Dict[str, Any] | EntityTypedField | None = None,
assigned_to: Dict[str, Any] | EntityTypedField | None = None,
date_created: Dict[str, Any] | EntityTimestampField | None = None,
date_updated: Dict[str, Any] | EntityTimestampField | None = None,
description: Dict[str, Any] | EntityStringField | None = None,
service: Dict[str, Any] | EntityStringField | None = None,
**kwargs)
+
+
+
+ +Expand source code + +
class IncidentEntityFields(JsonObject):
+    """Fields specific to incident entities"""
+
+    attributes = {
+        "status",
+        "priority",
+        "urgency",
+        "created_by",
+        "assigned_to",
+        "date_created",
+        "date_updated",
+        "description",
+        "service",
+    }
+
+    def __init__(
+        self,
+        status: Optional[Union[Dict[str, Any], EntityStringField]] = None,
+        priority: Optional[Union[Dict[str, Any], EntityStringField]] = None,
+        urgency: Optional[Union[Dict[str, Any], EntityStringField]] = None,
+        created_by: Optional[Union[Dict[str, Any], EntityTypedField]] = None,
+        assigned_to: Optional[Union[Dict[str, Any], EntityTypedField]] = None,
+        date_created: Optional[Union[Dict[str, Any], EntityTimestampField]] = None,
+        date_updated: Optional[Union[Dict[str, Any], EntityTimestampField]] = None,
+        description: Optional[Union[Dict[str, Any], EntityStringField]] = None,
+        service: Optional[Union[Dict[str, Any], EntityStringField]] = None,
+        **kwargs,
+    ):
+        self.status = status
+        self.priority = priority
+        self.urgency = urgency
+        self.created_by = created_by
+        self.assigned_to = assigned_to
+        self.date_created = date_created
+        self.date_updated = date_updated
+        self.description = description
+        self.service = service
+        self.additional_attributes = kwargs
+
+    def __str__(self):
+        return str(self.get_non_null_attributes())
+
+    def __repr__(self):
+        return self.__str__()
+
+

Fields specific to incident entities

+

Ancestors

+ +

Class variables

+
+
var attributes
+
+

The type of the None singleton.

+
+
+

Inherited members

+ +
+
+class Metadata +(event_type: str, event_payload: Dict[str, Any], **kwargs) +
+
+
+ +Expand source code + +
class Metadata(JsonObject):
+    """Message metadata
+
+    https://docs.slack.dev/messaging/message-metadata/
+    """
+
+    attributes = {
+        "event_type",
+        "event_payload",
+    }
+
+    def __init__(
+        self,
+        event_type: str,
+        event_payload: Dict[str, Any],
+        **kwargs,
+    ):
+        self.event_type = event_type
+        self.event_payload = event_payload
+        self.additional_attributes = kwargs
+
+    def __str__(self):
+        return str(self.get_non_null_attributes())
+
+    def __repr__(self):
+        return self.__str__()
+
+ +

Ancestors

+ +

Class variables

+
+
var attributes
+
+

The type of the None singleton.

+
+
+

Inherited members

+ +
+
+class TaskEntityFields +(description: Dict[str, Any] | EntityStringField | None = None,
created_by: Dict[str, Any] | EntityTypedField | None = None,
date_created: Dict[str, Any] | EntityTimestampField | None = None,
date_updated: Dict[str, Any] | EntityTimestampField | None = None,
assignee: Dict[str, Any] | EntityTypedField | None = None,
status: Dict[str, Any] | EntityStringField | None = None,
due_date: Dict[str, Any] | EntityTypedField | None = None,
priority: Dict[str, Any] | EntityStringField | None = None,
**kwargs)
+
+
+
+ +Expand source code + +
class TaskEntityFields(JsonObject):
+    """Fields specific to task entities"""
+
+    attributes = {
+        "description",
+        "created_by",
+        "date_created",
+        "date_updated",
+        "assignee",
+        "status",
+        "due_date",
+        "priority",
+    }
+
+    def __init__(
+        self,
+        description: Optional[Union[Dict[str, Any], EntityStringField]] = None,
+        created_by: Optional[Union[Dict[str, Any], EntityTypedField]] = None,
+        date_created: Optional[Union[Dict[str, Any], EntityTimestampField]] = None,
+        date_updated: Optional[Union[Dict[str, Any], EntityTimestampField]] = None,
+        assignee: Optional[Union[Dict[str, Any], EntityTypedField]] = None,
+        status: Optional[Union[Dict[str, Any], EntityStringField]] = None,
+        due_date: Optional[Union[Dict[str, Any], EntityTypedField]] = None,
+        priority: Optional[Union[Dict[str, Any], EntityStringField]] = None,
+        **kwargs,
+    ):
+        self.description = description
+        self.created_by = created_by
+        self.date_created = date_created
+        self.date_updated = date_updated
+        self.assignee = assignee
+        self.status = status
+        self.due_date = due_date
+        self.priority = priority
+        self.additional_attributes = kwargs
+
+    def __str__(self):
+        return str(self.get_non_null_attributes())
+
+    def __repr__(self):
+        return self.__str__()
+
+

Fields specific to task entities

+

Ancestors

+ +

Class variables

+
+
var attributes
+
+

The type of the None singleton.

+
+
+

Inherited members

+ +
+
+
+
+ +
+ + + diff --git a/docs/reference/models/views/index.html b/docs/reference/models/views/index.html new file mode 100644 index 000000000..d9e60359b --- /dev/null +++ b/docs/reference/models/views/index.html @@ -0,0 +1,454 @@ + + + + + + +slack_sdk.models.views API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.models.views

+
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class View +(type: str,
id: str | None = None,
callback_id: str | None = None,
external_id: str | None = None,
team_id: str | None = None,
bot_id: str | None = None,
app_id: str | None = None,
root_view_id: str | None = None,
previous_view_id: str | None = None,
title: str | dict | PlainTextObject | None = None,
submit: str | dict | PlainTextObject | None = None,
close: str | dict | PlainTextObject | None = None,
blocks: Sequence[dict | Block] | None = None,
private_metadata: str | None = None,
state: dict | ForwardRef('ViewState') | None = None,
hash: str | None = None,
clear_on_close: bool | None = None,
notify_on_close: bool | None = None,
**kwargs)
+
+
+
+ +Expand source code + +
class View(JsonObject):
+    """View object for modals and Home tabs.
+
+    https://docs.slack.dev/reference/views/
+    """
+
+    types = ["modal", "home", "workflow_step"]
+
+    attributes = {
+        "type",
+        "id",
+        "callback_id",
+        "external_id",
+        "team_id",
+        "bot_id",
+        "app_id",
+        "root_view_id",
+        "previous_view_id",
+        "title",
+        "submit",
+        "close",
+        "blocks",
+        "private_metadata",
+        "state",
+        "hash",
+        "clear_on_close",
+        "notify_on_close",
+    }
+
+    def __init__(
+        self,
+        # "modal", "home", and "workflow_step"
+        type: str,
+        id: Optional[str] = None,
+        callback_id: Optional[str] = None,
+        external_id: Optional[str] = None,
+        team_id: Optional[str] = None,
+        bot_id: Optional[str] = None,
+        app_id: Optional[str] = None,
+        root_view_id: Optional[str] = None,
+        previous_view_id: Optional[str] = None,
+        title: Optional[Union[str, dict, PlainTextObject]] = None,
+        submit: Optional[Union[str, dict, PlainTextObject]] = None,
+        close: Optional[Union[str, dict, PlainTextObject]] = None,
+        blocks: Optional[Sequence[Union[dict, Block]]] = None,
+        private_metadata: Optional[str] = None,
+        state: Optional[Union[dict, "ViewState"]] = None,
+        hash: Optional[str] = None,
+        clear_on_close: Optional[bool] = None,
+        notify_on_close: Optional[bool] = None,
+        **kwargs,
+    ):
+        self.type = type
+        self.id = id
+        self.callback_id = callback_id
+        self.external_id = external_id
+        self.team_id = team_id
+        self.bot_id = bot_id
+        self.app_id = app_id
+        self.root_view_id = root_view_id
+        self.previous_view_id = previous_view_id
+        self.title = TextObject.parse(title, default_type=PlainTextObject.type)  # type: ignore[arg-type]
+        self.submit = TextObject.parse(submit, default_type=PlainTextObject.type)  # type: ignore[arg-type]
+        self.close = TextObject.parse(close, default_type=PlainTextObject.type)  # type: ignore[arg-type]
+        self.blocks = Block.parse_all(blocks)
+        self.private_metadata = private_metadata
+        self.state = state
+        if self.state is not None and isinstance(self.state, dict):
+            self.state = ViewState(**self.state)
+        self.hash = hash
+        self.clear_on_close = clear_on_close
+        self.notify_on_close = notify_on_close
+        self.additional_attributes = kwargs
+
+    title_max_length = 24
+    blocks_max_length = 100
+    close_max_length = 24
+    submit_max_length = 24
+    private_metadata_max_length = 3000
+    callback_id_max_length: int = 255
+
+    @JsonValidator('type must be either "modal", "home" or "workflow_step"')
+    def _validate_type(self):
+        return self.type is not None and self.type in self.types
+
+    @JsonValidator(f"title must be between 1 and {title_max_length} characters")
+    def _validate_title_length(self):
+        return self.title is None or 1 <= len(self.title.text) <= self.title_max_length
+
+    @JsonValidator(f"views must contain between 1 and {blocks_max_length} blocks")
+    def _validate_blocks_length(self):
+        return self.blocks is None or 0 < len(self.blocks) <= self.blocks_max_length
+
+    @JsonValidator("home view cannot have submit and close")
+    def _validate_home_tab_structure(self):
+        return self.type != "home" or (self.type == "home" and self.close is None and self.submit is None)
+
+    @JsonValidator(f"close cannot exceed {close_max_length} characters")
+    def _validate_close_length(self):
+        return self.close is None or len(self.close.text) <= self.close_max_length
+
+    @JsonValidator(f"submit cannot exceed {submit_max_length} characters")
+    def _validate_submit_length(self):
+        return self.submit is None or len(self.submit.text) <= int(self.submit_max_length)
+
+    @JsonValidator(f"private_metadata cannot exceed {private_metadata_max_length} characters")
+    def _validate_private_metadata_max_length(self):
+        return self.private_metadata is None or len(self.private_metadata) <= self.private_metadata_max_length
+
+    @JsonValidator(f"callback_id cannot exceed {callback_id_max_length} characters")
+    def _validate_callback_id_max_length(self):
+        return self.callback_id is None or len(self.callback_id) <= self.callback_id_max_length
+
+    def __str__(self):
+        return str(self.get_non_null_attributes())
+
+    def __repr__(self):
+        return self.__str__()
+
+

View object for modals and Home tabs.

+

https://docs.slack.dev/reference/views/

+

Ancestors

+ +

Class variables

+
+
var attributes
+
+

The type of the None singleton.

+
+
var blocks_max_length
+
+

The type of the None singleton.

+
+
var callback_id_max_length : int
+
+

The type of the None singleton.

+
+
var close_max_length
+
+

The type of the None singleton.

+
+
var private_metadata_max_length
+
+

The type of the None singleton.

+
+
var submit_max_length
+
+

The type of the None singleton.

+
+
var title_max_length
+
+

The type of the None singleton.

+
+
var types
+
+

The type of the None singleton.

+
+
+

Inherited members

+ +
+
+class ViewState +(*,
values: Dict[str, Dict[str, dict | ForwardRef('ViewStateValue')]])
+
+
+
+ +Expand source code + +
class ViewState(JsonObject):
+    attributes = {"values"}
+    logger = logging.getLogger(__name__)
+
+    @classmethod
+    def _show_warning_about_unknown(cls, value):
+        c = value.__class__
+        name = ".".join([c.__module__, c.__name__])
+        cls.logger.warning(f"Unknown type for view.state.values detected ({name}) and ViewState skipped to add it")
+
+    def __init__(
+        self,
+        *,
+        values: Dict[str, Dict[str, Union[dict, "ViewStateValue"]]],
+    ):
+        value_objects: Dict[str, Dict[str, ViewStateValue]] = {}
+        new_state_values = copy.copy(values)
+        if isinstance(new_state_values, dict):  # just in case
+            for block_id, actions in new_state_values.items():
+                if actions is None:
+                    continue
+                elif isinstance(actions, dict):
+                    new_actions: Dict[str, Union[ViewStateValue, dict]] = copy.copy(actions)
+                    for action_id, v in actions.items():
+                        if isinstance(v, dict):
+                            d = copy.copy(v)
+                            value_object = ViewStateValue(**d)
+                        elif isinstance(v, ViewStateValue):
+                            value_object = v
+                        else:
+                            self._show_warning_about_unknown(v)
+                            continue
+                        new_actions[action_id] = value_object
+                    value_objects[block_id] = new_actions  # type: ignore[assignment]
+                else:
+                    self._show_warning_about_unknown(v)
+        self.values = value_objects
+
+    def to_dict(self, *args) -> Dict[str, Dict[str, Dict[str, dict]]]:
+        self.validate_json()
+        if self.values is not None:
+            dict_values: Dict[str, Dict[str, dict]] = {}
+            for block_id, actions in self.values.items():
+                if actions:
+                    dict_value: Dict[str, dict] = {action_id: value.to_dict() for action_id, value in actions.items()}
+                    dict_values[block_id] = dict_value
+            return {"values": dict_values}
+        else:
+            return {}
+
+

The base class for JSON serializable class objects

+

Ancestors

+ +

Class variables

+
+
var attributes
+
+

The type of the None singleton.

+
+
var logger
+
+

The type of the None singleton.

+
+
+

Inherited members

+ +
+
+class ViewStateValue +(*,
type: str | None = None,
value: str | None = None,
selected_date: str | None = None,
selected_time: str | None = None,
selected_conversation: str | None = None,
selected_channel: str | None = None,
selected_user: str | None = None,
selected_option: dict | Option | None = None,
selected_conversations: Sequence[str] | None = None,
selected_channels: Sequence[str] | None = None,
selected_users: Sequence[str] | None = None,
selected_options: Sequence[dict | Option] | None = None)
+
+
+
+ +Expand source code + +
class ViewStateValue(JsonObject):
+    attributes = {
+        "type",
+        "value",
+        "selected_date",
+        "selected_time",
+        "selected_conversation",
+        "selected_channel",
+        "selected_user",
+        "selected_option",
+        "selected_conversations",
+        "selected_channels",
+        "selected_users",
+        "selected_options",
+    }
+
+    def __init__(
+        self,
+        *,
+        type: Optional[str] = None,
+        value: Optional[str] = None,
+        selected_date: Optional[str] = None,
+        selected_time: Optional[str] = None,
+        selected_conversation: Optional[str] = None,
+        selected_channel: Optional[str] = None,
+        selected_user: Optional[str] = None,
+        selected_option: Optional[Union[dict, Option]] = None,
+        selected_conversations: Optional[Sequence[str]] = None,
+        selected_channels: Optional[Sequence[str]] = None,
+        selected_users: Optional[Sequence[str]] = None,
+        selected_options: Optional[Sequence[Union[dict, Option]]] = None,
+    ):
+        self.type = type
+        self.value = value
+        self.selected_date = selected_date
+        self.selected_time = selected_time
+        self.selected_conversation = selected_conversation
+        self.selected_channel = selected_channel
+        self.selected_user = selected_user
+        self.selected_option = selected_option
+        self.selected_conversations = selected_conversations
+        self.selected_channels = selected_channels
+        self.selected_users = selected_users
+
+        if isinstance(selected_options, list):
+            self.selected_options = []
+            for option in selected_options:
+                if isinstance(option, Option):
+                    self.selected_options.append(option)
+                elif isinstance(option, dict):
+                    self.selected_options.append(Option(**option))
+        else:
+            self.selected_options = selected_options  # type: ignore[assignment]
+
+

The base class for JSON serializable class objects

+

Ancestors

+ +

Class variables

+
+
var attributes
+
+

The type of the None singleton.

+
+
+

Inherited members

+ +
+
+
+
+ +
+ + + diff --git a/docs/reference/oauth/authorize_url_generator/index.html b/docs/reference/oauth/authorize_url_generator/index.html new file mode 100644 index 000000000..97e893156 --- /dev/null +++ b/docs/reference/oauth/authorize_url_generator/index.html @@ -0,0 +1,229 @@ + + + + + + +slack_sdk.oauth.authorize_url_generator API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.oauth.authorize_url_generator

+
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class AuthorizeUrlGenerator +(*,
client_id: str,
redirect_uri: str | None = None,
scopes: Sequence[str] | None = None,
user_scopes: Sequence[str] | None = None,
authorization_url: str = 'https://slack.com/oauth/v2/authorize')
+
+
+
+ +Expand source code + +
class AuthorizeUrlGenerator:
+    def __init__(
+        self,
+        *,
+        client_id: str,
+        redirect_uri: Optional[str] = None,
+        scopes: Optional[Sequence[str]] = None,
+        user_scopes: Optional[Sequence[str]] = None,
+        authorization_url: str = "https://slack.com/oauth/v2/authorize",
+    ):
+        self.client_id = client_id
+        self.redirect_uri = redirect_uri
+        self.scopes = scopes
+        self.user_scopes = user_scopes
+        self.authorization_url = authorization_url
+
+    def generate(self, state: str, team: Optional[str] = None) -> str:
+        scopes = ",".join(self.scopes) if self.scopes else ""
+        user_scopes = ",".join(self.user_scopes) if self.user_scopes else ""
+        url = (
+            f"{self.authorization_url}?"
+            f"state={state}&"
+            f"client_id={self.client_id}&"
+            f"scope={scopes}&"
+            f"user_scope={user_scopes}"
+        )
+        if self.redirect_uri is not None:
+            url += f"&redirect_uri={self.redirect_uri}"
+        if team is not None:
+            url += f"&team={team}"
+        return url
+
+
+

Methods

+
+
+def generate(self, state: str, team: str | None = None) ‑> str +
+
+
+ +Expand source code + +
def generate(self, state: str, team: Optional[str] = None) -> str:
+    scopes = ",".join(self.scopes) if self.scopes else ""
+    user_scopes = ",".join(self.user_scopes) if self.user_scopes else ""
+    url = (
+        f"{self.authorization_url}?"
+        f"state={state}&"
+        f"client_id={self.client_id}&"
+        f"scope={scopes}&"
+        f"user_scope={user_scopes}"
+    )
+    if self.redirect_uri is not None:
+        url += f"&redirect_uri={self.redirect_uri}"
+    if team is not None:
+        url += f"&team={team}"
+    return url
+
+
+
+
+
+
+class OpenIDConnectAuthorizeUrlGenerator +(*,
client_id: str,
redirect_uri: str,
scopes: Sequence[str] | None = None,
authorization_url: str = 'https://slack.com/openid/connect/authorize')
+
+
+
+ +Expand source code + +
class OpenIDConnectAuthorizeUrlGenerator:
+    """Refer to https://openid.net/specs/openid-connect-core-1_0.html"""
+
+    def __init__(
+        self,
+        *,
+        client_id: str,
+        redirect_uri: str,
+        scopes: Optional[Sequence[str]] = None,
+        authorization_url: str = "https://slack.com/openid/connect/authorize",
+    ):
+        self.client_id = client_id
+        self.redirect_uri = redirect_uri
+        self.scopes = scopes
+        self.authorization_url = authorization_url
+
+    def generate(self, state: str, nonce: Optional[str] = None, team: Optional[str] = None) -> str:
+        scopes = ",".join(self.scopes) if self.scopes else ""
+        url = (
+            f"{self.authorization_url}?"
+            "response_type=code&"
+            f"state={state}&"
+            f"client_id={self.client_id}&"
+            f"scope={scopes}&"
+            f"redirect_uri={self.redirect_uri}"
+        )
+        if team is not None:
+            url += f"&team={team}"
+        if nonce is not None:
+            url += f"&nonce={nonce}"
+        return url
+
+ +

Methods

+
+
+def generate(self, state: str, nonce: str | None = None, team: str | None = None) ‑> str +
+
+
+ +Expand source code + +
def generate(self, state: str, nonce: Optional[str] = None, team: Optional[str] = None) -> str:
+    scopes = ",".join(self.scopes) if self.scopes else ""
+    url = (
+        f"{self.authorization_url}?"
+        "response_type=code&"
+        f"state={state}&"
+        f"client_id={self.client_id}&"
+        f"scope={scopes}&"
+        f"redirect_uri={self.redirect_uri}"
+    )
+    if team is not None:
+        url += f"&team={team}"
+    if nonce is not None:
+        url += f"&nonce={nonce}"
+    return url
+
+
+
+
+
+
+
+
+ +
+ + + diff --git a/docs/reference/oauth/index.html b/docs/reference/oauth/index.html new file mode 100644 index 000000000..8fe69e5d0 --- /dev/null +++ b/docs/reference/oauth/index.html @@ -0,0 +1,936 @@ + + + + + + +slack_sdk.oauth API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.oauth

+
+
+

Modules for implementing the Slack OAuth flow

+

https://docs.slack.dev/tools/python-slack-sdk/oauth

+
+
+

Sub-modules

+
+
slack_sdk.oauth.authorize_url_generator
+
+
+
+
slack_sdk.oauth.installation_store
+
+
+
+
slack_sdk.oauth.redirect_uri_page_renderer
+
+
+
+
slack_sdk.oauth.state_store
+
+

OAuth state parameter data store …

+
+
slack_sdk.oauth.state_utils
+
+
+
+
slack_sdk.oauth.token_rotation
+
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class AuthorizeUrlGenerator +(*,
client_id: str,
redirect_uri: str | None = None,
scopes: Sequence[str] | None = None,
user_scopes: Sequence[str] | None = None,
authorization_url: str = 'https://slack.com/oauth/v2/authorize')
+
+
+
+ +Expand source code + +
class AuthorizeUrlGenerator:
+    def __init__(
+        self,
+        *,
+        client_id: str,
+        redirect_uri: Optional[str] = None,
+        scopes: Optional[Sequence[str]] = None,
+        user_scopes: Optional[Sequence[str]] = None,
+        authorization_url: str = "https://slack.com/oauth/v2/authorize",
+    ):
+        self.client_id = client_id
+        self.redirect_uri = redirect_uri
+        self.scopes = scopes
+        self.user_scopes = user_scopes
+        self.authorization_url = authorization_url
+
+    def generate(self, state: str, team: Optional[str] = None) -> str:
+        scopes = ",".join(self.scopes) if self.scopes else ""
+        user_scopes = ",".join(self.user_scopes) if self.user_scopes else ""
+        url = (
+            f"{self.authorization_url}?"
+            f"state={state}&"
+            f"client_id={self.client_id}&"
+            f"scope={scopes}&"
+            f"user_scope={user_scopes}"
+        )
+        if self.redirect_uri is not None:
+            url += f"&redirect_uri={self.redirect_uri}"
+        if team is not None:
+            url += f"&team={team}"
+        return url
+
+
+

Methods

+
+
+def generate(self, state: str, team: str | None = None) ‑> str +
+
+
+ +Expand source code + +
def generate(self, state: str, team: Optional[str] = None) -> str:
+    scopes = ",".join(self.scopes) if self.scopes else ""
+    user_scopes = ",".join(self.user_scopes) if self.user_scopes else ""
+    url = (
+        f"{self.authorization_url}?"
+        f"state={state}&"
+        f"client_id={self.client_id}&"
+        f"scope={scopes}&"
+        f"user_scope={user_scopes}"
+    )
+    if self.redirect_uri is not None:
+        url += f"&redirect_uri={self.redirect_uri}"
+    if team is not None:
+        url += f"&team={team}"
+    return url
+
+
+
+
+
+
+class InstallationStore +
+
+
+ +Expand source code + +
class InstallationStore:
+    """The installation store interface.
+
+    The minimum required methods are:
+
+    * save(installation)
+    * find_installation(enterprise_id, team_id, user_id, is_enterprise_install)
+
+    If you would like to properly handle app uninstallations and token revocations,
+    the following methods should be implemented.
+
+    * delete_installation(enterprise_id, team_id, user_id)
+    * delete_all(enterprise_id, team_id)
+
+    If your app needs only bot scope installations, the simpler way to implement would be:
+
+    * save(installation)
+    * find_bot(enterprise_id, team_id, is_enterprise_install)
+    * delete_bot(enterprise_id, team_id)
+    * delete_all(enterprise_id, team_id)
+    """
+
+    @property
+    def logger(self) -> Logger:
+        raise NotImplementedError()
+
+    def save(self, installation: Installation):
+        """Saves an installation data"""
+        raise NotImplementedError()
+
+    def save_bot(self, bot: Bot):
+        """Saves a bot installation data"""
+        raise NotImplementedError()
+
+    def find_bot(
+        self,
+        *,
+        enterprise_id: Optional[str],
+        team_id: Optional[str],
+        is_enterprise_install: Optional[bool] = False,
+    ) -> Optional[Bot]:
+        """Finds a bot scope installation per workspace / org"""
+        raise NotImplementedError()
+
+    def find_installation(
+        self,
+        *,
+        enterprise_id: Optional[str],
+        team_id: Optional[str],
+        user_id: Optional[str] = None,
+        is_enterprise_install: Optional[bool] = False,
+    ) -> Optional[Installation]:
+        """Finds a relevant installation for the given IDs.
+        If the user_id is absent, this method may return the latest installation in the workspace / org.
+        """
+        raise NotImplementedError()
+
+    def delete_bot(
+        self,
+        *,
+        enterprise_id: Optional[str],
+        team_id: Optional[str],
+    ) -> None:
+        """Deletes a bot scope installation per workspace / org"""
+        raise NotImplementedError()
+
+    def delete_installation(
+        self,
+        *,
+        enterprise_id: Optional[str],
+        team_id: Optional[str],
+        user_id: Optional[str] = None,
+    ) -> None:
+        """Deletes an installation that matches the given IDs"""
+        raise NotImplementedError()
+
+    def delete_all(
+        self,
+        *,
+        enterprise_id: Optional[str],
+        team_id: Optional[str],
+    ):
+        """Deletes all installation data for the given workspace / org"""
+        self.delete_bot(enterprise_id=enterprise_id, team_id=team_id)
+        self.delete_installation(enterprise_id=enterprise_id, team_id=team_id)
+
+

The installation store interface.

+

The minimum required methods are:

+
    +
  • save(installation)
  • +
  • find_installation(enterprise_id, team_id, user_id, is_enterprise_install)
  • +
+

If you would like to properly handle app uninstallations and token revocations, +the following methods should be implemented.

+
    +
  • delete_installation(enterprise_id, team_id, user_id)
  • +
  • delete_all(enterprise_id, team_id)
  • +
+

If your app needs only bot scope installations, the simpler way to implement would be:

+
    +
  • save(installation)
  • +
  • find_bot(enterprise_id, team_id, is_enterprise_install)
  • +
  • delete_bot(enterprise_id, team_id)
  • +
  • delete_all(enterprise_id, team_id)
  • +
+

Subclasses

+ +

Instance variables

+
+
prop logger : logging.Logger
+
+
+ +Expand source code + +
@property
+def logger(self) -> Logger:
+    raise NotImplementedError()
+
+
+
+
+

Methods

+
+
+def delete_all(self, *, enterprise_id: str | None, team_id: str | None) +
+
+
+ +Expand source code + +
def delete_all(
+    self,
+    *,
+    enterprise_id: Optional[str],
+    team_id: Optional[str],
+):
+    """Deletes all installation data for the given workspace / org"""
+    self.delete_bot(enterprise_id=enterprise_id, team_id=team_id)
+    self.delete_installation(enterprise_id=enterprise_id, team_id=team_id)
+
+

Deletes all installation data for the given workspace / org

+
+
+def delete_bot(self, *, enterprise_id: str | None, team_id: str | None) ‑> None +
+
+
+ +Expand source code + +
def delete_bot(
+    self,
+    *,
+    enterprise_id: Optional[str],
+    team_id: Optional[str],
+) -> None:
+    """Deletes a bot scope installation per workspace / org"""
+    raise NotImplementedError()
+
+

Deletes a bot scope installation per workspace / org

+
+
+def delete_installation(self, *, enterprise_id: str | None, team_id: str | None, user_id: str | None = None) ‑> None +
+
+
+ +Expand source code + +
def delete_installation(
+    self,
+    *,
+    enterprise_id: Optional[str],
+    team_id: Optional[str],
+    user_id: Optional[str] = None,
+) -> None:
+    """Deletes an installation that matches the given IDs"""
+    raise NotImplementedError()
+
+

Deletes an installation that matches the given IDs

+
+
+def find_bot(self,
*,
enterprise_id: str | None,
team_id: str | None,
is_enterprise_install: bool | None = False) ‑> Bot | None
+
+
+
+ +Expand source code + +
def find_bot(
+    self,
+    *,
+    enterprise_id: Optional[str],
+    team_id: Optional[str],
+    is_enterprise_install: Optional[bool] = False,
+) -> Optional[Bot]:
+    """Finds a bot scope installation per workspace / org"""
+    raise NotImplementedError()
+
+

Finds a bot scope installation per workspace / org

+
+
+def find_installation(self,
*,
enterprise_id: str | None,
team_id: str | None,
user_id: str | None = None,
is_enterprise_install: bool | None = False) ‑> Installation | None
+
+
+
+ +Expand source code + +
def find_installation(
+    self,
+    *,
+    enterprise_id: Optional[str],
+    team_id: Optional[str],
+    user_id: Optional[str] = None,
+    is_enterprise_install: Optional[bool] = False,
+) -> Optional[Installation]:
+    """Finds a relevant installation for the given IDs.
+    If the user_id is absent, this method may return the latest installation in the workspace / org.
+    """
+    raise NotImplementedError()
+
+

Finds a relevant installation for the given IDs. +If the user_id is absent, this method may return the latest installation in the workspace / org.

+
+
+def save(self,
installation: Installation)
+
+
+
+ +Expand source code + +
def save(self, installation: Installation):
+    """Saves an installation data"""
+    raise NotImplementedError()
+
+

Saves an installation data

+
+
+def save_bot(self,
bot: Bot)
+
+
+
+ +Expand source code + +
def save_bot(self, bot: Bot):
+    """Saves a bot installation data"""
+    raise NotImplementedError()
+
+

Saves a bot installation data

+
+
+
+
+class OAuthStateStore +
+
+
+ +Expand source code + +
class OAuthStateStore:
+    @property
+    def logger(self) -> Logger:
+        raise NotImplementedError()
+
+    def issue(self, *args, **kwargs) -> str:
+        raise NotImplementedError()
+
+    def consume(self, state: str) -> bool:
+        raise NotImplementedError()
+
+
+

Subclasses

+ +

Instance variables

+
+
prop logger : logging.Logger
+
+
+ +Expand source code + +
@property
+def logger(self) -> Logger:
+    raise NotImplementedError()
+
+
+
+
+

Methods

+
+
+def consume(self, state: str) ‑> bool +
+
+
+ +Expand source code + +
def consume(self, state: str) -> bool:
+    raise NotImplementedError()
+
+
+
+
+def issue(self, *args, **kwargs) ‑> str +
+
+
+ +Expand source code + +
def issue(self, *args, **kwargs) -> str:
+    raise NotImplementedError()
+
+
+
+
+
+
+class OAuthStateUtils +(*, cookie_name: str = 'slack-app-oauth-state', expiration_seconds: int = 600) +
+
+
+ +Expand source code + +
class OAuthStateUtils:
+    cookie_name: str
+    expiration_seconds: int
+
+    default_cookie_name: str = "slack-app-oauth-state"
+    default_expiration_seconds: int = 60 * 10  # 10 minutes
+
+    def __init__(
+        self,
+        *,
+        cookie_name: str = default_cookie_name,
+        expiration_seconds: int = default_expiration_seconds,
+    ):
+        self.cookie_name = cookie_name
+        self.expiration_seconds = expiration_seconds
+
+    def build_set_cookie_for_new_state(self, state: str) -> str:
+        return f"{self.cookie_name}={state}; " "Secure; " "HttpOnly; " "Path=/; " f"Max-Age={self.expiration_seconds}"
+
+    def build_set_cookie_for_deletion(self) -> str:
+        return f"{self.cookie_name}=deleted; " "Secure; " "HttpOnly; " "Path=/; " "Expires=Thu, 01 Jan 1970 00:00:00 GMT"
+
+    def is_valid_browser(
+        self,
+        state: Optional[str],
+        request_headers: Dict[str, Union[str, Sequence[str]]],
+    ) -> bool:
+        if state is None or request_headers is None or request_headers.get("cookie", None) is None:
+            return False
+        cookies = request_headers["cookie"]
+        if isinstance(cookies, str):
+            cookies = [cookies]
+        for cookie in cookies:
+            values = cookie.split(";")
+            for value in values:
+                # handle quoted cookie values (e.g. due to base64 encoding)
+                if value.strip().replace('"', "").replace("'", "") == f"{self.cookie_name}={state}":
+                    return True
+        return False
+
+
+

Class variables

+
+
var cookie_name : str
+
+

The type of the None singleton.

+
+ +
+

The type of the None singleton.

+
+
var default_expiration_seconds : int
+
+

The type of the None singleton.

+
+
var expiration_seconds : int
+
+

The type of the None singleton.

+
+
+

Methods

+
+ +
+
+ +Expand source code + +
def build_set_cookie_for_deletion(self) -> str:
+    return f"{self.cookie_name}=deleted; " "Secure; " "HttpOnly; " "Path=/; " "Expires=Thu, 01 Jan 1970 00:00:00 GMT"
+
+
+
+ +
+
+ +Expand source code + +
def build_set_cookie_for_new_state(self, state: str) -> str:
+    return f"{self.cookie_name}={state}; " "Secure; " "HttpOnly; " "Path=/; " f"Max-Age={self.expiration_seconds}"
+
+
+
+
+def is_valid_browser(self, state: str | None, request_headers: Dict[str, str | Sequence[str]]) ‑> bool +
+
+
+ +Expand source code + +
def is_valid_browser(
+    self,
+    state: Optional[str],
+    request_headers: Dict[str, Union[str, Sequence[str]]],
+) -> bool:
+    if state is None or request_headers is None or request_headers.get("cookie", None) is None:
+        return False
+    cookies = request_headers["cookie"]
+    if isinstance(cookies, str):
+        cookies = [cookies]
+    for cookie in cookies:
+        values = cookie.split(";")
+        for value in values:
+            # handle quoted cookie values (e.g. due to base64 encoding)
+            if value.strip().replace('"', "").replace("'", "") == f"{self.cookie_name}={state}":
+                return True
+    return False
+
+
+
+
+
+
+class OpenIDConnectAuthorizeUrlGenerator +(*,
client_id: str,
redirect_uri: str,
scopes: Sequence[str] | None = None,
authorization_url: str = 'https://slack.com/openid/connect/authorize')
+
+
+
+ +Expand source code + +
class OpenIDConnectAuthorizeUrlGenerator:
+    """Refer to https://openid.net/specs/openid-connect-core-1_0.html"""
+
+    def __init__(
+        self,
+        *,
+        client_id: str,
+        redirect_uri: str,
+        scopes: Optional[Sequence[str]] = None,
+        authorization_url: str = "https://slack.com/openid/connect/authorize",
+    ):
+        self.client_id = client_id
+        self.redirect_uri = redirect_uri
+        self.scopes = scopes
+        self.authorization_url = authorization_url
+
+    def generate(self, state: str, nonce: Optional[str] = None, team: Optional[str] = None) -> str:
+        scopes = ",".join(self.scopes) if self.scopes else ""
+        url = (
+            f"{self.authorization_url}?"
+            "response_type=code&"
+            f"state={state}&"
+            f"client_id={self.client_id}&"
+            f"scope={scopes}&"
+            f"redirect_uri={self.redirect_uri}"
+        )
+        if team is not None:
+            url += f"&team={team}"
+        if nonce is not None:
+            url += f"&nonce={nonce}"
+        return url
+
+ +

Methods

+
+
+def generate(self, state: str, nonce: str | None = None, team: str | None = None) ‑> str +
+
+
+ +Expand source code + +
def generate(self, state: str, nonce: Optional[str] = None, team: Optional[str] = None) -> str:
+    scopes = ",".join(self.scopes) if self.scopes else ""
+    url = (
+        f"{self.authorization_url}?"
+        "response_type=code&"
+        f"state={state}&"
+        f"client_id={self.client_id}&"
+        f"scope={scopes}&"
+        f"redirect_uri={self.redirect_uri}"
+    )
+    if team is not None:
+        url += f"&team={team}"
+    if nonce is not None:
+        url += f"&nonce={nonce}"
+    return url
+
+
+
+
+
+
+class RedirectUriPageRenderer +(*,
install_path: str,
redirect_uri_path: str,
success_url: str | None = None,
failure_url: str | None = None)
+
+
+
+ +Expand source code + +
class RedirectUriPageRenderer:
+    def __init__(
+        self,
+        *,
+        install_path: str,
+        redirect_uri_path: str,
+        success_url: Optional[str] = None,
+        failure_url: Optional[str] = None,
+    ):
+        self.install_path = install_path
+        self.redirect_uri_path = redirect_uri_path
+        self.success_url = success_url
+        self.failure_url = failure_url
+
+    def render_success_page(
+        self,
+        app_id: str,
+        team_id: Optional[str],
+        is_enterprise_install: Optional[bool] = None,
+        enterprise_url: Optional[str] = None,
+    ) -> str:
+        url = self.success_url
+        if url is None:
+            if is_enterprise_install is True and enterprise_url is not None and app_id is not None:
+                url = f"{enterprise_url}manage/organization/apps/profile/{app_id}/workspaces/add"
+            elif team_id is None or app_id is None:
+                url = "slack://open"
+            else:
+                url = f"slack://app?team={team_id}&id={app_id}"
+        browser_url = f"https://app.slack.com/client/{team_id}"
+
+        return f"""
+<html>
+<head>
+<meta http-equiv="refresh" content="0; URL={html.escape(url)}">
+<style>
+body {{
+  padding: 10px 15px;
+  font-family: verdana;
+  text-align: center;
+}}
+</style>
+</head>
+<body>
+<h2>Thank you!</h2>
+<p>Redirecting to the Slack App... click <a href="{html.escape(url)}">here</a>. If you use the browser version of Slack, click <a href="{html.escape(browser_url)}" target="_blank">this link</a> instead.</p>
+</body>
+</html>
+"""  # noqa: E501
+
+    def render_failure_page(self, reason: str) -> str:
+        return f"""
+<html>
+<head>
+<style>
+body {{
+  padding: 10px 15px;
+  font-family: verdana;
+  text-align: center;
+}}
+</style>
+</head>
+<body>
+<h2>Oops, Something Went Wrong!</h2>
+<p>Please try again from <a href="{html.escape(self.install_path)}">here</a> or contact the app owner (reason: {html.escape(reason)})</p>
+</body>
+</html>
+"""  # noqa: E501
+
+
+

Methods

+
+
+def render_failure_page(self, reason: str) ‑> str +
+
+
+ +Expand source code + +
    def render_failure_page(self, reason: str) -> str:
+        return f"""
+<html>
+<head>
+<style>
+body {{
+  padding: 10px 15px;
+  font-family: verdana;
+  text-align: center;
+}}
+</style>
+</head>
+<body>
+<h2>Oops, Something Went Wrong!</h2>
+<p>Please try again from <a href="{html.escape(self.install_path)}">here</a> or contact the app owner (reason: {html.escape(reason)})</p>
+</body>
+</html>
+"""  # noqa: E501
+
+
+
+
+def render_success_page(self,
app_id: str,
team_id: str | None,
is_enterprise_install: bool | None = None,
enterprise_url: str | None = None) ‑> str
+
+
+
+ +Expand source code + +
    def render_success_page(
+        self,
+        app_id: str,
+        team_id: Optional[str],
+        is_enterprise_install: Optional[bool] = None,
+        enterprise_url: Optional[str] = None,
+    ) -> str:
+        url = self.success_url
+        if url is None:
+            if is_enterprise_install is True and enterprise_url is not None and app_id is not None:
+                url = f"{enterprise_url}manage/organization/apps/profile/{app_id}/workspaces/add"
+            elif team_id is None or app_id is None:
+                url = "slack://open"
+            else:
+                url = f"slack://app?team={team_id}&id={app_id}"
+        browser_url = f"https://app.slack.com/client/{team_id}"
+
+        return f"""
+<html>
+<head>
+<meta http-equiv="refresh" content="0; URL={html.escape(url)}">
+<style>
+body {{
+  padding: 10px 15px;
+  font-family: verdana;
+  text-align: center;
+}}
+</style>
+</head>
+<body>
+<h2>Thank you!</h2>
+<p>Redirecting to the Slack App... click <a href="{html.escape(url)}">here</a>. If you use the browser version of Slack, click <a href="{html.escape(browser_url)}" target="_blank">this link</a> instead.</p>
+</body>
+</html>
+"""  # noqa: E501
+
+
+
+
+
+
+
+
+ +
+ + + diff --git a/docs/reference/oauth/installation_store/amazon_s3/index.html b/docs/reference/oauth/installation_store/amazon_s3/index.html new file mode 100644 index 000000000..0b3afe30a --- /dev/null +++ b/docs/reference/oauth/installation_store/amazon_s3/index.html @@ -0,0 +1,527 @@ + + + + + + +slack_sdk.oauth.installation_store.amazon_s3 API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.oauth.installation_store.amazon_s3

+
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class AmazonS3InstallationStore +(*,
s3_client: botocore.client.BaseClient,
bucket_name: str,
client_id: str,
historical_data_enabled: bool = True,
logger: logging.Logger = <Logger slack_sdk.oauth.installation_store.amazon_s3 (WARNING)>)
+
+
+
+ +Expand source code + +
class AmazonS3InstallationStore(InstallationStore, AsyncInstallationStore):
+    def __init__(
+        self,
+        *,
+        s3_client: BaseClient,
+        bucket_name: str,
+        client_id: str,
+        historical_data_enabled: bool = True,
+        logger: Logger = logging.getLogger(__name__),
+    ):
+        self.s3_client = s3_client
+        self.bucket_name = bucket_name
+        self.historical_data_enabled = historical_data_enabled
+        self.client_id = client_id
+        self._logger = logger
+
+    @property
+    def logger(self) -> Logger:
+        if self._logger is None:
+            self._logger = logging.getLogger(__name__)
+        return self._logger
+
+    async def async_save(self, installation: Installation):
+        return self.save(installation)
+
+    async def async_save_bot(self, bot: Bot):
+        return self.save_bot(bot)
+
+    def save(self, installation: Installation):
+        none = "none"
+        e_id = installation.enterprise_id or none
+        t_id = installation.team_id or none
+        workspace_path = f"{self.client_id}/{e_id}-{t_id}"
+
+        self.save_bot(installation.to_bot())
+
+        if self.historical_data_enabled:
+            history_version: str = str(installation.installed_at)
+
+            # per workspace
+            entity: str = json.dumps(installation.__dict__)
+            response = self.s3_client.put_object(
+                Bucket=self.bucket_name,
+                Body=entity,
+                Key=f"{workspace_path}/installer-latest",
+            )
+            self.logger.debug(f"S3 put_object response: {response}")
+            response = self.s3_client.put_object(
+                Bucket=self.bucket_name,
+                Body=entity,
+                Key=f"{workspace_path}/installer-{history_version}",
+            )
+            self.logger.debug(f"S3 put_object response: {response}")
+
+            # per workspace per user
+            u_id = installation.user_id or none
+            entity = json.dumps(installation.__dict__)
+            response = self.s3_client.put_object(
+                Bucket=self.bucket_name,
+                Body=entity,
+                Key=f"{workspace_path}/installer-{u_id}-latest",
+            )
+            self.logger.debug(f"S3 put_object response: {response}")
+            response = self.s3_client.put_object(
+                Bucket=self.bucket_name,
+                Body=entity,
+                Key=f"{workspace_path}/installer-{u_id}-{history_version}",
+            )
+            self.logger.debug(f"S3 put_object response: {response}")
+
+        else:
+            # per workspace
+            entity = json.dumps(installation.__dict__)
+            response = self.s3_client.put_object(
+                Bucket=self.bucket_name,
+                Body=entity,
+                Key=f"{workspace_path}/installer-latest",
+            )
+            self.logger.debug(f"S3 put_object response: {response}")
+
+            # per workspace per user
+            u_id = installation.user_id or none
+            entity = json.dumps(installation.__dict__)
+            response = self.s3_client.put_object(
+                Bucket=self.bucket_name,
+                Body=entity,
+                Key=f"{workspace_path}/installer-{u_id}-latest",
+            )
+            self.logger.debug(f"S3 put_object response: {response}")
+
+    def save_bot(self, bot: Bot):
+        if bot.bot_token is None:
+            self.logger.debug("Skipped saving a new row because of the absense of bot token in it")
+            return
+
+        none = "none"
+        e_id = bot.enterprise_id or none
+        t_id = bot.team_id or none
+        workspace_path = f"{self.client_id}/{e_id}-{t_id}"
+
+        if self.historical_data_enabled:
+            history_version: str = str(bot.installed_at)
+            entity: str = json.dumps(bot.__dict__)
+            response = self.s3_client.put_object(
+                Bucket=self.bucket_name,
+                Body=entity,
+                Key=f"{workspace_path}/bot-latest",
+            )
+            self.logger.debug(f"S3 put_object response: {response}")
+            response = self.s3_client.put_object(
+                Bucket=self.bucket_name,
+                Body=entity,
+                Key=f"{workspace_path}/bot-{history_version}",
+            )
+            self.logger.debug(f"S3 put_object response: {response}")
+
+        else:
+            entity = json.dumps(bot.__dict__)
+            response = self.s3_client.put_object(
+                Bucket=self.bucket_name,
+                Body=entity,
+                Key=f"{workspace_path}/bot-latest",
+            )
+            self.logger.debug(f"S3 put_object response: {response}")
+
+    async def async_find_bot(
+        self,
+        *,
+        enterprise_id: Optional[str],
+        team_id: Optional[str],
+        is_enterprise_install: Optional[bool] = False,
+    ) -> Optional[Bot]:
+        return self.find_bot(
+            enterprise_id=enterprise_id,
+            team_id=team_id,
+            is_enterprise_install=is_enterprise_install,
+        )
+
+    def find_bot(
+        self,
+        *,
+        enterprise_id: Optional[str],
+        team_id: Optional[str],
+        is_enterprise_install: Optional[bool] = False,
+    ) -> Optional[Bot]:
+        none = "none"
+        e_id = enterprise_id or none
+        t_id = team_id or none
+        if is_enterprise_install:
+            t_id = none
+        workspace_path = f"{self.client_id}/{e_id}-{t_id}"
+        try:
+            fetch_response = self.s3_client.get_object(
+                Bucket=self.bucket_name,
+                Key=f"{workspace_path}/bot-latest",
+            )
+            self.logger.debug(f"S3 get_object response: {fetch_response}")
+            body = fetch_response["Body"].read().decode("utf-8")
+            data = json.loads(body)
+            return Bot(**data)
+        except Exception as e:
+            message = f"Failed to find bot installation data for enterprise: {e_id}, team: {t_id}: {e}"
+            self.logger.warning(message)
+            return None
+
+    async def async_find_installation(
+        self,
+        *,
+        enterprise_id: Optional[str],
+        team_id: Optional[str],
+        user_id: Optional[str] = None,
+        is_enterprise_install: Optional[bool] = False,
+    ) -> Optional[Installation]:
+        return self.find_installation(
+            enterprise_id=enterprise_id,
+            team_id=team_id,
+            user_id=user_id,
+            is_enterprise_install=is_enterprise_install,
+        )
+
+    def find_installation(
+        self,
+        *,
+        enterprise_id: Optional[str],
+        team_id: Optional[str],
+        user_id: Optional[str] = None,
+        is_enterprise_install: Optional[bool] = False,
+    ) -> Optional[Installation]:
+        none = "none"
+        e_id = enterprise_id or none
+        t_id = team_id or none
+        if is_enterprise_install:
+            t_id = none
+        workspace_path = f"{self.client_id}/{e_id}-{t_id}"
+        try:
+            key = f"{workspace_path}/installer-{user_id}-latest" if user_id else f"{workspace_path}/installer-latest"
+            fetch_response = self.s3_client.get_object(
+                Bucket=self.bucket_name,
+                Key=key,
+            )
+            self.logger.debug(f"S3 get_object response: {fetch_response}")
+            body = fetch_response["Body"].read().decode("utf-8")
+            data = json.loads(body)
+            installation = Installation(**data)
+
+            has_user_installation = user_id is not None and installation is not None
+            no_bot_token_installation = installation is not None and installation.bot_token is None
+            should_find_bot_installation = has_user_installation or no_bot_token_installation
+            if should_find_bot_installation:
+                # Retrieve the latest bot token, just in case
+                # See also: https://github.com/slackapi/bolt-python/issues/664
+                latest_bot_installation = self.find_bot(
+                    enterprise_id=enterprise_id,
+                    team_id=team_id,
+                    is_enterprise_install=is_enterprise_install,
+                )
+                if latest_bot_installation is not None and installation.bot_token != latest_bot_installation.bot_token:
+                    # NOTE: this logic is based on the assumption that every single installation has bot scopes
+                    # If you need to installation patterns without bot scopes in the same S3 bucket,
+                    # please fork this code and implement your own logic.
+                    installation.bot_id = latest_bot_installation.bot_id
+                    installation.bot_user_id = latest_bot_installation.bot_user_id
+                    installation.bot_token = latest_bot_installation.bot_token
+                    installation.bot_scopes = latest_bot_installation.bot_scopes
+                    installation.bot_refresh_token = latest_bot_installation.bot_refresh_token
+                    installation.bot_token_expires_at = latest_bot_installation.bot_token_expires_at
+
+            return installation
+
+        except Exception as e:
+            message = f"Failed to find an installation data for enterprise: {e_id}, team: {t_id}: {e}"
+            self.logger.warning(message)
+            return None
+
+    async def async_delete_bot(self, *, enterprise_id: Optional[str], team_id: Optional[str]) -> None:
+        return self.delete_bot(
+            enterprise_id=enterprise_id,
+            team_id=team_id,
+        )
+
+    def delete_bot(self, *, enterprise_id: Optional[str], team_id: Optional[str]) -> None:
+        none = "none"
+        e_id = enterprise_id or none
+        t_id = team_id or none
+        workspace_path = f"{self.client_id}/{e_id}-{t_id}"
+        objects = self.s3_client.list_objects(
+            Bucket=self.bucket_name,
+            Prefix=f"{workspace_path}/bot-",
+        )
+        for content in objects.get("Contents", []):
+            key = content.get("Key")
+            if key is not None:
+                self.logger.info(f"Going to delete bot installation ({key})")
+                try:
+                    self.s3_client.delete_object(
+                        Bucket=self.bucket_name,
+                        Key=content.get("Key"),
+                    )
+                except Exception as e:
+                    message = f"Failed to find bot installation data for enterprise: {e_id}, team: {t_id}: {e}"
+                    raise SlackClientConfigurationError(message)
+
+    async def async_delete_installation(
+        self,
+        *,
+        enterprise_id: Optional[str],
+        team_id: Optional[str],
+        user_id: Optional[str] = None,
+    ) -> None:
+        return self.delete_installation(
+            enterprise_id=enterprise_id,
+            team_id=team_id,
+            user_id=user_id,
+        )
+
+    def delete_installation(
+        self,
+        *,
+        enterprise_id: Optional[str],
+        team_id: Optional[str],
+        user_id: Optional[str] = None,
+    ) -> None:
+        none = "none"
+        e_id = enterprise_id or none
+        t_id = team_id or none
+        workspace_path = f"{self.client_id}/{e_id}-{t_id}"
+        objects = self.s3_client.list_objects(
+            Bucket=self.bucket_name,
+            Prefix=f"{workspace_path}/installer-{user_id or ''}",
+        )
+        deleted_keys = []
+        for content in objects.get("Contents", []):
+            key = content.get("Key")
+            if key is not None:
+                self.logger.info(f"Going to delete installation ({key})")
+                try:
+                    self.s3_client.delete_object(
+                        Bucket=self.bucket_name,
+                        Key=key,
+                    )
+                    deleted_keys.append(key)
+                except Exception as e:
+                    message = f"Failed to find bot installation data for enterprise: {e_id}, team: {t_id}: {e}"
+                    raise SlackClientConfigurationError(message)
+
+                try:
+                    no_user_id_key = key.replace(f"-{user_id}", "")
+                    if not no_user_id_key.endswith("installer-latest"):
+                        self.s3_client.delete_object(
+                            Bucket=self.bucket_name,
+                            Key=no_user_id_key,
+                        )
+                        deleted_keys.append(no_user_id_key)
+                except Exception as e:
+                    message = f"Failed to find bot installation data for enterprise: {e_id}, team: {t_id}: {e}"
+                    raise SlackClientConfigurationError(message)
+
+        # Check the remaining installation data
+        objects = self.s3_client.list_objects(
+            Bucket=self.bucket_name,
+            Prefix=f"{workspace_path}/installer-",
+            MaxKeys=10,  # the small number would be enough for this purpose
+        )
+        keys = [c.get("Key") for c in objects.get("Contents", []) if c.get("Key") not in deleted_keys]
+        # If only installer-latest remains, we should delete the one as well
+        if len(keys) == 1 and keys[0].endswith("installer-latest"):
+            content = objects.get("Contents", [])[0]
+            try:
+                self.s3_client.delete_object(
+                    Bucket=self.bucket_name,
+                    Key=content.get("Key"),
+                )
+            except Exception as e:
+                message = f"Failed to find bot installation data for enterprise: {e_id}, team: {t_id}: {e}"
+                raise SlackClientConfigurationError(message)
+
+

The installation store interface.

+

The minimum required methods are:

+
    +
  • save(installation)
  • +
  • find_installation(enterprise_id, team_id, user_id, is_enterprise_install)
  • +
+

If you would like to properly handle app uninstallations and token revocations, +the following methods should be implemented.

+
    +
  • delete_installation(enterprise_id, team_id, user_id)
  • +
  • delete_all(enterprise_id, team_id)
  • +
+

If your app needs only bot scope installations, the simpler way to implement would be:

+
    +
  • save(installation)
  • +
  • find_bot(enterprise_id, team_id, is_enterprise_install)
  • +
  • delete_bot(enterprise_id, team_id)
  • +
  • delete_all(enterprise_id, team_id)
  • +
+

Ancestors

+ +

Instance variables

+
+
prop logger : logging.Logger
+
+
+ +Expand source code + +
@property
+def logger(self) -> Logger:
+    if self._logger is None:
+        self._logger = logging.getLogger(__name__)
+    return self._logger
+
+
+
+
+

Inherited members

+ +
+
+
+
+ +
+ + + diff --git a/docs/reference/oauth/installation_store/async_cacheable_installation_store.html b/docs/reference/oauth/installation_store/async_cacheable_installation_store.html new file mode 100644 index 000000000..a38205174 --- /dev/null +++ b/docs/reference/oauth/installation_store/async_cacheable_installation_store.html @@ -0,0 +1,293 @@ + + + + + + +slack_sdk.oauth.installation_store.async_cacheable_installation_store API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.oauth.installation_store.async_cacheable_installation_store

+
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class AsyncCacheableInstallationStore +(installation_store: AsyncInstallationStore) +
+
+
+ +Expand source code + +
class AsyncCacheableInstallationStore(AsyncInstallationStore):
+    underlying: AsyncInstallationStore
+    cached_bots: Dict[str, Bot]
+    cached_installations: Dict[str, Installation]
+
+    def __init__(self, installation_store: AsyncInstallationStore):
+        """A simple memory cache wrapper for any installation stores.
+
+        Args:
+            installation_store: The installation store to wrap
+        """
+        self.underlying = installation_store
+        self.cached_bots = {}
+        self.cached_installations = {}
+
+    @property
+    def logger(self) -> Logger:
+        return self.underlying.logger
+
+    async def async_save(self, installation: Installation):
+        # Invalidate cache data for update operations
+        key = f"{installation.enterprise_id or ''}-{installation.team_id or ''}"
+        if key in self.cached_bots:
+            self.cached_bots.pop(key)
+        key = f"{installation.enterprise_id or ''}-{installation.team_id or ''}-{installation.user_id or ''}"
+        if key in self.cached_installations:
+            self.cached_installations.pop(key)
+        return await self.underlying.async_save(installation)
+
+    async def async_save_bot(self, bot: Bot):
+        # Invalidate cache data for update operations
+        key = f"{bot.enterprise_id or ''}-{bot.team_id or ''}"
+        if key in self.cached_bots:
+            self.cached_bots.pop(key)
+        return await self.underlying.async_save_bot(bot)
+
+    async def async_find_bot(
+        self,
+        *,
+        enterprise_id: Optional[str],
+        team_id: Optional[str],
+        is_enterprise_install: Optional[bool] = False,
+    ) -> Optional[Bot]:
+        if is_enterprise_install or team_id is None:
+            team_id = ""
+        key = f"{enterprise_id or ''}-{team_id or ''}"
+        if key in self.cached_bots:
+            return self.cached_bots[key]
+        bot = await self.underlying.async_find_bot(
+            enterprise_id=enterprise_id,
+            team_id=team_id,
+            is_enterprise_install=is_enterprise_install,
+        )
+        if bot:
+            self.cached_bots[key] = bot
+        return bot
+
+    async def async_find_installation(
+        self,
+        *,
+        enterprise_id: Optional[str],
+        team_id: Optional[str],
+        user_id: Optional[str] = None,
+        is_enterprise_install: Optional[bool] = False,
+    ) -> Optional[Installation]:
+        if is_enterprise_install or team_id is None:
+            team_id = ""
+        key = f"{enterprise_id or ''}-{team_id or ''}-{user_id or ''}"
+        if key in self.cached_installations:
+            return self.cached_installations[key]
+        installation = await self.underlying.async_find_installation(
+            enterprise_id=enterprise_id,
+            team_id=team_id,
+            user_id=user_id,
+            is_enterprise_install=is_enterprise_install,
+        )
+        if installation:
+            self.cached_installations[key] = installation
+        return installation
+
+    async def async_delete_bot(
+        self,
+        *,
+        enterprise_id: Optional[str],
+        team_id: Optional[str],
+    ) -> None:
+        await self.underlying.async_delete_bot(
+            enterprise_id=enterprise_id,
+            team_id=team_id,
+        )
+        key = f"{enterprise_id or ''}-{team_id or ''}"
+        self.cached_bots.pop(key)
+
+    async def async_delete_installation(
+        self,
+        *,
+        enterprise_id: Optional[str],
+        team_id: Optional[str],
+        user_id: Optional[str] = None,
+    ) -> None:
+        await self.underlying.async_delete_installation(
+            enterprise_id=enterprise_id,
+            team_id=team_id,
+            user_id=user_id,
+        )
+        key_prefix = f"{enterprise_id or ''}-{team_id or ''}"
+        for key in list(self.cached_installations.keys()):
+            if key.startswith(key_prefix):
+                self.cached_installations.pop(key)
+
+    async def async_delete_all(
+        self,
+        *,
+        enterprise_id: Optional[str],
+        team_id: Optional[str],
+    ):
+        await self.underlying.async_delete_all(
+            enterprise_id=enterprise_id,
+            team_id=team_id,
+        )
+        key_prefix = f"{enterprise_id or ''}-{team_id or ''}"
+        for key in list(self.cached_bots.keys()):
+            if key.startswith(key_prefix):
+                self.cached_bots.pop(key)
+        for key in list(self.cached_installations.keys()):
+            if key.startswith(key_prefix):
+                self.cached_installations.pop(key)
+
+

The installation store interface for asyncio-based apps.

+

The minimum required methods are:

+
    +
  • async_save(installation)
  • +
  • async_find_installation(enterprise_id, team_id, user_id, is_enterprise_install)
  • +
+

If you would like to properly handle app uninstallations and token revocations, +the following methods should be implemented.

+
    +
  • async_delete_installation(enterprise_id, team_id, user_id)
  • +
  • async_delete_all(enterprise_id, team_id)
  • +
+

If your app needs only bot scope installations, the simpler way to implement would be:

+
    +
  • async_save(installation)
  • +
  • async_find_bot(enterprise_id, team_id, is_enterprise_install)
  • +
  • async_delete_bot(enterprise_id, team_id)
  • +
  • async_delete_all(enterprise_id, team_id)
  • +
+

A simple memory cache wrapper for any installation stores.

+

Args

+
+
installation_store
+
The installation store to wrap
+
+

Ancestors

+ +

Class variables

+
+
var cached_bots : Dict[str, Bot]
+
+

The type of the None singleton.

+
+
var cached_installations : Dict[str, Installation]
+
+

The type of the None singleton.

+
+
var underlyingAsyncInstallationStore
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
prop logger : logging.Logger
+
+
+ +Expand source code + +
@property
+def logger(self) -> Logger:
+    return self.underlying.logger
+
+
+
+
+

Inherited members

+ +
+
+
+
+ +
+ + + diff --git a/docs/reference/oauth/installation_store/async_installation_store.html b/docs/reference/oauth/installation_store/async_installation_store.html new file mode 100644 index 000000000..e6e6583e4 --- /dev/null +++ b/docs/reference/oauth/installation_store/async_installation_store.html @@ -0,0 +1,357 @@ + + + + + + +slack_sdk.oauth.installation_store.async_installation_store API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.oauth.installation_store.async_installation_store

+
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class AsyncInstallationStore +
+
+
+ +Expand source code + +
class AsyncInstallationStore:
+    """The installation store interface for asyncio-based apps.
+
+    The minimum required methods are:
+
+    * async_save(installation)
+    * async_find_installation(enterprise_id, team_id, user_id, is_enterprise_install)
+
+    If you would like to properly handle app uninstallations and token revocations,
+    the following methods should be implemented.
+
+    * async_delete_installation(enterprise_id, team_id, user_id)
+    * async_delete_all(enterprise_id, team_id)
+
+    If your app needs only bot scope installations, the simpler way to implement would be:
+
+    * async_save(installation)
+    * async_find_bot(enterprise_id, team_id, is_enterprise_install)
+    * async_delete_bot(enterprise_id, team_id)
+    * async_delete_all(enterprise_id, team_id)
+    """
+
+    @property
+    def logger(self) -> Logger:
+        raise NotImplementedError()
+
+    async def async_save(self, installation: Installation):
+        """Saves an installation data"""
+        raise NotImplementedError()
+
+    async def async_save_bot(self, bot: Bot):
+        """Saves a bot installation data"""
+        raise NotImplementedError()
+
+    async def async_find_bot(
+        self,
+        *,
+        enterprise_id: Optional[str],
+        team_id: Optional[str],
+        is_enterprise_install: Optional[bool] = False,
+    ) -> Optional[Bot]:
+        """Finds a bot scope installation per workspace / org"""
+        raise NotImplementedError()
+
+    async def async_find_installation(
+        self,
+        *,
+        enterprise_id: Optional[str],
+        team_id: Optional[str],
+        user_id: Optional[str] = None,
+        is_enterprise_install: Optional[bool] = False,
+    ) -> Optional[Installation]:
+        """Finds a relevant installation for the given IDs.
+        If the user_id is absent, this method may return the latest installation in the workspace / org.
+        """
+        raise NotImplementedError()
+
+    async def async_delete_bot(
+        self,
+        *,
+        enterprise_id: Optional[str],
+        team_id: Optional[str],
+    ) -> None:
+        """Deletes a bot scope installation per workspace / org"""
+        raise NotImplementedError()
+
+    async def async_delete_installation(
+        self,
+        *,
+        enterprise_id: Optional[str],
+        team_id: Optional[str],
+        user_id: Optional[str] = None,
+    ) -> None:
+        """Deletes an installation that matches the given IDs"""
+        raise NotImplementedError()
+
+    async def async_delete_all(
+        self,
+        *,
+        enterprise_id: Optional[str],
+        team_id: Optional[str],
+    ):
+        """Deletes all installation data for the given workspace / org"""
+        await self.async_delete_bot(enterprise_id=enterprise_id, team_id=team_id)
+        await self.async_delete_installation(enterprise_id=enterprise_id, team_id=team_id)
+
+

The installation store interface for asyncio-based apps.

+

The minimum required methods are:

+
    +
  • async_save(installation)
  • +
  • async_find_installation(enterprise_id, team_id, user_id, is_enterprise_install)
  • +
+

If you would like to properly handle app uninstallations and token revocations, +the following methods should be implemented.

+
    +
  • async_delete_installation(enterprise_id, team_id, user_id)
  • +
  • async_delete_all(enterprise_id, team_id)
  • +
+

If your app needs only bot scope installations, the simpler way to implement would be:

+
    +
  • async_save(installation)
  • +
  • async_find_bot(enterprise_id, team_id, is_enterprise_install)
  • +
  • async_delete_bot(enterprise_id, team_id)
  • +
  • async_delete_all(enterprise_id, team_id)
  • +
+

Subclasses

+ +

Instance variables

+
+
prop logger : logging.Logger
+
+
+ +Expand source code + +
@property
+def logger(self) -> Logger:
+    raise NotImplementedError()
+
+
+
+
+

Methods

+
+
+async def async_delete_all(self, *, enterprise_id: str | None, team_id: str | None) +
+
+
+ +Expand source code + +
async def async_delete_all(
+    self,
+    *,
+    enterprise_id: Optional[str],
+    team_id: Optional[str],
+):
+    """Deletes all installation data for the given workspace / org"""
+    await self.async_delete_bot(enterprise_id=enterprise_id, team_id=team_id)
+    await self.async_delete_installation(enterprise_id=enterprise_id, team_id=team_id)
+
+

Deletes all installation data for the given workspace / org

+
+
+async def async_delete_bot(self, *, enterprise_id: str | None, team_id: str | None) ‑> None +
+
+
+ +Expand source code + +
async def async_delete_bot(
+    self,
+    *,
+    enterprise_id: Optional[str],
+    team_id: Optional[str],
+) -> None:
+    """Deletes a bot scope installation per workspace / org"""
+    raise NotImplementedError()
+
+

Deletes a bot scope installation per workspace / org

+
+
+async def async_delete_installation(self, *, enterprise_id: str | None, team_id: str | None, user_id: str | None = None) ‑> None +
+
+
+ +Expand source code + +
async def async_delete_installation(
+    self,
+    *,
+    enterprise_id: Optional[str],
+    team_id: Optional[str],
+    user_id: Optional[str] = None,
+) -> None:
+    """Deletes an installation that matches the given IDs"""
+    raise NotImplementedError()
+
+

Deletes an installation that matches the given IDs

+
+
+async def async_find_bot(self,
*,
enterprise_id: str | None,
team_id: str | None,
is_enterprise_install: bool | None = False) ‑> Bot | None
+
+
+
+ +Expand source code + +
async def async_find_bot(
+    self,
+    *,
+    enterprise_id: Optional[str],
+    team_id: Optional[str],
+    is_enterprise_install: Optional[bool] = False,
+) -> Optional[Bot]:
+    """Finds a bot scope installation per workspace / org"""
+    raise NotImplementedError()
+
+

Finds a bot scope installation per workspace / org

+
+
+async def async_find_installation(self,
*,
enterprise_id: str | None,
team_id: str | None,
user_id: str | None = None,
is_enterprise_install: bool | None = False) ‑> Installation | None
+
+
+
+ +Expand source code + +
async def async_find_installation(
+    self,
+    *,
+    enterprise_id: Optional[str],
+    team_id: Optional[str],
+    user_id: Optional[str] = None,
+    is_enterprise_install: Optional[bool] = False,
+) -> Optional[Installation]:
+    """Finds a relevant installation for the given IDs.
+    If the user_id is absent, this method may return the latest installation in the workspace / org.
+    """
+    raise NotImplementedError()
+
+

Finds a relevant installation for the given IDs. +If the user_id is absent, this method may return the latest installation in the workspace / org.

+
+
+async def async_save(self,
installation: Installation)
+
+
+
+ +Expand source code + +
async def async_save(self, installation: Installation):
+    """Saves an installation data"""
+    raise NotImplementedError()
+
+

Saves an installation data

+
+
+async def async_save_bot(self,
bot: Bot)
+
+
+
+ +Expand source code + +
async def async_save_bot(self, bot: Bot):
+    """Saves a bot installation data"""
+    raise NotImplementedError()
+
+

Saves a bot installation data

+
+
+
+
+
+
+ +
+ + + diff --git a/docs/reference/oauth/installation_store/cacheable_installation_store.html b/docs/reference/oauth/installation_store/cacheable_installation_store.html new file mode 100644 index 000000000..54bb7662d --- /dev/null +++ b/docs/reference/oauth/installation_store/cacheable_installation_store.html @@ -0,0 +1,295 @@ + + + + + + +slack_sdk.oauth.installation_store.cacheable_installation_store API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.oauth.installation_store.cacheable_installation_store

+
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class CacheableInstallationStore +(installation_store: InstallationStore) +
+
+
+ +Expand source code + +
class CacheableInstallationStore(InstallationStore):
+    underlying: InstallationStore
+    cached_bots: Dict[str, Bot]
+    cached_installations: Dict[str, Installation]
+
+    def __init__(self, installation_store: InstallationStore):
+        """A simple memory cache wrapper for any installation stores.
+
+        Args:
+            installation_store: The installation store to wrap
+        """
+        self.underlying = installation_store
+        self.cached_bots = {}
+        self.cached_installations = {}
+
+    @property
+    def logger(self) -> Logger:
+        return self.underlying.logger
+
+    def save(self, installation: Installation):
+        # Invalidate cache data for update operations
+        key = f"{installation.enterprise_id or ''}-{installation.team_id or ''}"
+        if key in self.cached_bots:
+            self.cached_bots.pop(key)
+        key = f"{installation.enterprise_id or ''}-{installation.team_id or ''}-{installation.user_id or ''}"
+        if key in self.cached_installations:
+            self.cached_installations.pop(key)
+
+        return self.underlying.save(installation)
+
+    def save_bot(self, bot: Bot):
+        # Invalidate cache data for update operations
+        key = f"{bot.enterprise_id or ''}-{bot.team_id or ''}"
+        if key in self.cached_bots:
+            self.cached_bots.pop(key)
+        return self.underlying.save_bot(bot)
+
+    def find_bot(
+        self,
+        *,
+        enterprise_id: Optional[str],
+        team_id: Optional[str],
+        is_enterprise_install: Optional[bool] = False,
+    ) -> Optional[Bot]:
+        if is_enterprise_install or team_id is None:
+            team_id = ""
+        key = f"{enterprise_id or ''}-{team_id or ''}"
+        if key in self.cached_bots:
+            return self.cached_bots[key]
+        bot = self.underlying.find_bot(
+            enterprise_id=enterprise_id,
+            team_id=team_id,
+            is_enterprise_install=is_enterprise_install,
+        )
+        if bot:
+            self.cached_bots[key] = bot
+        return bot
+
+    def find_installation(
+        self,
+        *,
+        enterprise_id: Optional[str],
+        team_id: Optional[str],
+        user_id: Optional[str] = None,
+        is_enterprise_install: Optional[bool] = False,
+    ) -> Optional[Installation]:
+        if is_enterprise_install or team_id is None:
+            team_id = ""
+        key = f"{enterprise_id or ''}-{team_id or ''}-{user_id or ''}"
+        if key in self.cached_installations:
+            return self.cached_installations[key]
+        installation = self.underlying.find_installation(
+            enterprise_id=enterprise_id,
+            team_id=team_id,
+            user_id=user_id,
+            is_enterprise_install=is_enterprise_install,
+        )
+        if installation:
+            self.cached_installations[key] = installation
+        return installation
+
+    def delete_bot(
+        self,
+        *,
+        enterprise_id: Optional[str],
+        team_id: Optional[str],
+    ) -> None:
+        self.underlying.delete_bot(
+            enterprise_id=enterprise_id,
+            team_id=team_id,
+        )
+        key = f"{enterprise_id or ''}-{team_id or ''}"
+        if key in self.cached_bots:
+            self.cached_bots.pop(key)
+
+    def delete_installation(
+        self,
+        *,
+        enterprise_id: Optional[str],
+        team_id: Optional[str],
+        user_id: Optional[str] = None,
+    ) -> None:
+        self.underlying.delete_installation(
+            enterprise_id=enterprise_id,
+            team_id=team_id,
+            user_id=user_id,
+        )
+        key_prefix = f"{enterprise_id or ''}-{team_id or ''}"
+        for key in list(self.cached_installations.keys()):
+            if key.startswith(key_prefix):
+                self.cached_installations.pop(key)
+
+    def delete_all(
+        self,
+        *,
+        enterprise_id: Optional[str],
+        team_id: Optional[str],
+    ):
+        self.underlying.delete_all(
+            enterprise_id=enterprise_id,
+            team_id=team_id,
+        )
+        key_prefix = f"{enterprise_id or ''}-{team_id or ''}"
+        for key in list(self.cached_bots.keys()):
+            if key.startswith(key_prefix):
+                self.cached_bots.pop(key)
+        for key in list(self.cached_installations.keys()):
+            if key.startswith(key_prefix):
+                self.cached_installations.pop(key)
+
+

The installation store interface.

+

The minimum required methods are:

+
    +
  • save(installation)
  • +
  • find_installation(enterprise_id, team_id, user_id, is_enterprise_install)
  • +
+

If you would like to properly handle app uninstallations and token revocations, +the following methods should be implemented.

+
    +
  • delete_installation(enterprise_id, team_id, user_id)
  • +
  • delete_all(enterprise_id, team_id)
  • +
+

If your app needs only bot scope installations, the simpler way to implement would be:

+
    +
  • save(installation)
  • +
  • find_bot(enterprise_id, team_id, is_enterprise_install)
  • +
  • delete_bot(enterprise_id, team_id)
  • +
  • delete_all(enterprise_id, team_id)
  • +
+

A simple memory cache wrapper for any installation stores.

+

Args

+
+
installation_store
+
The installation store to wrap
+
+

Ancestors

+ +

Class variables

+
+
var cached_bots : Dict[str, Bot]
+
+

The type of the None singleton.

+
+
var cached_installations : Dict[str, Installation]
+
+

The type of the None singleton.

+
+
var underlyingInstallationStore
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
prop logger : logging.Logger
+
+
+ +Expand source code + +
@property
+def logger(self) -> Logger:
+    return self.underlying.logger
+
+
+
+
+

Inherited members

+ +
+
+
+
+ +
+ + + diff --git a/docs/reference/oauth/installation_store/file/index.html b/docs/reference/oauth/installation_store/file/index.html new file mode 100644 index 000000000..db77a087c --- /dev/null +++ b/docs/reference/oauth/installation_store/file/index.html @@ -0,0 +1,428 @@ + + + + + + +slack_sdk.oauth.installation_store.file API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.oauth.installation_store.file

+
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class FileInstallationStore +(*,
base_dir: str = '$HOME/.bolt-app-installation',
historical_data_enabled: bool = True,
client_id: str | None = None,
logger: logging.Logger = <Logger slack_sdk.oauth.installation_store.file (WARNING)>)
+
+
+
+ +Expand source code + +
class FileInstallationStore(InstallationStore, AsyncInstallationStore):
+    def __init__(
+        self,
+        *,
+        base_dir: str = str(Path.home()) + "/.bolt-app-installation",
+        historical_data_enabled: bool = True,
+        client_id: Optional[str] = None,
+        logger: Logger = logging.getLogger(__name__),
+    ):
+        self.base_dir = base_dir
+        self.historical_data_enabled = historical_data_enabled
+        self.client_id = client_id
+        if self.client_id is not None:
+            self.base_dir = f"{self.base_dir}/{self.client_id}"
+        self._logger = logger
+
+    @property
+    def logger(self) -> Logger:
+        if self._logger is None:
+            self._logger = logging.getLogger(__name__)
+        return self._logger
+
+    async def async_save(self, installation: Installation):
+        return self.save(installation)
+
+    async def async_save_bot(self, bot: Bot):
+        return self.save_bot(bot)
+
+    def save(self, installation: Installation):
+        none = "none"
+        e_id = installation.enterprise_id or none
+        t_id = installation.team_id or none
+        team_installation_dir = f"{self.base_dir}/{e_id}-{t_id}"
+        self._mkdir(team_installation_dir)
+
+        self.save_bot(installation.to_bot())
+
+        if self.historical_data_enabled:
+            history_version: str = str(installation.installed_at)
+
+            # per workspace
+            entity: str = json.dumps(installation.__dict__)
+            with open(f"{team_installation_dir}/installer-latest", "w") as f:
+                f.write(entity)
+            with open(f"{team_installation_dir}/installer-{history_version}", "w") as f:
+                f.write(entity)
+
+            # per workspace per user
+            u_id = installation.user_id or none
+            entity = json.dumps(installation.__dict__)
+            with open(f"{team_installation_dir}/installer-{u_id}-latest", "w") as f:
+                f.write(entity)
+            with open(f"{team_installation_dir}/installer-{u_id}-{history_version}", "w") as f:
+                f.write(entity)
+
+        else:
+            u_id = installation.user_id or none
+            installer_filepath = f"{team_installation_dir}/installer-{u_id}-latest"
+            with open(installer_filepath, "w") as f:
+                entity = json.dumps(installation.__dict__)
+                f.write(entity)
+
+    def save_bot(self, bot: Bot):
+        if bot.bot_token is None:
+            self.logger.debug("Skipped saving a new row because of the absense of bot token in it")
+            return
+
+        none = "none"
+        e_id = bot.enterprise_id or none
+        t_id = bot.team_id or none
+        team_installation_dir = f"{self.base_dir}/{e_id}-{t_id}"
+        self._mkdir(team_installation_dir)
+
+        if self.historical_data_enabled:
+            history_version: str = str(bot.installed_at)
+
+            entity: str = json.dumps(bot.__dict__)
+            with open(f"{team_installation_dir}/bot-latest", "w") as f:
+                f.write(entity)
+            with open(f"{team_installation_dir}/bot-{history_version}", "w") as f:
+                f.write(entity)
+        else:
+            with open(f"{team_installation_dir}/bot-latest", "w") as f:
+                entity = json.dumps(bot.__dict__)
+                f.write(entity)
+
+    async def async_find_bot(
+        self,
+        *,
+        enterprise_id: Optional[str],
+        team_id: Optional[str],
+        is_enterprise_install: Optional[bool] = False,
+    ) -> Optional[Bot]:
+        return self.find_bot(
+            enterprise_id=enterprise_id,
+            team_id=team_id,
+            is_enterprise_install=is_enterprise_install,
+        )
+
+    def find_bot(
+        self,
+        *,
+        enterprise_id: Optional[str],
+        team_id: Optional[str],
+        is_enterprise_install: Optional[bool] = False,
+    ) -> Optional[Bot]:
+        none = "none"
+        e_id = enterprise_id or none
+        t_id = team_id or none
+        if is_enterprise_install:
+            t_id = none
+        bot_filepath = f"{self.base_dir}/{e_id}-{t_id}/bot-latest"
+        try:
+            with open(bot_filepath) as f:
+                data = json.loads(f.read())
+                return Bot(**data)
+        except FileNotFoundError as e:
+            message = f"Installation data missing for enterprise: {e_id}, team: {t_id}: {e}"
+            self.logger.debug(message)
+            return None
+
+    async def async_find_installation(
+        self,
+        *,
+        enterprise_id: Optional[str],
+        team_id: Optional[str],
+        user_id: Optional[str] = None,
+        is_enterprise_install: Optional[bool] = False,
+    ) -> Optional[Installation]:
+        return self.find_installation(
+            enterprise_id=enterprise_id,
+            team_id=team_id,
+            user_id=user_id,
+            is_enterprise_install=is_enterprise_install,
+        )
+
+    def find_installation(
+        self,
+        *,
+        enterprise_id: Optional[str],
+        team_id: Optional[str],
+        user_id: Optional[str] = None,
+        is_enterprise_install: Optional[bool] = False,
+    ) -> Optional[Installation]:
+        none = "none"
+        e_id = enterprise_id or none
+        t_id = team_id or none
+        if is_enterprise_install:
+            t_id = none
+        installation_filepath = f"{self.base_dir}/{e_id}-{t_id}/installer-latest"
+        if user_id is not None:
+            installation_filepath = f"{self.base_dir}/{e_id}-{t_id}/installer-{user_id}-latest"
+
+        try:
+            installation: Optional[Installation] = None
+            with open(installation_filepath) as f:
+                data = json.loads(f.read())
+                installation = Installation(**data)
+
+            has_user_installation = user_id is not None and installation is not None
+            no_bot_token_installation = installation is not None and installation.bot_token is None
+            should_find_bot_installation = has_user_installation or no_bot_token_installation
+            if should_find_bot_installation:
+                # Retrieve the latest bot token, just in case
+                # See also: https://github.com/slackapi/bolt-python/issues/664
+                latest_bot_installation = self.find_bot(
+                    enterprise_id=enterprise_id,
+                    team_id=team_id,
+                    is_enterprise_install=is_enterprise_install,
+                )
+                if latest_bot_installation is not None and installation.bot_token != latest_bot_installation.bot_token:
+                    # NOTE: this logic is based on the assumption that every single installation has bot scopes
+                    # If you need to installation patterns without bot scopes in the same S3 bucket,
+                    # please fork this code and implement your own logic.
+                    installation.bot_id = latest_bot_installation.bot_id
+                    installation.bot_user_id = latest_bot_installation.bot_user_id
+                    installation.bot_token = latest_bot_installation.bot_token
+                    installation.bot_scopes = latest_bot_installation.bot_scopes
+                    installation.bot_refresh_token = latest_bot_installation.bot_refresh_token
+                    installation.bot_token_expires_at = latest_bot_installation.bot_token_expires_at
+
+            return installation
+
+        except FileNotFoundError as e:
+            message = f"Installation data missing for enterprise: {e_id}, team: {t_id}: {e}"
+            self.logger.debug(message)
+            return None
+
+    async def async_delete_bot(self, *, enterprise_id: Optional[str], team_id: Optional[str]) -> None:
+        return self.delete_bot(enterprise_id=enterprise_id, team_id=team_id)
+
+    def delete_bot(self, *, enterprise_id: Optional[str], team_id: Optional[str]) -> None:
+        none = "none"
+        e_id = enterprise_id or none
+        t_id = team_id or none
+        filepath_glob = f"{self.base_dir}/{e_id}-{t_id}/bot-*"
+        self._delete_by_glob(e_id, t_id, filepath_glob)
+
+    async def async_delete_installation(
+        self,
+        *,
+        enterprise_id: Optional[str],
+        team_id: Optional[str],
+        user_id: Optional[str] = None,
+    ) -> None:
+        return self.delete_installation(enterprise_id=enterprise_id, team_id=team_id, user_id=user_id)
+
+    def delete_installation(
+        self,
+        *,
+        enterprise_id: Optional[str],
+        team_id: Optional[str],
+        user_id: Optional[str] = None,
+    ) -> None:
+        none = "none"
+        e_id = enterprise_id or none
+        t_id = team_id or none
+        if user_id is not None:
+            filepath_glob = f"{self.base_dir}/{e_id}-{t_id}/installer-{user_id}-*"
+        else:
+            filepath_glob = f"{self.base_dir}/{e_id}-{t_id}/installer-*"
+        self._delete_by_glob(e_id, t_id, filepath_glob)
+
+    def _delete_by_glob(self, e_id: str, t_id: str, filepath_glob: str):
+        for filepath in glob.glob(filepath_glob):
+            try:
+                os.remove(filepath)
+            except FileNotFoundError as e:
+                message = f"Failed to delete installation data for enterprise: {e_id}, team: {t_id}: {e}"
+                self.logger.warning(message)
+
+    @staticmethod
+    def _mkdir(path: Union[str, Path]):
+        if isinstance(path, str):
+            path = Path(path)
+        path.mkdir(parents=True, exist_ok=True)
+
+

The installation store interface.

+

The minimum required methods are:

+
    +
  • save(installation)
  • +
  • find_installation(enterprise_id, team_id, user_id, is_enterprise_install)
  • +
+

If you would like to properly handle app uninstallations and token revocations, +the following methods should be implemented.

+
    +
  • delete_installation(enterprise_id, team_id, user_id)
  • +
  • delete_all(enterprise_id, team_id)
  • +
+

If your app needs only bot scope installations, the simpler way to implement would be:

+
    +
  • save(installation)
  • +
  • find_bot(enterprise_id, team_id, is_enterprise_install)
  • +
  • delete_bot(enterprise_id, team_id)
  • +
  • delete_all(enterprise_id, team_id)
  • +
+

Ancestors

+ +

Instance variables

+
+
prop logger : logging.Logger
+
+
+ +Expand source code + +
@property
+def logger(self) -> Logger:
+    if self._logger is None:
+        self._logger = logging.getLogger(__name__)
+    return self._logger
+
+
+
+
+

Inherited members

+ +
+
+
+
+ +
+ + + diff --git a/docs/reference/oauth/installation_store/index.html b/docs/reference/oauth/installation_store/index.html new file mode 100644 index 000000000..4f90fb1c5 --- /dev/null +++ b/docs/reference/oauth/installation_store/index.html @@ -0,0 +1,1455 @@ + + + + + + +slack_sdk.oauth.installation_store API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.oauth.installation_store

+
+
+
+
+

Sub-modules

+
+
slack_sdk.oauth.installation_store.amazon_s3
+
+
+
+
slack_sdk.oauth.installation_store.async_cacheable_installation_store
+
+
+
+
slack_sdk.oauth.installation_store.async_installation_store
+
+
+
+
slack_sdk.oauth.installation_store.cacheable_installation_store
+
+
+
+
slack_sdk.oauth.installation_store.file
+
+
+
+
slack_sdk.oauth.installation_store.installation_store
+
+

Slack installation data store …

+
+
slack_sdk.oauth.installation_store.internals
+
+
+
+
slack_sdk.oauth.installation_store.models
+
+
+
+
slack_sdk.oauth.installation_store.sqlalchemy
+
+
+
+
slack_sdk.oauth.installation_store.sqlite3
+
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class Bot +(*,
app_id: str | None = None,
enterprise_id: str | None = None,
enterprise_name: str | None = None,
team_id: str | None = None,
team_name: str | None = None,
bot_token: str,
bot_id: str,
bot_user_id: str,
bot_scopes: str | Sequence[str] = '',
bot_refresh_token: str | None = None,
bot_token_expires_in: int | None = None,
bot_token_expires_at: int | datetime.datetime | str | None = None,
is_enterprise_install: bool | None = False,
installed_at: float | datetime.datetime | str,
custom_values: Dict[str, Any] | None = None)
+
+
+
+ +Expand source code + +
class Bot:
+    app_id: Optional[str]
+    enterprise_id: Optional[str]
+    enterprise_name: Optional[str]
+    team_id: Optional[str]
+    team_name: Optional[str]
+    bot_token: str
+    bot_id: str
+    bot_user_id: str
+    bot_scopes: Sequence[str]
+    # only when token rotation is enabled
+    bot_refresh_token: Optional[str]
+    # only when token rotation is enabled
+    bot_token_expires_at: Optional[int]
+    is_enterprise_install: bool
+    installed_at: float
+
+    custom_values: Dict[str, Any]
+
+    def __init__(
+        self,
+        *,
+        app_id: Optional[str] = None,
+        # org / workspace
+        enterprise_id: Optional[str] = None,
+        enterprise_name: Optional[str] = None,
+        team_id: Optional[str] = None,
+        team_name: Optional[str] = None,
+        # bot
+        bot_token: str,
+        bot_id: str,
+        bot_user_id: str,
+        bot_scopes: Union[str, Sequence[str]] = "",
+        # only when token rotation is enabled
+        bot_refresh_token: Optional[str] = None,
+        # only when token rotation is enabled
+        bot_token_expires_in: Optional[int] = None,
+        # only for duplicating this object
+        # only when token rotation is enabled
+        bot_token_expires_at: Optional[Union[int, datetime, str]] = None,
+        is_enterprise_install: Optional[bool] = False,
+        # timestamps
+        # The expected value type is float but the internals handle other types too
+        # for str values, we support only ISO datetime format.
+        installed_at: Union[float, datetime, str],
+        # custom values
+        custom_values: Optional[Dict[str, Any]] = None,
+    ):
+        self.app_id = app_id
+        self.enterprise_id = enterprise_id
+        self.enterprise_name = enterprise_name
+        self.team_id = team_id
+        self.team_name = team_name
+
+        self.bot_token = bot_token
+        self.bot_id = bot_id
+        self.bot_user_id = bot_user_id
+        if isinstance(bot_scopes, str):
+            self.bot_scopes = bot_scopes.split(",") if len(bot_scopes) > 0 else []
+        else:
+            self.bot_scopes = bot_scopes
+        self.bot_refresh_token = bot_refresh_token
+
+        if bot_token_expires_at is not None:
+            self.bot_token_expires_at = _timestamp_to_type(bot_token_expires_at, int)
+        elif bot_token_expires_in is not None:
+            self.bot_token_expires_at = int(time()) + bot_token_expires_in
+        else:
+            self.bot_token_expires_at = None
+
+        self.is_enterprise_install = is_enterprise_install or False
+
+        self.installed_at = _timestamp_to_type(installed_at, float)
+
+        self.custom_values = custom_values if custom_values is not None else {}
+
+    def set_custom_value(self, name: str, value: Any):
+        self.custom_values[name] = value
+
+    def get_custom_value(self, name: str) -> Optional[Any]:
+        return self.custom_values.get(name)
+
+    def _to_standard_value_dict(self) -> Dict[str, Any]:
+        return {
+            "app_id": self.app_id,
+            "enterprise_id": self.enterprise_id,
+            "enterprise_name": self.enterprise_name,
+            "team_id": self.team_id,
+            "team_name": self.team_name,
+            "bot_token": self.bot_token,
+            "bot_id": self.bot_id,
+            "bot_user_id": self.bot_user_id,
+            "bot_scopes": ",".join(self.bot_scopes) if self.bot_scopes else None,
+            "bot_refresh_token": self.bot_refresh_token,
+            "bot_token_expires_at": (
+                datetime.utcfromtimestamp(self.bot_token_expires_at) if self.bot_token_expires_at is not None else None
+            ),
+            "is_enterprise_install": self.is_enterprise_install,
+            "installed_at": datetime.utcfromtimestamp(self.installed_at),
+        }
+
+    def to_dict_for_copying(self) -> Dict[str, Any]:
+        return {"custom_values": self.custom_values, **self._to_standard_value_dict()}
+
+    def to_dict(self) -> Dict[str, Any]:
+        # prioritize standard_values over custom_values
+        # when the same keys exist in both
+        return {**self.custom_values, **self._to_standard_value_dict()}
+
+
+

Class variables

+
+
var app_id : str | None
+
+

The type of the None singleton.

+
+
var bot_id : str
+
+

The type of the None singleton.

+
+
var bot_refresh_token : str | None
+
+

The type of the None singleton.

+
+
var bot_scopes : Sequence[str]
+
+

The type of the None singleton.

+
+
var bot_token : str
+
+

The type of the None singleton.

+
+
var bot_token_expires_at : int | None
+
+

The type of the None singleton.

+
+
var bot_user_id : str
+
+

The type of the None singleton.

+
+
var custom_values : Dict[str, Any]
+
+

The type of the None singleton.

+
+
var enterprise_id : str | None
+
+

The type of the None singleton.

+
+
var enterprise_name : str | None
+
+

The type of the None singleton.

+
+
var installed_at : float
+
+

The type of the None singleton.

+
+
var is_enterprise_install : bool
+
+

The type of the None singleton.

+
+
var team_id : str | None
+
+

The type of the None singleton.

+
+
var team_name : str | None
+
+

The type of the None singleton.

+
+
+

Methods

+
+
+def get_custom_value(self, name: str) ‑> Any | None +
+
+
+ +Expand source code + +
def get_custom_value(self, name: str) -> Optional[Any]:
+    return self.custom_values.get(name)
+
+
+
+
+def set_custom_value(self, name: str, value: Any) +
+
+
+ +Expand source code + +
def set_custom_value(self, name: str, value: Any):
+    self.custom_values[name] = value
+
+
+
+
+def to_dict(self) ‑> Dict[str, Any] +
+
+
+ +Expand source code + +
def to_dict(self) -> Dict[str, Any]:
+    # prioritize standard_values over custom_values
+    # when the same keys exist in both
+    return {**self.custom_values, **self._to_standard_value_dict()}
+
+
+
+
+def to_dict_for_copying(self) ‑> Dict[str, Any] +
+
+
+ +Expand source code + +
def to_dict_for_copying(self) -> Dict[str, Any]:
+    return {"custom_values": self.custom_values, **self._to_standard_value_dict()}
+
+
+
+
+
+
+class FileInstallationStore +(*,
base_dir: str = '$HOME/.bolt-app-installation',
historical_data_enabled: bool = True,
client_id: str | None = None,
logger: logging.Logger = <Logger slack_sdk.oauth.installation_store.file (WARNING)>)
+
+
+
+ +Expand source code + +
class FileInstallationStore(InstallationStore, AsyncInstallationStore):
+    def __init__(
+        self,
+        *,
+        base_dir: str = str(Path.home()) + "/.bolt-app-installation",
+        historical_data_enabled: bool = True,
+        client_id: Optional[str] = None,
+        logger: Logger = logging.getLogger(__name__),
+    ):
+        self.base_dir = base_dir
+        self.historical_data_enabled = historical_data_enabled
+        self.client_id = client_id
+        if self.client_id is not None:
+            self.base_dir = f"{self.base_dir}/{self.client_id}"
+        self._logger = logger
+
+    @property
+    def logger(self) -> Logger:
+        if self._logger is None:
+            self._logger = logging.getLogger(__name__)
+        return self._logger
+
+    async def async_save(self, installation: Installation):
+        return self.save(installation)
+
+    async def async_save_bot(self, bot: Bot):
+        return self.save_bot(bot)
+
+    def save(self, installation: Installation):
+        none = "none"
+        e_id = installation.enterprise_id or none
+        t_id = installation.team_id or none
+        team_installation_dir = f"{self.base_dir}/{e_id}-{t_id}"
+        self._mkdir(team_installation_dir)
+
+        self.save_bot(installation.to_bot())
+
+        if self.historical_data_enabled:
+            history_version: str = str(installation.installed_at)
+
+            # per workspace
+            entity: str = json.dumps(installation.__dict__)
+            with open(f"{team_installation_dir}/installer-latest", "w") as f:
+                f.write(entity)
+            with open(f"{team_installation_dir}/installer-{history_version}", "w") as f:
+                f.write(entity)
+
+            # per workspace per user
+            u_id = installation.user_id or none
+            entity = json.dumps(installation.__dict__)
+            with open(f"{team_installation_dir}/installer-{u_id}-latest", "w") as f:
+                f.write(entity)
+            with open(f"{team_installation_dir}/installer-{u_id}-{history_version}", "w") as f:
+                f.write(entity)
+
+        else:
+            u_id = installation.user_id or none
+            installer_filepath = f"{team_installation_dir}/installer-{u_id}-latest"
+            with open(installer_filepath, "w") as f:
+                entity = json.dumps(installation.__dict__)
+                f.write(entity)
+
+    def save_bot(self, bot: Bot):
+        if bot.bot_token is None:
+            self.logger.debug("Skipped saving a new row because of the absense of bot token in it")
+            return
+
+        none = "none"
+        e_id = bot.enterprise_id or none
+        t_id = bot.team_id or none
+        team_installation_dir = f"{self.base_dir}/{e_id}-{t_id}"
+        self._mkdir(team_installation_dir)
+
+        if self.historical_data_enabled:
+            history_version: str = str(bot.installed_at)
+
+            entity: str = json.dumps(bot.__dict__)
+            with open(f"{team_installation_dir}/bot-latest", "w") as f:
+                f.write(entity)
+            with open(f"{team_installation_dir}/bot-{history_version}", "w") as f:
+                f.write(entity)
+        else:
+            with open(f"{team_installation_dir}/bot-latest", "w") as f:
+                entity = json.dumps(bot.__dict__)
+                f.write(entity)
+
+    async def async_find_bot(
+        self,
+        *,
+        enterprise_id: Optional[str],
+        team_id: Optional[str],
+        is_enterprise_install: Optional[bool] = False,
+    ) -> Optional[Bot]:
+        return self.find_bot(
+            enterprise_id=enterprise_id,
+            team_id=team_id,
+            is_enterprise_install=is_enterprise_install,
+        )
+
+    def find_bot(
+        self,
+        *,
+        enterprise_id: Optional[str],
+        team_id: Optional[str],
+        is_enterprise_install: Optional[bool] = False,
+    ) -> Optional[Bot]:
+        none = "none"
+        e_id = enterprise_id or none
+        t_id = team_id or none
+        if is_enterprise_install:
+            t_id = none
+        bot_filepath = f"{self.base_dir}/{e_id}-{t_id}/bot-latest"
+        try:
+            with open(bot_filepath) as f:
+                data = json.loads(f.read())
+                return Bot(**data)
+        except FileNotFoundError as e:
+            message = f"Installation data missing for enterprise: {e_id}, team: {t_id}: {e}"
+            self.logger.debug(message)
+            return None
+
+    async def async_find_installation(
+        self,
+        *,
+        enterprise_id: Optional[str],
+        team_id: Optional[str],
+        user_id: Optional[str] = None,
+        is_enterprise_install: Optional[bool] = False,
+    ) -> Optional[Installation]:
+        return self.find_installation(
+            enterprise_id=enterprise_id,
+            team_id=team_id,
+            user_id=user_id,
+            is_enterprise_install=is_enterprise_install,
+        )
+
+    def find_installation(
+        self,
+        *,
+        enterprise_id: Optional[str],
+        team_id: Optional[str],
+        user_id: Optional[str] = None,
+        is_enterprise_install: Optional[bool] = False,
+    ) -> Optional[Installation]:
+        none = "none"
+        e_id = enterprise_id or none
+        t_id = team_id or none
+        if is_enterprise_install:
+            t_id = none
+        installation_filepath = f"{self.base_dir}/{e_id}-{t_id}/installer-latest"
+        if user_id is not None:
+            installation_filepath = f"{self.base_dir}/{e_id}-{t_id}/installer-{user_id}-latest"
+
+        try:
+            installation: Optional[Installation] = None
+            with open(installation_filepath) as f:
+                data = json.loads(f.read())
+                installation = Installation(**data)
+
+            has_user_installation = user_id is not None and installation is not None
+            no_bot_token_installation = installation is not None and installation.bot_token is None
+            should_find_bot_installation = has_user_installation or no_bot_token_installation
+            if should_find_bot_installation:
+                # Retrieve the latest bot token, just in case
+                # See also: https://github.com/slackapi/bolt-python/issues/664
+                latest_bot_installation = self.find_bot(
+                    enterprise_id=enterprise_id,
+                    team_id=team_id,
+                    is_enterprise_install=is_enterprise_install,
+                )
+                if latest_bot_installation is not None and installation.bot_token != latest_bot_installation.bot_token:
+                    # NOTE: this logic is based on the assumption that every single installation has bot scopes
+                    # If you need to installation patterns without bot scopes in the same S3 bucket,
+                    # please fork this code and implement your own logic.
+                    installation.bot_id = latest_bot_installation.bot_id
+                    installation.bot_user_id = latest_bot_installation.bot_user_id
+                    installation.bot_token = latest_bot_installation.bot_token
+                    installation.bot_scopes = latest_bot_installation.bot_scopes
+                    installation.bot_refresh_token = latest_bot_installation.bot_refresh_token
+                    installation.bot_token_expires_at = latest_bot_installation.bot_token_expires_at
+
+            return installation
+
+        except FileNotFoundError as e:
+            message = f"Installation data missing for enterprise: {e_id}, team: {t_id}: {e}"
+            self.logger.debug(message)
+            return None
+
+    async def async_delete_bot(self, *, enterprise_id: Optional[str], team_id: Optional[str]) -> None:
+        return self.delete_bot(enterprise_id=enterprise_id, team_id=team_id)
+
+    def delete_bot(self, *, enterprise_id: Optional[str], team_id: Optional[str]) -> None:
+        none = "none"
+        e_id = enterprise_id or none
+        t_id = team_id or none
+        filepath_glob = f"{self.base_dir}/{e_id}-{t_id}/bot-*"
+        self._delete_by_glob(e_id, t_id, filepath_glob)
+
+    async def async_delete_installation(
+        self,
+        *,
+        enterprise_id: Optional[str],
+        team_id: Optional[str],
+        user_id: Optional[str] = None,
+    ) -> None:
+        return self.delete_installation(enterprise_id=enterprise_id, team_id=team_id, user_id=user_id)
+
+    def delete_installation(
+        self,
+        *,
+        enterprise_id: Optional[str],
+        team_id: Optional[str],
+        user_id: Optional[str] = None,
+    ) -> None:
+        none = "none"
+        e_id = enterprise_id or none
+        t_id = team_id or none
+        if user_id is not None:
+            filepath_glob = f"{self.base_dir}/{e_id}-{t_id}/installer-{user_id}-*"
+        else:
+            filepath_glob = f"{self.base_dir}/{e_id}-{t_id}/installer-*"
+        self._delete_by_glob(e_id, t_id, filepath_glob)
+
+    def _delete_by_glob(self, e_id: str, t_id: str, filepath_glob: str):
+        for filepath in glob.glob(filepath_glob):
+            try:
+                os.remove(filepath)
+            except FileNotFoundError as e:
+                message = f"Failed to delete installation data for enterprise: {e_id}, team: {t_id}: {e}"
+                self.logger.warning(message)
+
+    @staticmethod
+    def _mkdir(path: Union[str, Path]):
+        if isinstance(path, str):
+            path = Path(path)
+        path.mkdir(parents=True, exist_ok=True)
+
+

The installation store interface.

+

The minimum required methods are:

+
    +
  • save(installation)
  • +
  • find_installation(enterprise_id, team_id, user_id, is_enterprise_install)
  • +
+

If you would like to properly handle app uninstallations and token revocations, +the following methods should be implemented.

+
    +
  • delete_installation(enterprise_id, team_id, user_id)
  • +
  • delete_all(enterprise_id, team_id)
  • +
+

If your app needs only bot scope installations, the simpler way to implement would be:

+
    +
  • save(installation)
  • +
  • find_bot(enterprise_id, team_id, is_enterprise_install)
  • +
  • delete_bot(enterprise_id, team_id)
  • +
  • delete_all(enterprise_id, team_id)
  • +
+

Ancestors

+ +

Instance variables

+
+
prop logger : logging.Logger
+
+
+ +Expand source code + +
@property
+def logger(self) -> Logger:
+    if self._logger is None:
+        self._logger = logging.getLogger(__name__)
+    return self._logger
+
+
+
+
+

Inherited members

+ +
+
+class Installation +(*,
app_id: str | None = None,
enterprise_id: str | None = None,
enterprise_name: str | None = None,
enterprise_url: str | None = None,
team_id: str | None = None,
team_name: str | None = None,
bot_token: str | None = None,
bot_id: str | None = None,
bot_user_id: str | None = None,
bot_scopes: str | Sequence[str] = '',
bot_refresh_token: str | None = None,
bot_token_expires_in: int | None = None,
bot_token_expires_at: int | datetime.datetime | str | None = None,
user_id: str,
user_token: str | None = None,
user_scopes: str | Sequence[str] = '',
user_refresh_token: str | None = None,
user_token_expires_in: int | None = None,
user_token_expires_at: int | datetime.datetime | str | None = None,
incoming_webhook_url: str | None = None,
incoming_webhook_channel: str | None = None,
incoming_webhook_channel_id: str | None = None,
incoming_webhook_configuration_url: str | None = None,
is_enterprise_install: bool | None = False,
token_type: str | None = None,
installed_at: float | datetime.datetime | str | None = None,
custom_values: Dict[str, Any] | None = None)
+
+
+
+ +Expand source code + +
class Installation:
+    app_id: Optional[str]
+    enterprise_id: Optional[str]
+    enterprise_name: Optional[str]
+    enterprise_url: Optional[str]
+    team_id: Optional[str]
+    team_name: Optional[str]
+    bot_token: Optional[str]
+    bot_id: Optional[str]
+    bot_user_id: Optional[str]
+    bot_scopes: Optional[Sequence[str]]
+    bot_refresh_token: Optional[str]  # only when token rotation is enabled
+    # only when token rotation is enabled
+    # Unix time (seconds): only when token rotation is enabled
+    bot_token_expires_at: Optional[int]
+    user_id: str
+    user_token: Optional[str]
+    user_scopes: Optional[Sequence[str]]
+    user_refresh_token: Optional[str]  # only when token rotation is enabled
+    # Unix time (seconds): only when token rotation is enabled
+    user_token_expires_at: Optional[int]
+    incoming_webhook_url: Optional[str]
+    incoming_webhook_channel: Optional[str]
+    incoming_webhook_channel_id: Optional[str]
+    incoming_webhook_configuration_url: Optional[str]
+    is_enterprise_install: bool
+    token_type: Optional[str]
+    installed_at: float
+
+    custom_values: Dict[str, Any]
+
+    def __init__(
+        self,
+        *,
+        app_id: Optional[str] = None,
+        # org / workspace
+        enterprise_id: Optional[str] = None,
+        enterprise_name: Optional[str] = None,
+        enterprise_url: Optional[str] = None,
+        team_id: Optional[str] = None,
+        team_name: Optional[str] = None,
+        # bot
+        bot_token: Optional[str] = None,
+        bot_id: Optional[str] = None,
+        bot_user_id: Optional[str] = None,
+        bot_scopes: Union[str, Sequence[str]] = "",
+        bot_refresh_token: Optional[str] = None,  # only when token rotation is enabled
+        # only when token rotation is enabled
+        bot_token_expires_in: Optional[int] = None,
+        # only for duplicating this object
+        # only when token rotation is enabled
+        bot_token_expires_at: Optional[Union[int, datetime, str]] = None,
+        # installer
+        user_id: str,
+        user_token: Optional[str] = None,
+        user_scopes: Union[str, Sequence[str]] = "",
+        user_refresh_token: Optional[str] = None,  # only when token rotation is enabled
+        # only when token rotation is enabled
+        user_token_expires_in: Optional[int] = None,
+        # only for duplicating this object
+        # only when token rotation is enabled
+        user_token_expires_at: Optional[Union[int, datetime, str]] = None,
+        # incoming webhook
+        incoming_webhook_url: Optional[str] = None,
+        incoming_webhook_channel: Optional[str] = None,
+        incoming_webhook_channel_id: Optional[str] = None,
+        incoming_webhook_configuration_url: Optional[str] = None,
+        # org app
+        is_enterprise_install: Optional[bool] = False,
+        token_type: Optional[str] = None,
+        # timestamps
+        # The expected value type is float but the internals handle other types too
+        # for str values, we supports only ISO datetime format.
+        installed_at: Optional[Union[float, datetime, str]] = None,
+        # custom values
+        custom_values: Optional[Dict[str, Any]] = None,
+    ):
+        self.app_id = app_id
+        self.enterprise_id = enterprise_id
+        self.enterprise_name = enterprise_name
+        self.enterprise_url = enterprise_url
+        self.team_id = team_id
+        self.team_name = team_name
+        self.bot_token = bot_token
+        self.bot_id = bot_id
+        self.bot_user_id = bot_user_id
+        if isinstance(bot_scopes, str):
+            self.bot_scopes = bot_scopes.split(",") if len(bot_scopes) > 0 else []
+        else:
+            self.bot_scopes = bot_scopes
+        self.bot_refresh_token = bot_refresh_token
+
+        if bot_token_expires_at is not None:
+            self.bot_token_expires_at = _timestamp_to_type(bot_token_expires_at, int)
+        elif bot_token_expires_in is not None:
+            self.bot_token_expires_at = int(time()) + bot_token_expires_in
+        else:
+            self.bot_token_expires_at = None
+
+        self.user_id = user_id
+        self.user_token = user_token
+        if isinstance(user_scopes, str):
+            self.user_scopes = user_scopes.split(",") if len(user_scopes) > 0 else []
+        else:
+            self.user_scopes = user_scopes
+        self.user_refresh_token = user_refresh_token
+
+        if user_token_expires_at is not None:
+            self.user_token_expires_at = _timestamp_to_type(user_token_expires_at, int)
+        elif user_token_expires_in is not None:
+            self.user_token_expires_at = int(time()) + user_token_expires_in
+        else:
+            self.user_token_expires_at = None
+
+        self.incoming_webhook_url = incoming_webhook_url
+        self.incoming_webhook_channel = incoming_webhook_channel
+        self.incoming_webhook_channel_id = incoming_webhook_channel_id
+        self.incoming_webhook_configuration_url = incoming_webhook_configuration_url
+
+        self.is_enterprise_install = is_enterprise_install or False
+        self.token_type = token_type
+
+        if installed_at is None:
+            self.installed_at = datetime.now().timestamp()
+        else:
+            self.installed_at = _timestamp_to_type(installed_at, float)
+
+        self.custom_values = custom_values if custom_values is not None else {}
+
+    def to_bot(self) -> Bot:
+        return Bot(
+            app_id=self.app_id,
+            enterprise_id=self.enterprise_id,
+            enterprise_name=self.enterprise_name,
+            team_id=self.team_id,
+            team_name=self.team_name,
+            bot_token=self.bot_token,  # type: ignore[arg-type]
+            bot_id=self.bot_id,  # type: ignore[arg-type]
+            bot_user_id=self.bot_user_id,  # type: ignore[arg-type]
+            bot_scopes=self.bot_scopes,  # type: ignore[arg-type]
+            bot_refresh_token=self.bot_refresh_token,
+            bot_token_expires_at=self.bot_token_expires_at,
+            is_enterprise_install=self.is_enterprise_install,
+            installed_at=self.installed_at,
+            custom_values=self.custom_values,
+        )
+
+    def set_custom_value(self, name: str, value: Any):
+        self.custom_values[name] = value
+
+    def get_custom_value(self, name: str) -> Optional[Any]:
+        return self.custom_values.get(name)
+
+    def _to_standard_value_dict(self) -> Dict[str, Any]:
+        return {
+            "app_id": self.app_id,
+            "enterprise_id": self.enterprise_id,
+            "enterprise_name": self.enterprise_name,
+            "enterprise_url": self.enterprise_url,
+            "team_id": self.team_id,
+            "team_name": self.team_name,
+            "bot_token": self.bot_token,
+            "bot_id": self.bot_id,
+            "bot_user_id": self.bot_user_id,
+            "bot_scopes": ",".join(self.bot_scopes) if self.bot_scopes else None,
+            "bot_refresh_token": self.bot_refresh_token,
+            "bot_token_expires_at": (
+                datetime.utcfromtimestamp(self.bot_token_expires_at) if self.bot_token_expires_at is not None else None
+            ),
+            "user_id": self.user_id,
+            "user_token": self.user_token,
+            "user_scopes": ",".join(self.user_scopes) if self.user_scopes else None,
+            "user_refresh_token": self.user_refresh_token,
+            "user_token_expires_at": (
+                datetime.utcfromtimestamp(self.user_token_expires_at) if self.user_token_expires_at is not None else None
+            ),
+            "incoming_webhook_url": self.incoming_webhook_url,
+            "incoming_webhook_channel": self.incoming_webhook_channel,
+            "incoming_webhook_channel_id": self.incoming_webhook_channel_id,
+            "incoming_webhook_configuration_url": self.incoming_webhook_configuration_url,
+            "is_enterprise_install": self.is_enterprise_install,
+            "token_type": self.token_type,
+            "installed_at": datetime.utcfromtimestamp(self.installed_at),
+        }
+
+    def to_dict_for_copying(self) -> Dict[str, Any]:
+        return {"custom_values": self.custom_values, **self._to_standard_value_dict()}
+
+    def to_dict(self) -> Dict[str, Any]:
+        # prioritize standard_values over custom_values
+        # when the same keys exist in both
+        return {**self.custom_values, **self._to_standard_value_dict()}
+
+
+

Class variables

+
+
var app_id : str | None
+
+

The type of the None singleton.

+
+
var bot_id : str | None
+
+

The type of the None singleton.

+
+
var bot_refresh_token : str | None
+
+

The type of the None singleton.

+
+
var bot_scopes : Sequence[str] | None
+
+

The type of the None singleton.

+
+
var bot_token : str | None
+
+

The type of the None singleton.

+
+
var bot_token_expires_at : int | None
+
+

The type of the None singleton.

+
+
var bot_user_id : str | None
+
+

The type of the None singleton.

+
+
var custom_values : Dict[str, Any]
+
+

The type of the None singleton.

+
+
var enterprise_id : str | None
+
+

The type of the None singleton.

+
+
var enterprise_name : str | None
+
+

The type of the None singleton.

+
+
var enterprise_url : str | None
+
+

The type of the None singleton.

+
+
var incoming_webhook_channel : str | None
+
+

The type of the None singleton.

+
+
var incoming_webhook_channel_id : str | None
+
+

The type of the None singleton.

+
+
var incoming_webhook_configuration_url : str | None
+
+

The type of the None singleton.

+
+
var incoming_webhook_url : str | None
+
+

The type of the None singleton.

+
+
var installed_at : float
+
+

The type of the None singleton.

+
+
var is_enterprise_install : bool
+
+

The type of the None singleton.

+
+
var team_id : str | None
+
+

The type of the None singleton.

+
+
var team_name : str | None
+
+

The type of the None singleton.

+
+
var token_type : str | None
+
+

The type of the None singleton.

+
+
var user_id : str
+
+

The type of the None singleton.

+
+
var user_refresh_token : str | None
+
+

The type of the None singleton.

+
+
var user_scopes : Sequence[str] | None
+
+

The type of the None singleton.

+
+
var user_token : str | None
+
+

The type of the None singleton.

+
+
var user_token_expires_at : int | None
+
+

The type of the None singleton.

+
+
+

Methods

+
+
+def get_custom_value(self, name: str) ‑> Any | None +
+
+
+ +Expand source code + +
def get_custom_value(self, name: str) -> Optional[Any]:
+    return self.custom_values.get(name)
+
+
+
+
+def set_custom_value(self, name: str, value: Any) +
+
+
+ +Expand source code + +
def set_custom_value(self, name: str, value: Any):
+    self.custom_values[name] = value
+
+
+
+
+def to_bot(self) ‑> Bot +
+
+
+ +Expand source code + +
def to_bot(self) -> Bot:
+    return Bot(
+        app_id=self.app_id,
+        enterprise_id=self.enterprise_id,
+        enterprise_name=self.enterprise_name,
+        team_id=self.team_id,
+        team_name=self.team_name,
+        bot_token=self.bot_token,  # type: ignore[arg-type]
+        bot_id=self.bot_id,  # type: ignore[arg-type]
+        bot_user_id=self.bot_user_id,  # type: ignore[arg-type]
+        bot_scopes=self.bot_scopes,  # type: ignore[arg-type]
+        bot_refresh_token=self.bot_refresh_token,
+        bot_token_expires_at=self.bot_token_expires_at,
+        is_enterprise_install=self.is_enterprise_install,
+        installed_at=self.installed_at,
+        custom_values=self.custom_values,
+    )
+
+
+
+
+def to_dict(self) ‑> Dict[str, Any] +
+
+
+ +Expand source code + +
def to_dict(self) -> Dict[str, Any]:
+    # prioritize standard_values over custom_values
+    # when the same keys exist in both
+    return {**self.custom_values, **self._to_standard_value_dict()}
+
+
+
+
+def to_dict_for_copying(self) ‑> Dict[str, Any] +
+
+
+ +Expand source code + +
def to_dict_for_copying(self) -> Dict[str, Any]:
+    return {"custom_values": self.custom_values, **self._to_standard_value_dict()}
+
+
+
+
+
+
+class InstallationStore +
+
+
+ +Expand source code + +
class InstallationStore:
+    """The installation store interface.
+
+    The minimum required methods are:
+
+    * save(installation)
+    * find_installation(enterprise_id, team_id, user_id, is_enterprise_install)
+
+    If you would like to properly handle app uninstallations and token revocations,
+    the following methods should be implemented.
+
+    * delete_installation(enterprise_id, team_id, user_id)
+    * delete_all(enterprise_id, team_id)
+
+    If your app needs only bot scope installations, the simpler way to implement would be:
+
+    * save(installation)
+    * find_bot(enterprise_id, team_id, is_enterprise_install)
+    * delete_bot(enterprise_id, team_id)
+    * delete_all(enterprise_id, team_id)
+    """
+
+    @property
+    def logger(self) -> Logger:
+        raise NotImplementedError()
+
+    def save(self, installation: Installation):
+        """Saves an installation data"""
+        raise NotImplementedError()
+
+    def save_bot(self, bot: Bot):
+        """Saves a bot installation data"""
+        raise NotImplementedError()
+
+    def find_bot(
+        self,
+        *,
+        enterprise_id: Optional[str],
+        team_id: Optional[str],
+        is_enterprise_install: Optional[bool] = False,
+    ) -> Optional[Bot]:
+        """Finds a bot scope installation per workspace / org"""
+        raise NotImplementedError()
+
+    def find_installation(
+        self,
+        *,
+        enterprise_id: Optional[str],
+        team_id: Optional[str],
+        user_id: Optional[str] = None,
+        is_enterprise_install: Optional[bool] = False,
+    ) -> Optional[Installation]:
+        """Finds a relevant installation for the given IDs.
+        If the user_id is absent, this method may return the latest installation in the workspace / org.
+        """
+        raise NotImplementedError()
+
+    def delete_bot(
+        self,
+        *,
+        enterprise_id: Optional[str],
+        team_id: Optional[str],
+    ) -> None:
+        """Deletes a bot scope installation per workspace / org"""
+        raise NotImplementedError()
+
+    def delete_installation(
+        self,
+        *,
+        enterprise_id: Optional[str],
+        team_id: Optional[str],
+        user_id: Optional[str] = None,
+    ) -> None:
+        """Deletes an installation that matches the given IDs"""
+        raise NotImplementedError()
+
+    def delete_all(
+        self,
+        *,
+        enterprise_id: Optional[str],
+        team_id: Optional[str],
+    ):
+        """Deletes all installation data for the given workspace / org"""
+        self.delete_bot(enterprise_id=enterprise_id, team_id=team_id)
+        self.delete_installation(enterprise_id=enterprise_id, team_id=team_id)
+
+

The installation store interface.

+

The minimum required methods are:

+
    +
  • save(installation)
  • +
  • find_installation(enterprise_id, team_id, user_id, is_enterprise_install)
  • +
+

If you would like to properly handle app uninstallations and token revocations, +the following methods should be implemented.

+
    +
  • delete_installation(enterprise_id, team_id, user_id)
  • +
  • delete_all(enterprise_id, team_id)
  • +
+

If your app needs only bot scope installations, the simpler way to implement would be:

+
    +
  • save(installation)
  • +
  • find_bot(enterprise_id, team_id, is_enterprise_install)
  • +
  • delete_bot(enterprise_id, team_id)
  • +
  • delete_all(enterprise_id, team_id)
  • +
+

Subclasses

+ +

Instance variables

+
+
prop logger : logging.Logger
+
+
+ +Expand source code + +
@property
+def logger(self) -> Logger:
+    raise NotImplementedError()
+
+
+
+
+

Methods

+
+
+def delete_all(self, *, enterprise_id: str | None, team_id: str | None) +
+
+
+ +Expand source code + +
def delete_all(
+    self,
+    *,
+    enterprise_id: Optional[str],
+    team_id: Optional[str],
+):
+    """Deletes all installation data for the given workspace / org"""
+    self.delete_bot(enterprise_id=enterprise_id, team_id=team_id)
+    self.delete_installation(enterprise_id=enterprise_id, team_id=team_id)
+
+

Deletes all installation data for the given workspace / org

+
+
+def delete_bot(self, *, enterprise_id: str | None, team_id: str | None) ‑> None +
+
+
+ +Expand source code + +
def delete_bot(
+    self,
+    *,
+    enterprise_id: Optional[str],
+    team_id: Optional[str],
+) -> None:
+    """Deletes a bot scope installation per workspace / org"""
+    raise NotImplementedError()
+
+

Deletes a bot scope installation per workspace / org

+
+
+def delete_installation(self, *, enterprise_id: str | None, team_id: str | None, user_id: str | None = None) ‑> None +
+
+
+ +Expand source code + +
def delete_installation(
+    self,
+    *,
+    enterprise_id: Optional[str],
+    team_id: Optional[str],
+    user_id: Optional[str] = None,
+) -> None:
+    """Deletes an installation that matches the given IDs"""
+    raise NotImplementedError()
+
+

Deletes an installation that matches the given IDs

+
+
+def find_bot(self,
*,
enterprise_id: str | None,
team_id: str | None,
is_enterprise_install: bool | None = False) ‑> Bot | None
+
+
+
+ +Expand source code + +
def find_bot(
+    self,
+    *,
+    enterprise_id: Optional[str],
+    team_id: Optional[str],
+    is_enterprise_install: Optional[bool] = False,
+) -> Optional[Bot]:
+    """Finds a bot scope installation per workspace / org"""
+    raise NotImplementedError()
+
+

Finds a bot scope installation per workspace / org

+
+
+def find_installation(self,
*,
enterprise_id: str | None,
team_id: str | None,
user_id: str | None = None,
is_enterprise_install: bool | None = False) ‑> Installation | None
+
+
+
+ +Expand source code + +
def find_installation(
+    self,
+    *,
+    enterprise_id: Optional[str],
+    team_id: Optional[str],
+    user_id: Optional[str] = None,
+    is_enterprise_install: Optional[bool] = False,
+) -> Optional[Installation]:
+    """Finds a relevant installation for the given IDs.
+    If the user_id is absent, this method may return the latest installation in the workspace / org.
+    """
+    raise NotImplementedError()
+
+

Finds a relevant installation for the given IDs. +If the user_id is absent, this method may return the latest installation in the workspace / org.

+
+
+def save(self,
installation: Installation)
+
+
+
+ +Expand source code + +
def save(self, installation: Installation):
+    """Saves an installation data"""
+    raise NotImplementedError()
+
+

Saves an installation data

+
+
+def save_bot(self,
bot: Bot)
+
+
+
+ +Expand source code + +
def save_bot(self, bot: Bot):
+    """Saves a bot installation data"""
+    raise NotImplementedError()
+
+

Saves a bot installation data

+
+
+
+
+
+
+ +
+ + + diff --git a/docs/reference/oauth/installation_store/installation_store.html b/docs/reference/oauth/installation_store/installation_store.html new file mode 100644 index 000000000..86533f424 --- /dev/null +++ b/docs/reference/oauth/installation_store/installation_store.html @@ -0,0 +1,359 @@ + + + + + + +slack_sdk.oauth.installation_store.installation_store API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.oauth.installation_store.installation_store

+
+
+

Slack installation data store

+

Refer to https://docs.slack.dev/tools/python-slack-sdk/oauth for details.

+
+
+
+
+
+
+
+
+

Classes

+
+
+class InstallationStore +
+
+
+ +Expand source code + +
class InstallationStore:
+    """The installation store interface.
+
+    The minimum required methods are:
+
+    * save(installation)
+    * find_installation(enterprise_id, team_id, user_id, is_enterprise_install)
+
+    If you would like to properly handle app uninstallations and token revocations,
+    the following methods should be implemented.
+
+    * delete_installation(enterprise_id, team_id, user_id)
+    * delete_all(enterprise_id, team_id)
+
+    If your app needs only bot scope installations, the simpler way to implement would be:
+
+    * save(installation)
+    * find_bot(enterprise_id, team_id, is_enterprise_install)
+    * delete_bot(enterprise_id, team_id)
+    * delete_all(enterprise_id, team_id)
+    """
+
+    @property
+    def logger(self) -> Logger:
+        raise NotImplementedError()
+
+    def save(self, installation: Installation):
+        """Saves an installation data"""
+        raise NotImplementedError()
+
+    def save_bot(self, bot: Bot):
+        """Saves a bot installation data"""
+        raise NotImplementedError()
+
+    def find_bot(
+        self,
+        *,
+        enterprise_id: Optional[str],
+        team_id: Optional[str],
+        is_enterprise_install: Optional[bool] = False,
+    ) -> Optional[Bot]:
+        """Finds a bot scope installation per workspace / org"""
+        raise NotImplementedError()
+
+    def find_installation(
+        self,
+        *,
+        enterprise_id: Optional[str],
+        team_id: Optional[str],
+        user_id: Optional[str] = None,
+        is_enterprise_install: Optional[bool] = False,
+    ) -> Optional[Installation]:
+        """Finds a relevant installation for the given IDs.
+        If the user_id is absent, this method may return the latest installation in the workspace / org.
+        """
+        raise NotImplementedError()
+
+    def delete_bot(
+        self,
+        *,
+        enterprise_id: Optional[str],
+        team_id: Optional[str],
+    ) -> None:
+        """Deletes a bot scope installation per workspace / org"""
+        raise NotImplementedError()
+
+    def delete_installation(
+        self,
+        *,
+        enterprise_id: Optional[str],
+        team_id: Optional[str],
+        user_id: Optional[str] = None,
+    ) -> None:
+        """Deletes an installation that matches the given IDs"""
+        raise NotImplementedError()
+
+    def delete_all(
+        self,
+        *,
+        enterprise_id: Optional[str],
+        team_id: Optional[str],
+    ):
+        """Deletes all installation data for the given workspace / org"""
+        self.delete_bot(enterprise_id=enterprise_id, team_id=team_id)
+        self.delete_installation(enterprise_id=enterprise_id, team_id=team_id)
+
+

The installation store interface.

+

The minimum required methods are:

+
    +
  • save(installation)
  • +
  • find_installation(enterprise_id, team_id, user_id, is_enterprise_install)
  • +
+

If you would like to properly handle app uninstallations and token revocations, +the following methods should be implemented.

+
    +
  • delete_installation(enterprise_id, team_id, user_id)
  • +
  • delete_all(enterprise_id, team_id)
  • +
+

If your app needs only bot scope installations, the simpler way to implement would be:

+
    +
  • save(installation)
  • +
  • find_bot(enterprise_id, team_id, is_enterprise_install)
  • +
  • delete_bot(enterprise_id, team_id)
  • +
  • delete_all(enterprise_id, team_id)
  • +
+

Subclasses

+ +

Instance variables

+
+
prop logger : logging.Logger
+
+
+ +Expand source code + +
@property
+def logger(self) -> Logger:
+    raise NotImplementedError()
+
+
+
+
+

Methods

+
+
+def delete_all(self, *, enterprise_id: str | None, team_id: str | None) +
+
+
+ +Expand source code + +
def delete_all(
+    self,
+    *,
+    enterprise_id: Optional[str],
+    team_id: Optional[str],
+):
+    """Deletes all installation data for the given workspace / org"""
+    self.delete_bot(enterprise_id=enterprise_id, team_id=team_id)
+    self.delete_installation(enterprise_id=enterprise_id, team_id=team_id)
+
+

Deletes all installation data for the given workspace / org

+
+
+def delete_bot(self, *, enterprise_id: str | None, team_id: str | None) ‑> None +
+
+
+ +Expand source code + +
def delete_bot(
+    self,
+    *,
+    enterprise_id: Optional[str],
+    team_id: Optional[str],
+) -> None:
+    """Deletes a bot scope installation per workspace / org"""
+    raise NotImplementedError()
+
+

Deletes a bot scope installation per workspace / org

+
+
+def delete_installation(self, *, enterprise_id: str | None, team_id: str | None, user_id: str | None = None) ‑> None +
+
+
+ +Expand source code + +
def delete_installation(
+    self,
+    *,
+    enterprise_id: Optional[str],
+    team_id: Optional[str],
+    user_id: Optional[str] = None,
+) -> None:
+    """Deletes an installation that matches the given IDs"""
+    raise NotImplementedError()
+
+

Deletes an installation that matches the given IDs

+
+
+def find_bot(self,
*,
enterprise_id: str | None,
team_id: str | None,
is_enterprise_install: bool | None = False) ‑> Bot | None
+
+
+
+ +Expand source code + +
def find_bot(
+    self,
+    *,
+    enterprise_id: Optional[str],
+    team_id: Optional[str],
+    is_enterprise_install: Optional[bool] = False,
+) -> Optional[Bot]:
+    """Finds a bot scope installation per workspace / org"""
+    raise NotImplementedError()
+
+

Finds a bot scope installation per workspace / org

+
+
+def find_installation(self,
*,
enterprise_id: str | None,
team_id: str | None,
user_id: str | None = None,
is_enterprise_install: bool | None = False) ‑> Installation | None
+
+
+
+ +Expand source code + +
def find_installation(
+    self,
+    *,
+    enterprise_id: Optional[str],
+    team_id: Optional[str],
+    user_id: Optional[str] = None,
+    is_enterprise_install: Optional[bool] = False,
+) -> Optional[Installation]:
+    """Finds a relevant installation for the given IDs.
+    If the user_id is absent, this method may return the latest installation in the workspace / org.
+    """
+    raise NotImplementedError()
+
+

Finds a relevant installation for the given IDs. +If the user_id is absent, this method may return the latest installation in the workspace / org.

+
+
+def save(self,
installation: Installation)
+
+
+
+ +Expand source code + +
def save(self, installation: Installation):
+    """Saves an installation data"""
+    raise NotImplementedError()
+
+

Saves an installation data

+
+
+def save_bot(self,
bot: Bot)
+
+
+
+ +Expand source code + +
def save_bot(self, bot: Bot):
+    """Saves a bot installation data"""
+    raise NotImplementedError()
+
+

Saves a bot installation data

+
+
+
+
+
+
+ +
+ + + diff --git a/docs/reference/oauth/installation_store/internals.html b/docs/reference/oauth/installation_store/internals.html new file mode 100644 index 000000000..4bf376755 --- /dev/null +++ b/docs/reference/oauth/installation_store/internals.html @@ -0,0 +1,66 @@ + + + + + + +slack_sdk.oauth.installation_store.internals API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.oauth.installation_store.internals

+
+
+
+
+
+
+
+
+
+
+
+
+ +
+ + + diff --git a/docs/reference/oauth/installation_store/models/bot.html b/docs/reference/oauth/installation_store/models/bot.html new file mode 100644 index 000000000..bb6182df2 --- /dev/null +++ b/docs/reference/oauth/installation_store/models/bot.html @@ -0,0 +1,332 @@ + + + + + + +slack_sdk.oauth.installation_store.models.bot API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.oauth.installation_store.models.bot

+
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class Bot +(*,
app_id: str | None = None,
enterprise_id: str | None = None,
enterprise_name: str | None = None,
team_id: str | None = None,
team_name: str | None = None,
bot_token: str,
bot_id: str,
bot_user_id: str,
bot_scopes: str | Sequence[str] = '',
bot_refresh_token: str | None = None,
bot_token_expires_in: int | None = None,
bot_token_expires_at: int | datetime.datetime | str | None = None,
is_enterprise_install: bool | None = False,
installed_at: float | datetime.datetime | str,
custom_values: Dict[str, Any] | None = None)
+
+
+
+ +Expand source code + +
class Bot:
+    app_id: Optional[str]
+    enterprise_id: Optional[str]
+    enterprise_name: Optional[str]
+    team_id: Optional[str]
+    team_name: Optional[str]
+    bot_token: str
+    bot_id: str
+    bot_user_id: str
+    bot_scopes: Sequence[str]
+    # only when token rotation is enabled
+    bot_refresh_token: Optional[str]
+    # only when token rotation is enabled
+    bot_token_expires_at: Optional[int]
+    is_enterprise_install: bool
+    installed_at: float
+
+    custom_values: Dict[str, Any]
+
+    def __init__(
+        self,
+        *,
+        app_id: Optional[str] = None,
+        # org / workspace
+        enterprise_id: Optional[str] = None,
+        enterprise_name: Optional[str] = None,
+        team_id: Optional[str] = None,
+        team_name: Optional[str] = None,
+        # bot
+        bot_token: str,
+        bot_id: str,
+        bot_user_id: str,
+        bot_scopes: Union[str, Sequence[str]] = "",
+        # only when token rotation is enabled
+        bot_refresh_token: Optional[str] = None,
+        # only when token rotation is enabled
+        bot_token_expires_in: Optional[int] = None,
+        # only for duplicating this object
+        # only when token rotation is enabled
+        bot_token_expires_at: Optional[Union[int, datetime, str]] = None,
+        is_enterprise_install: Optional[bool] = False,
+        # timestamps
+        # The expected value type is float but the internals handle other types too
+        # for str values, we support only ISO datetime format.
+        installed_at: Union[float, datetime, str],
+        # custom values
+        custom_values: Optional[Dict[str, Any]] = None,
+    ):
+        self.app_id = app_id
+        self.enterprise_id = enterprise_id
+        self.enterprise_name = enterprise_name
+        self.team_id = team_id
+        self.team_name = team_name
+
+        self.bot_token = bot_token
+        self.bot_id = bot_id
+        self.bot_user_id = bot_user_id
+        if isinstance(bot_scopes, str):
+            self.bot_scopes = bot_scopes.split(",") if len(bot_scopes) > 0 else []
+        else:
+            self.bot_scopes = bot_scopes
+        self.bot_refresh_token = bot_refresh_token
+
+        if bot_token_expires_at is not None:
+            self.bot_token_expires_at = _timestamp_to_type(bot_token_expires_at, int)
+        elif bot_token_expires_in is not None:
+            self.bot_token_expires_at = int(time()) + bot_token_expires_in
+        else:
+            self.bot_token_expires_at = None
+
+        self.is_enterprise_install = is_enterprise_install or False
+
+        self.installed_at = _timestamp_to_type(installed_at, float)
+
+        self.custom_values = custom_values if custom_values is not None else {}
+
+    def set_custom_value(self, name: str, value: Any):
+        self.custom_values[name] = value
+
+    def get_custom_value(self, name: str) -> Optional[Any]:
+        return self.custom_values.get(name)
+
+    def _to_standard_value_dict(self) -> Dict[str, Any]:
+        return {
+            "app_id": self.app_id,
+            "enterprise_id": self.enterprise_id,
+            "enterprise_name": self.enterprise_name,
+            "team_id": self.team_id,
+            "team_name": self.team_name,
+            "bot_token": self.bot_token,
+            "bot_id": self.bot_id,
+            "bot_user_id": self.bot_user_id,
+            "bot_scopes": ",".join(self.bot_scopes) if self.bot_scopes else None,
+            "bot_refresh_token": self.bot_refresh_token,
+            "bot_token_expires_at": (
+                datetime.utcfromtimestamp(self.bot_token_expires_at) if self.bot_token_expires_at is not None else None
+            ),
+            "is_enterprise_install": self.is_enterprise_install,
+            "installed_at": datetime.utcfromtimestamp(self.installed_at),
+        }
+
+    def to_dict_for_copying(self) -> Dict[str, Any]:
+        return {"custom_values": self.custom_values, **self._to_standard_value_dict()}
+
+    def to_dict(self) -> Dict[str, Any]:
+        # prioritize standard_values over custom_values
+        # when the same keys exist in both
+        return {**self.custom_values, **self._to_standard_value_dict()}
+
+
+

Class variables

+
+
var app_id : str | None
+
+

The type of the None singleton.

+
+
var bot_id : str
+
+

The type of the None singleton.

+
+
var bot_refresh_token : str | None
+
+

The type of the None singleton.

+
+
var bot_scopes : Sequence[str]
+
+

The type of the None singleton.

+
+
var bot_token : str
+
+

The type of the None singleton.

+
+
var bot_token_expires_at : int | None
+
+

The type of the None singleton.

+
+
var bot_user_id : str
+
+

The type of the None singleton.

+
+
var custom_values : Dict[str, Any]
+
+

The type of the None singleton.

+
+
var enterprise_id : str | None
+
+

The type of the None singleton.

+
+
var enterprise_name : str | None
+
+

The type of the None singleton.

+
+
var installed_at : float
+
+

The type of the None singleton.

+
+
var is_enterprise_install : bool
+
+

The type of the None singleton.

+
+
var team_id : str | None
+
+

The type of the None singleton.

+
+
var team_name : str | None
+
+

The type of the None singleton.

+
+
+

Methods

+
+
+def get_custom_value(self, name: str) ‑> Any | None +
+
+
+ +Expand source code + +
def get_custom_value(self, name: str) -> Optional[Any]:
+    return self.custom_values.get(name)
+
+
+
+
+def set_custom_value(self, name: str, value: Any) +
+
+
+ +Expand source code + +
def set_custom_value(self, name: str, value: Any):
+    self.custom_values[name] = value
+
+
+
+
+def to_dict(self) ‑> Dict[str, Any] +
+
+
+ +Expand source code + +
def to_dict(self) -> Dict[str, Any]:
+    # prioritize standard_values over custom_values
+    # when the same keys exist in both
+    return {**self.custom_values, **self._to_standard_value_dict()}
+
+
+
+
+def to_dict_for_copying(self) ‑> Dict[str, Any] +
+
+
+ +Expand source code + +
def to_dict_for_copying(self) -> Dict[str, Any]:
+    return {"custom_values": self.custom_values, **self._to_standard_value_dict()}
+
+
+
+
+
+
+
+
+ +
+ + + diff --git a/docs/reference/oauth/installation_store/models/index.html b/docs/reference/oauth/installation_store/models/index.html new file mode 100644 index 000000000..b96d16179 --- /dev/null +++ b/docs/reference/oauth/installation_store/models/index.html @@ -0,0 +1,776 @@ + + + + + + +slack_sdk.oauth.installation_store.models API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.oauth.installation_store.models

+
+
+
+
+

Sub-modules

+
+
slack_sdk.oauth.installation_store.models.bot
+
+
+
+
slack_sdk.oauth.installation_store.models.installation
+
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class Bot +(*,
app_id: str | None = None,
enterprise_id: str | None = None,
enterprise_name: str | None = None,
team_id: str | None = None,
team_name: str | None = None,
bot_token: str,
bot_id: str,
bot_user_id: str,
bot_scopes: str | Sequence[str] = '',
bot_refresh_token: str | None = None,
bot_token_expires_in: int | None = None,
bot_token_expires_at: int | datetime.datetime | str | None = None,
is_enterprise_install: bool | None = False,
installed_at: float | datetime.datetime | str,
custom_values: Dict[str, Any] | None = None)
+
+
+
+ +Expand source code + +
class Bot:
+    app_id: Optional[str]
+    enterprise_id: Optional[str]
+    enterprise_name: Optional[str]
+    team_id: Optional[str]
+    team_name: Optional[str]
+    bot_token: str
+    bot_id: str
+    bot_user_id: str
+    bot_scopes: Sequence[str]
+    # only when token rotation is enabled
+    bot_refresh_token: Optional[str]
+    # only when token rotation is enabled
+    bot_token_expires_at: Optional[int]
+    is_enterprise_install: bool
+    installed_at: float
+
+    custom_values: Dict[str, Any]
+
+    def __init__(
+        self,
+        *,
+        app_id: Optional[str] = None,
+        # org / workspace
+        enterprise_id: Optional[str] = None,
+        enterprise_name: Optional[str] = None,
+        team_id: Optional[str] = None,
+        team_name: Optional[str] = None,
+        # bot
+        bot_token: str,
+        bot_id: str,
+        bot_user_id: str,
+        bot_scopes: Union[str, Sequence[str]] = "",
+        # only when token rotation is enabled
+        bot_refresh_token: Optional[str] = None,
+        # only when token rotation is enabled
+        bot_token_expires_in: Optional[int] = None,
+        # only for duplicating this object
+        # only when token rotation is enabled
+        bot_token_expires_at: Optional[Union[int, datetime, str]] = None,
+        is_enterprise_install: Optional[bool] = False,
+        # timestamps
+        # The expected value type is float but the internals handle other types too
+        # for str values, we support only ISO datetime format.
+        installed_at: Union[float, datetime, str],
+        # custom values
+        custom_values: Optional[Dict[str, Any]] = None,
+    ):
+        self.app_id = app_id
+        self.enterprise_id = enterprise_id
+        self.enterprise_name = enterprise_name
+        self.team_id = team_id
+        self.team_name = team_name
+
+        self.bot_token = bot_token
+        self.bot_id = bot_id
+        self.bot_user_id = bot_user_id
+        if isinstance(bot_scopes, str):
+            self.bot_scopes = bot_scopes.split(",") if len(bot_scopes) > 0 else []
+        else:
+            self.bot_scopes = bot_scopes
+        self.bot_refresh_token = bot_refresh_token
+
+        if bot_token_expires_at is not None:
+            self.bot_token_expires_at = _timestamp_to_type(bot_token_expires_at, int)
+        elif bot_token_expires_in is not None:
+            self.bot_token_expires_at = int(time()) + bot_token_expires_in
+        else:
+            self.bot_token_expires_at = None
+
+        self.is_enterprise_install = is_enterprise_install or False
+
+        self.installed_at = _timestamp_to_type(installed_at, float)
+
+        self.custom_values = custom_values if custom_values is not None else {}
+
+    def set_custom_value(self, name: str, value: Any):
+        self.custom_values[name] = value
+
+    def get_custom_value(self, name: str) -> Optional[Any]:
+        return self.custom_values.get(name)
+
+    def _to_standard_value_dict(self) -> Dict[str, Any]:
+        return {
+            "app_id": self.app_id,
+            "enterprise_id": self.enterprise_id,
+            "enterprise_name": self.enterprise_name,
+            "team_id": self.team_id,
+            "team_name": self.team_name,
+            "bot_token": self.bot_token,
+            "bot_id": self.bot_id,
+            "bot_user_id": self.bot_user_id,
+            "bot_scopes": ",".join(self.bot_scopes) if self.bot_scopes else None,
+            "bot_refresh_token": self.bot_refresh_token,
+            "bot_token_expires_at": (
+                datetime.utcfromtimestamp(self.bot_token_expires_at) if self.bot_token_expires_at is not None else None
+            ),
+            "is_enterprise_install": self.is_enterprise_install,
+            "installed_at": datetime.utcfromtimestamp(self.installed_at),
+        }
+
+    def to_dict_for_copying(self) -> Dict[str, Any]:
+        return {"custom_values": self.custom_values, **self._to_standard_value_dict()}
+
+    def to_dict(self) -> Dict[str, Any]:
+        # prioritize standard_values over custom_values
+        # when the same keys exist in both
+        return {**self.custom_values, **self._to_standard_value_dict()}
+
+
+

Class variables

+
+
var app_id : str | None
+
+

The type of the None singleton.

+
+
var bot_id : str
+
+

The type of the None singleton.

+
+
var bot_refresh_token : str | None
+
+

The type of the None singleton.

+
+
var bot_scopes : Sequence[str]
+
+

The type of the None singleton.

+
+
var bot_token : str
+
+

The type of the None singleton.

+
+
var bot_token_expires_at : int | None
+
+

The type of the None singleton.

+
+
var bot_user_id : str
+
+

The type of the None singleton.

+
+
var custom_values : Dict[str, Any]
+
+

The type of the None singleton.

+
+
var enterprise_id : str | None
+
+

The type of the None singleton.

+
+
var enterprise_name : str | None
+
+

The type of the None singleton.

+
+
var installed_at : float
+
+

The type of the None singleton.

+
+
var is_enterprise_install : bool
+
+

The type of the None singleton.

+
+
var team_id : str | None
+
+

The type of the None singleton.

+
+
var team_name : str | None
+
+

The type of the None singleton.

+
+
+

Methods

+
+
+def get_custom_value(self, name: str) ‑> Any | None +
+
+
+ +Expand source code + +
def get_custom_value(self, name: str) -> Optional[Any]:
+    return self.custom_values.get(name)
+
+
+
+
+def set_custom_value(self, name: str, value: Any) +
+
+
+ +Expand source code + +
def set_custom_value(self, name: str, value: Any):
+    self.custom_values[name] = value
+
+
+
+
+def to_dict(self) ‑> Dict[str, Any] +
+
+
+ +Expand source code + +
def to_dict(self) -> Dict[str, Any]:
+    # prioritize standard_values over custom_values
+    # when the same keys exist in both
+    return {**self.custom_values, **self._to_standard_value_dict()}
+
+
+
+
+def to_dict_for_copying(self) ‑> Dict[str, Any] +
+
+
+ +Expand source code + +
def to_dict_for_copying(self) -> Dict[str, Any]:
+    return {"custom_values": self.custom_values, **self._to_standard_value_dict()}
+
+
+
+
+
+
+class Installation +(*,
app_id: str | None = None,
enterprise_id: str | None = None,
enterprise_name: str | None = None,
enterprise_url: str | None = None,
team_id: str | None = None,
team_name: str | None = None,
bot_token: str | None = None,
bot_id: str | None = None,
bot_user_id: str | None = None,
bot_scopes: str | Sequence[str] = '',
bot_refresh_token: str | None = None,
bot_token_expires_in: int | None = None,
bot_token_expires_at: int | datetime.datetime | str | None = None,
user_id: str,
user_token: str | None = None,
user_scopes: str | Sequence[str] = '',
user_refresh_token: str | None = None,
user_token_expires_in: int | None = None,
user_token_expires_at: int | datetime.datetime | str | None = None,
incoming_webhook_url: str | None = None,
incoming_webhook_channel: str | None = None,
incoming_webhook_channel_id: str | None = None,
incoming_webhook_configuration_url: str | None = None,
is_enterprise_install: bool | None = False,
token_type: str | None = None,
installed_at: float | datetime.datetime | str | None = None,
custom_values: Dict[str, Any] | None = None)
+
+
+
+ +Expand source code + +
class Installation:
+    app_id: Optional[str]
+    enterprise_id: Optional[str]
+    enterprise_name: Optional[str]
+    enterprise_url: Optional[str]
+    team_id: Optional[str]
+    team_name: Optional[str]
+    bot_token: Optional[str]
+    bot_id: Optional[str]
+    bot_user_id: Optional[str]
+    bot_scopes: Optional[Sequence[str]]
+    bot_refresh_token: Optional[str]  # only when token rotation is enabled
+    # only when token rotation is enabled
+    # Unix time (seconds): only when token rotation is enabled
+    bot_token_expires_at: Optional[int]
+    user_id: str
+    user_token: Optional[str]
+    user_scopes: Optional[Sequence[str]]
+    user_refresh_token: Optional[str]  # only when token rotation is enabled
+    # Unix time (seconds): only when token rotation is enabled
+    user_token_expires_at: Optional[int]
+    incoming_webhook_url: Optional[str]
+    incoming_webhook_channel: Optional[str]
+    incoming_webhook_channel_id: Optional[str]
+    incoming_webhook_configuration_url: Optional[str]
+    is_enterprise_install: bool
+    token_type: Optional[str]
+    installed_at: float
+
+    custom_values: Dict[str, Any]
+
+    def __init__(
+        self,
+        *,
+        app_id: Optional[str] = None,
+        # org / workspace
+        enterprise_id: Optional[str] = None,
+        enterprise_name: Optional[str] = None,
+        enterprise_url: Optional[str] = None,
+        team_id: Optional[str] = None,
+        team_name: Optional[str] = None,
+        # bot
+        bot_token: Optional[str] = None,
+        bot_id: Optional[str] = None,
+        bot_user_id: Optional[str] = None,
+        bot_scopes: Union[str, Sequence[str]] = "",
+        bot_refresh_token: Optional[str] = None,  # only when token rotation is enabled
+        # only when token rotation is enabled
+        bot_token_expires_in: Optional[int] = None,
+        # only for duplicating this object
+        # only when token rotation is enabled
+        bot_token_expires_at: Optional[Union[int, datetime, str]] = None,
+        # installer
+        user_id: str,
+        user_token: Optional[str] = None,
+        user_scopes: Union[str, Sequence[str]] = "",
+        user_refresh_token: Optional[str] = None,  # only when token rotation is enabled
+        # only when token rotation is enabled
+        user_token_expires_in: Optional[int] = None,
+        # only for duplicating this object
+        # only when token rotation is enabled
+        user_token_expires_at: Optional[Union[int, datetime, str]] = None,
+        # incoming webhook
+        incoming_webhook_url: Optional[str] = None,
+        incoming_webhook_channel: Optional[str] = None,
+        incoming_webhook_channel_id: Optional[str] = None,
+        incoming_webhook_configuration_url: Optional[str] = None,
+        # org app
+        is_enterprise_install: Optional[bool] = False,
+        token_type: Optional[str] = None,
+        # timestamps
+        # The expected value type is float but the internals handle other types too
+        # for str values, we supports only ISO datetime format.
+        installed_at: Optional[Union[float, datetime, str]] = None,
+        # custom values
+        custom_values: Optional[Dict[str, Any]] = None,
+    ):
+        self.app_id = app_id
+        self.enterprise_id = enterprise_id
+        self.enterprise_name = enterprise_name
+        self.enterprise_url = enterprise_url
+        self.team_id = team_id
+        self.team_name = team_name
+        self.bot_token = bot_token
+        self.bot_id = bot_id
+        self.bot_user_id = bot_user_id
+        if isinstance(bot_scopes, str):
+            self.bot_scopes = bot_scopes.split(",") if len(bot_scopes) > 0 else []
+        else:
+            self.bot_scopes = bot_scopes
+        self.bot_refresh_token = bot_refresh_token
+
+        if bot_token_expires_at is not None:
+            self.bot_token_expires_at = _timestamp_to_type(bot_token_expires_at, int)
+        elif bot_token_expires_in is not None:
+            self.bot_token_expires_at = int(time()) + bot_token_expires_in
+        else:
+            self.bot_token_expires_at = None
+
+        self.user_id = user_id
+        self.user_token = user_token
+        if isinstance(user_scopes, str):
+            self.user_scopes = user_scopes.split(",") if len(user_scopes) > 0 else []
+        else:
+            self.user_scopes = user_scopes
+        self.user_refresh_token = user_refresh_token
+
+        if user_token_expires_at is not None:
+            self.user_token_expires_at = _timestamp_to_type(user_token_expires_at, int)
+        elif user_token_expires_in is not None:
+            self.user_token_expires_at = int(time()) + user_token_expires_in
+        else:
+            self.user_token_expires_at = None
+
+        self.incoming_webhook_url = incoming_webhook_url
+        self.incoming_webhook_channel = incoming_webhook_channel
+        self.incoming_webhook_channel_id = incoming_webhook_channel_id
+        self.incoming_webhook_configuration_url = incoming_webhook_configuration_url
+
+        self.is_enterprise_install = is_enterprise_install or False
+        self.token_type = token_type
+
+        if installed_at is None:
+            self.installed_at = datetime.now().timestamp()
+        else:
+            self.installed_at = _timestamp_to_type(installed_at, float)
+
+        self.custom_values = custom_values if custom_values is not None else {}
+
+    def to_bot(self) -> Bot:
+        return Bot(
+            app_id=self.app_id,
+            enterprise_id=self.enterprise_id,
+            enterprise_name=self.enterprise_name,
+            team_id=self.team_id,
+            team_name=self.team_name,
+            bot_token=self.bot_token,  # type: ignore[arg-type]
+            bot_id=self.bot_id,  # type: ignore[arg-type]
+            bot_user_id=self.bot_user_id,  # type: ignore[arg-type]
+            bot_scopes=self.bot_scopes,  # type: ignore[arg-type]
+            bot_refresh_token=self.bot_refresh_token,
+            bot_token_expires_at=self.bot_token_expires_at,
+            is_enterprise_install=self.is_enterprise_install,
+            installed_at=self.installed_at,
+            custom_values=self.custom_values,
+        )
+
+    def set_custom_value(self, name: str, value: Any):
+        self.custom_values[name] = value
+
+    def get_custom_value(self, name: str) -> Optional[Any]:
+        return self.custom_values.get(name)
+
+    def _to_standard_value_dict(self) -> Dict[str, Any]:
+        return {
+            "app_id": self.app_id,
+            "enterprise_id": self.enterprise_id,
+            "enterprise_name": self.enterprise_name,
+            "enterprise_url": self.enterprise_url,
+            "team_id": self.team_id,
+            "team_name": self.team_name,
+            "bot_token": self.bot_token,
+            "bot_id": self.bot_id,
+            "bot_user_id": self.bot_user_id,
+            "bot_scopes": ",".join(self.bot_scopes) if self.bot_scopes else None,
+            "bot_refresh_token": self.bot_refresh_token,
+            "bot_token_expires_at": (
+                datetime.utcfromtimestamp(self.bot_token_expires_at) if self.bot_token_expires_at is not None else None
+            ),
+            "user_id": self.user_id,
+            "user_token": self.user_token,
+            "user_scopes": ",".join(self.user_scopes) if self.user_scopes else None,
+            "user_refresh_token": self.user_refresh_token,
+            "user_token_expires_at": (
+                datetime.utcfromtimestamp(self.user_token_expires_at) if self.user_token_expires_at is not None else None
+            ),
+            "incoming_webhook_url": self.incoming_webhook_url,
+            "incoming_webhook_channel": self.incoming_webhook_channel,
+            "incoming_webhook_channel_id": self.incoming_webhook_channel_id,
+            "incoming_webhook_configuration_url": self.incoming_webhook_configuration_url,
+            "is_enterprise_install": self.is_enterprise_install,
+            "token_type": self.token_type,
+            "installed_at": datetime.utcfromtimestamp(self.installed_at),
+        }
+
+    def to_dict_for_copying(self) -> Dict[str, Any]:
+        return {"custom_values": self.custom_values, **self._to_standard_value_dict()}
+
+    def to_dict(self) -> Dict[str, Any]:
+        # prioritize standard_values over custom_values
+        # when the same keys exist in both
+        return {**self.custom_values, **self._to_standard_value_dict()}
+
+
+

Class variables

+
+
var app_id : str | None
+
+

The type of the None singleton.

+
+
var bot_id : str | None
+
+

The type of the None singleton.

+
+
var bot_refresh_token : str | None
+
+

The type of the None singleton.

+
+
var bot_scopes : Sequence[str] | None
+
+

The type of the None singleton.

+
+
var bot_token : str | None
+
+

The type of the None singleton.

+
+
var bot_token_expires_at : int | None
+
+

The type of the None singleton.

+
+
var bot_user_id : str | None
+
+

The type of the None singleton.

+
+
var custom_values : Dict[str, Any]
+
+

The type of the None singleton.

+
+
var enterprise_id : str | None
+
+

The type of the None singleton.

+
+
var enterprise_name : str | None
+
+

The type of the None singleton.

+
+
var enterprise_url : str | None
+
+

The type of the None singleton.

+
+
var incoming_webhook_channel : str | None
+
+

The type of the None singleton.

+
+
var incoming_webhook_channel_id : str | None
+
+

The type of the None singleton.

+
+
var incoming_webhook_configuration_url : str | None
+
+

The type of the None singleton.

+
+
var incoming_webhook_url : str | None
+
+

The type of the None singleton.

+
+
var installed_at : float
+
+

The type of the None singleton.

+
+
var is_enterprise_install : bool
+
+

The type of the None singleton.

+
+
var team_id : str | None
+
+

The type of the None singleton.

+
+
var team_name : str | None
+
+

The type of the None singleton.

+
+
var token_type : str | None
+
+

The type of the None singleton.

+
+
var user_id : str
+
+

The type of the None singleton.

+
+
var user_refresh_token : str | None
+
+

The type of the None singleton.

+
+
var user_scopes : Sequence[str] | None
+
+

The type of the None singleton.

+
+
var user_token : str | None
+
+

The type of the None singleton.

+
+
var user_token_expires_at : int | None
+
+

The type of the None singleton.

+
+
+

Methods

+
+
+def get_custom_value(self, name: str) ‑> Any | None +
+
+
+ +Expand source code + +
def get_custom_value(self, name: str) -> Optional[Any]:
+    return self.custom_values.get(name)
+
+
+
+
+def set_custom_value(self, name: str, value: Any) +
+
+
+ +Expand source code + +
def set_custom_value(self, name: str, value: Any):
+    self.custom_values[name] = value
+
+
+
+
+def to_bot(self) ‑> Bot +
+
+
+ +Expand source code + +
def to_bot(self) -> Bot:
+    return Bot(
+        app_id=self.app_id,
+        enterprise_id=self.enterprise_id,
+        enterprise_name=self.enterprise_name,
+        team_id=self.team_id,
+        team_name=self.team_name,
+        bot_token=self.bot_token,  # type: ignore[arg-type]
+        bot_id=self.bot_id,  # type: ignore[arg-type]
+        bot_user_id=self.bot_user_id,  # type: ignore[arg-type]
+        bot_scopes=self.bot_scopes,  # type: ignore[arg-type]
+        bot_refresh_token=self.bot_refresh_token,
+        bot_token_expires_at=self.bot_token_expires_at,
+        is_enterprise_install=self.is_enterprise_install,
+        installed_at=self.installed_at,
+        custom_values=self.custom_values,
+    )
+
+
+
+
+def to_dict(self) ‑> Dict[str, Any] +
+
+
+ +Expand source code + +
def to_dict(self) -> Dict[str, Any]:
+    # prioritize standard_values over custom_values
+    # when the same keys exist in both
+    return {**self.custom_values, **self._to_standard_value_dict()}
+
+
+
+
+def to_dict_for_copying(self) ‑> Dict[str, Any] +
+
+
+ +Expand source code + +
def to_dict_for_copying(self) -> Dict[str, Any]:
+    return {"custom_values": self.custom_values, **self._to_standard_value_dict()}
+
+
+
+
+
+
+
+
+ +
+ + + diff --git a/docs/reference/oauth/installation_store/models/installation.html b/docs/reference/oauth/installation_store/models/installation.html new file mode 100644 index 000000000..c0de92807 --- /dev/null +++ b/docs/reference/oauth/installation_store/models/installation.html @@ -0,0 +1,500 @@ + + + + + + +slack_sdk.oauth.installation_store.models.installation API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.oauth.installation_store.models.installation

+
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class Installation +(*,
app_id: str | None = None,
enterprise_id: str | None = None,
enterprise_name: str | None = None,
enterprise_url: str | None = None,
team_id: str | None = None,
team_name: str | None = None,
bot_token: str | None = None,
bot_id: str | None = None,
bot_user_id: str | None = None,
bot_scopes: str | Sequence[str] = '',
bot_refresh_token: str | None = None,
bot_token_expires_in: int | None = None,
bot_token_expires_at: int | datetime.datetime | str | None = None,
user_id: str,
user_token: str | None = None,
user_scopes: str | Sequence[str] = '',
user_refresh_token: str | None = None,
user_token_expires_in: int | None = None,
user_token_expires_at: int | datetime.datetime | str | None = None,
incoming_webhook_url: str | None = None,
incoming_webhook_channel: str | None = None,
incoming_webhook_channel_id: str | None = None,
incoming_webhook_configuration_url: str | None = None,
is_enterprise_install: bool | None = False,
token_type: str | None = None,
installed_at: float | datetime.datetime | str | None = None,
custom_values: Dict[str, Any] | None = None)
+
+
+
+ +Expand source code + +
class Installation:
+    app_id: Optional[str]
+    enterprise_id: Optional[str]
+    enterprise_name: Optional[str]
+    enterprise_url: Optional[str]
+    team_id: Optional[str]
+    team_name: Optional[str]
+    bot_token: Optional[str]
+    bot_id: Optional[str]
+    bot_user_id: Optional[str]
+    bot_scopes: Optional[Sequence[str]]
+    bot_refresh_token: Optional[str]  # only when token rotation is enabled
+    # only when token rotation is enabled
+    # Unix time (seconds): only when token rotation is enabled
+    bot_token_expires_at: Optional[int]
+    user_id: str
+    user_token: Optional[str]
+    user_scopes: Optional[Sequence[str]]
+    user_refresh_token: Optional[str]  # only when token rotation is enabled
+    # Unix time (seconds): only when token rotation is enabled
+    user_token_expires_at: Optional[int]
+    incoming_webhook_url: Optional[str]
+    incoming_webhook_channel: Optional[str]
+    incoming_webhook_channel_id: Optional[str]
+    incoming_webhook_configuration_url: Optional[str]
+    is_enterprise_install: bool
+    token_type: Optional[str]
+    installed_at: float
+
+    custom_values: Dict[str, Any]
+
+    def __init__(
+        self,
+        *,
+        app_id: Optional[str] = None,
+        # org / workspace
+        enterprise_id: Optional[str] = None,
+        enterprise_name: Optional[str] = None,
+        enterprise_url: Optional[str] = None,
+        team_id: Optional[str] = None,
+        team_name: Optional[str] = None,
+        # bot
+        bot_token: Optional[str] = None,
+        bot_id: Optional[str] = None,
+        bot_user_id: Optional[str] = None,
+        bot_scopes: Union[str, Sequence[str]] = "",
+        bot_refresh_token: Optional[str] = None,  # only when token rotation is enabled
+        # only when token rotation is enabled
+        bot_token_expires_in: Optional[int] = None,
+        # only for duplicating this object
+        # only when token rotation is enabled
+        bot_token_expires_at: Optional[Union[int, datetime, str]] = None,
+        # installer
+        user_id: str,
+        user_token: Optional[str] = None,
+        user_scopes: Union[str, Sequence[str]] = "",
+        user_refresh_token: Optional[str] = None,  # only when token rotation is enabled
+        # only when token rotation is enabled
+        user_token_expires_in: Optional[int] = None,
+        # only for duplicating this object
+        # only when token rotation is enabled
+        user_token_expires_at: Optional[Union[int, datetime, str]] = None,
+        # incoming webhook
+        incoming_webhook_url: Optional[str] = None,
+        incoming_webhook_channel: Optional[str] = None,
+        incoming_webhook_channel_id: Optional[str] = None,
+        incoming_webhook_configuration_url: Optional[str] = None,
+        # org app
+        is_enterprise_install: Optional[bool] = False,
+        token_type: Optional[str] = None,
+        # timestamps
+        # The expected value type is float but the internals handle other types too
+        # for str values, we supports only ISO datetime format.
+        installed_at: Optional[Union[float, datetime, str]] = None,
+        # custom values
+        custom_values: Optional[Dict[str, Any]] = None,
+    ):
+        self.app_id = app_id
+        self.enterprise_id = enterprise_id
+        self.enterprise_name = enterprise_name
+        self.enterprise_url = enterprise_url
+        self.team_id = team_id
+        self.team_name = team_name
+        self.bot_token = bot_token
+        self.bot_id = bot_id
+        self.bot_user_id = bot_user_id
+        if isinstance(bot_scopes, str):
+            self.bot_scopes = bot_scopes.split(",") if len(bot_scopes) > 0 else []
+        else:
+            self.bot_scopes = bot_scopes
+        self.bot_refresh_token = bot_refresh_token
+
+        if bot_token_expires_at is not None:
+            self.bot_token_expires_at = _timestamp_to_type(bot_token_expires_at, int)
+        elif bot_token_expires_in is not None:
+            self.bot_token_expires_at = int(time()) + bot_token_expires_in
+        else:
+            self.bot_token_expires_at = None
+
+        self.user_id = user_id
+        self.user_token = user_token
+        if isinstance(user_scopes, str):
+            self.user_scopes = user_scopes.split(",") if len(user_scopes) > 0 else []
+        else:
+            self.user_scopes = user_scopes
+        self.user_refresh_token = user_refresh_token
+
+        if user_token_expires_at is not None:
+            self.user_token_expires_at = _timestamp_to_type(user_token_expires_at, int)
+        elif user_token_expires_in is not None:
+            self.user_token_expires_at = int(time()) + user_token_expires_in
+        else:
+            self.user_token_expires_at = None
+
+        self.incoming_webhook_url = incoming_webhook_url
+        self.incoming_webhook_channel = incoming_webhook_channel
+        self.incoming_webhook_channel_id = incoming_webhook_channel_id
+        self.incoming_webhook_configuration_url = incoming_webhook_configuration_url
+
+        self.is_enterprise_install = is_enterprise_install or False
+        self.token_type = token_type
+
+        if installed_at is None:
+            self.installed_at = datetime.now().timestamp()
+        else:
+            self.installed_at = _timestamp_to_type(installed_at, float)
+
+        self.custom_values = custom_values if custom_values is not None else {}
+
+    def to_bot(self) -> Bot:
+        return Bot(
+            app_id=self.app_id,
+            enterprise_id=self.enterprise_id,
+            enterprise_name=self.enterprise_name,
+            team_id=self.team_id,
+            team_name=self.team_name,
+            bot_token=self.bot_token,  # type: ignore[arg-type]
+            bot_id=self.bot_id,  # type: ignore[arg-type]
+            bot_user_id=self.bot_user_id,  # type: ignore[arg-type]
+            bot_scopes=self.bot_scopes,  # type: ignore[arg-type]
+            bot_refresh_token=self.bot_refresh_token,
+            bot_token_expires_at=self.bot_token_expires_at,
+            is_enterprise_install=self.is_enterprise_install,
+            installed_at=self.installed_at,
+            custom_values=self.custom_values,
+        )
+
+    def set_custom_value(self, name: str, value: Any):
+        self.custom_values[name] = value
+
+    def get_custom_value(self, name: str) -> Optional[Any]:
+        return self.custom_values.get(name)
+
+    def _to_standard_value_dict(self) -> Dict[str, Any]:
+        return {
+            "app_id": self.app_id,
+            "enterprise_id": self.enterprise_id,
+            "enterprise_name": self.enterprise_name,
+            "enterprise_url": self.enterprise_url,
+            "team_id": self.team_id,
+            "team_name": self.team_name,
+            "bot_token": self.bot_token,
+            "bot_id": self.bot_id,
+            "bot_user_id": self.bot_user_id,
+            "bot_scopes": ",".join(self.bot_scopes) if self.bot_scopes else None,
+            "bot_refresh_token": self.bot_refresh_token,
+            "bot_token_expires_at": (
+                datetime.utcfromtimestamp(self.bot_token_expires_at) if self.bot_token_expires_at is not None else None
+            ),
+            "user_id": self.user_id,
+            "user_token": self.user_token,
+            "user_scopes": ",".join(self.user_scopes) if self.user_scopes else None,
+            "user_refresh_token": self.user_refresh_token,
+            "user_token_expires_at": (
+                datetime.utcfromtimestamp(self.user_token_expires_at) if self.user_token_expires_at is not None else None
+            ),
+            "incoming_webhook_url": self.incoming_webhook_url,
+            "incoming_webhook_channel": self.incoming_webhook_channel,
+            "incoming_webhook_channel_id": self.incoming_webhook_channel_id,
+            "incoming_webhook_configuration_url": self.incoming_webhook_configuration_url,
+            "is_enterprise_install": self.is_enterprise_install,
+            "token_type": self.token_type,
+            "installed_at": datetime.utcfromtimestamp(self.installed_at),
+        }
+
+    def to_dict_for_copying(self) -> Dict[str, Any]:
+        return {"custom_values": self.custom_values, **self._to_standard_value_dict()}
+
+    def to_dict(self) -> Dict[str, Any]:
+        # prioritize standard_values over custom_values
+        # when the same keys exist in both
+        return {**self.custom_values, **self._to_standard_value_dict()}
+
+
+

Class variables

+
+
var app_id : str | None
+
+

The type of the None singleton.

+
+
var bot_id : str | None
+
+

The type of the None singleton.

+
+
var bot_refresh_token : str | None
+
+

The type of the None singleton.

+
+
var bot_scopes : Sequence[str] | None
+
+

The type of the None singleton.

+
+
var bot_token : str | None
+
+

The type of the None singleton.

+
+
var bot_token_expires_at : int | None
+
+

The type of the None singleton.

+
+
var bot_user_id : str | None
+
+

The type of the None singleton.

+
+
var custom_values : Dict[str, Any]
+
+

The type of the None singleton.

+
+
var enterprise_id : str | None
+
+

The type of the None singleton.

+
+
var enterprise_name : str | None
+
+

The type of the None singleton.

+
+
var enterprise_url : str | None
+
+

The type of the None singleton.

+
+
var incoming_webhook_channel : str | None
+
+

The type of the None singleton.

+
+
var incoming_webhook_channel_id : str | None
+
+

The type of the None singleton.

+
+
var incoming_webhook_configuration_url : str | None
+
+

The type of the None singleton.

+
+
var incoming_webhook_url : str | None
+
+

The type of the None singleton.

+
+
var installed_at : float
+
+

The type of the None singleton.

+
+
var is_enterprise_install : bool
+
+

The type of the None singleton.

+
+
var team_id : str | None
+
+

The type of the None singleton.

+
+
var team_name : str | None
+
+

The type of the None singleton.

+
+
var token_type : str | None
+
+

The type of the None singleton.

+
+
var user_id : str
+
+

The type of the None singleton.

+
+
var user_refresh_token : str | None
+
+

The type of the None singleton.

+
+
var user_scopes : Sequence[str] | None
+
+

The type of the None singleton.

+
+
var user_token : str | None
+
+

The type of the None singleton.

+
+
var user_token_expires_at : int | None
+
+

The type of the None singleton.

+
+
+

Methods

+
+
+def get_custom_value(self, name: str) ‑> Any | None +
+
+
+ +Expand source code + +
def get_custom_value(self, name: str) -> Optional[Any]:
+    return self.custom_values.get(name)
+
+
+
+
+def set_custom_value(self, name: str, value: Any) +
+
+
+ +Expand source code + +
def set_custom_value(self, name: str, value: Any):
+    self.custom_values[name] = value
+
+
+
+
+def to_bot(self) ‑> Bot +
+
+
+ +Expand source code + +
def to_bot(self) -> Bot:
+    return Bot(
+        app_id=self.app_id,
+        enterprise_id=self.enterprise_id,
+        enterprise_name=self.enterprise_name,
+        team_id=self.team_id,
+        team_name=self.team_name,
+        bot_token=self.bot_token,  # type: ignore[arg-type]
+        bot_id=self.bot_id,  # type: ignore[arg-type]
+        bot_user_id=self.bot_user_id,  # type: ignore[arg-type]
+        bot_scopes=self.bot_scopes,  # type: ignore[arg-type]
+        bot_refresh_token=self.bot_refresh_token,
+        bot_token_expires_at=self.bot_token_expires_at,
+        is_enterprise_install=self.is_enterprise_install,
+        installed_at=self.installed_at,
+        custom_values=self.custom_values,
+    )
+
+
+
+
+def to_dict(self) ‑> Dict[str, Any] +
+
+
+ +Expand source code + +
def to_dict(self) -> Dict[str, Any]:
+    # prioritize standard_values over custom_values
+    # when the same keys exist in both
+    return {**self.custom_values, **self._to_standard_value_dict()}
+
+
+
+
+def to_dict_for_copying(self) ‑> Dict[str, Any] +
+
+
+ +Expand source code + +
def to_dict_for_copying(self) -> Dict[str, Any]:
+    return {"custom_values": self.custom_values, **self._to_standard_value_dict()}
+
+
+
+
+
+
+
+
+ +
+ + + diff --git a/docs/reference/oauth/installation_store/sqlalchemy/index.html b/docs/reference/oauth/installation_store/sqlalchemy/index.html new file mode 100644 index 000000000..8861dd476 --- /dev/null +++ b/docs/reference/oauth/installation_store/sqlalchemy/index.html @@ -0,0 +1,942 @@ + + + + + + +slack_sdk.oauth.installation_store.sqlalchemy API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.oauth.installation_store.sqlalchemy

+
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class AsyncSQLAlchemyInstallationStore +(client_id: str,
engine: sqlalchemy.ext.asyncio.engine.AsyncEngine,
bots_table_name: str = 'slack_bots',
installations_table_name: str = 'slack_installations',
logger: logging.Logger = <Logger slack_sdk.oauth.installation_store.sqlalchemy (WARNING)>)
+
+
+
+ +Expand source code + +
class AsyncSQLAlchemyInstallationStore(AsyncInstallationStore):
+    default_bots_table_name: str = "slack_bots"
+    default_installations_table_name: str = "slack_installations"
+
+    client_id: str
+    engine: AsyncEngine
+    metadata: MetaData
+    installations: Table
+
+    def __init__(
+        self,
+        client_id: str,
+        engine: AsyncEngine,
+        bots_table_name: str = default_bots_table_name,
+        installations_table_name: str = default_installations_table_name,
+        logger: Logger = logging.getLogger(__name__),
+    ):
+        self.metadata = sqlalchemy.MetaData()
+        self.bots = self.build_bots_table(metadata=self.metadata, table_name=bots_table_name)
+        self.installations = self.build_installations_table(metadata=self.metadata, table_name=installations_table_name)
+        self.client_id = client_id
+        self._logger = logger
+        self.engine = engine
+
+    @classmethod
+    def build_installations_table(cls, metadata: MetaData, table_name: str) -> Table:
+        return SQLAlchemyInstallationStore.build_installations_table(metadata, table_name)
+
+    @classmethod
+    def build_bots_table(cls, metadata: MetaData, table_name: str) -> Table:
+        return SQLAlchemyInstallationStore.build_bots_table(metadata, table_name)
+
+    async def create_tables(self):
+        async with self.engine.begin() as conn:
+            await conn.run_sync(self.metadata.create_all)
+
+    @property
+    def logger(self) -> Logger:
+        return self._logger
+
+    async def async_save(self, installation: Installation):
+        async with self.engine.begin() as conn:
+            i = installation.to_dict()
+            i["client_id"] = self.client_id
+
+            i_column = self.installations.c
+            installations_rows = await conn.execute(
+                sqlalchemy.select(i_column.id)
+                .where(
+                    and_(
+                        i_column.client_id == self.client_id,
+                        i_column.enterprise_id == installation.enterprise_id,
+                        i_column.team_id == installation.team_id,
+                        i_column.installed_at == i.get("installed_at"),
+                    )
+                )
+                .limit(1)
+            )
+            installations_row_id: Optional[str] = None
+            for row in installations_rows.mappings():
+                installations_row_id = row["id"]
+            if installations_row_id is None:
+                await conn.execute(self.installations.insert(), i)
+            else:
+                update_statement = self.installations.update().where(i_column.id == installations_row_id).values(**i)
+                await conn.execute(update_statement, i)
+
+        # bots
+        await self.async_save_bot(installation.to_bot())
+
+    async def async_save_bot(self, bot: Bot):
+        async with self.engine.begin() as conn:
+            # bots
+            b = bot.to_dict()
+            b["client_id"] = self.client_id
+
+            b_column = self.bots.c
+            bots_rows = await conn.execute(
+                sqlalchemy.select(b_column.id)
+                .where(
+                    and_(
+                        b_column.client_id == self.client_id,
+                        b_column.enterprise_id == bot.enterprise_id,
+                        b_column.team_id == bot.team_id,
+                        b_column.installed_at == b.get("installed_at"),
+                    )
+                )
+                .limit(1)
+            )
+            bots_row_id: Optional[str] = None
+            for row in bots_rows.mappings():
+                bots_row_id = row["id"]
+            if bots_row_id is None:
+                await conn.execute(self.bots.insert(), b)
+            else:
+                update_statement = self.bots.update().where(b_column.id == bots_row_id).values(**b)
+                await conn.execute(update_statement, b)
+
+    async def async_find_bot(
+        self,
+        *,
+        enterprise_id: Optional[str],
+        team_id: Optional[str],
+        is_enterprise_install: Optional[bool] = False,
+    ) -> Optional[Bot]:
+        if is_enterprise_install or team_id is None:
+            team_id = None
+
+        c = self.bots.c
+        query = (
+            self.bots.select()
+            .where(
+                and_(
+                    c.client_id == self.client_id,
+                    c.enterprise_id == enterprise_id,
+                    c.team_id == team_id,
+                    c.bot_token.is_not(None),  # the latest one that has a bot token
+                )
+            )
+            .order_by(desc(c.installed_at))
+            .limit(1)
+        )
+
+        async with self.engine.connect() as conn:
+            result: object = await conn.execute(query)
+            for row in result.mappings():  # type: ignore[attr-defined]
+                return SQLAlchemyInstallationStore.build_bot_entity(row)
+            return None
+
+    async def async_find_installation(
+        self,
+        *,
+        enterprise_id: Optional[str],
+        team_id: Optional[str],
+        user_id: Optional[str] = None,
+        is_enterprise_install: Optional[bool] = False,
+    ) -> Optional[Installation]:
+        if is_enterprise_install or team_id is None:
+            team_id = None
+
+        c = self.installations.c
+        where_clause = and_(
+            c.client_id == self.client_id,
+            c.enterprise_id == enterprise_id,
+            c.team_id == team_id,
+        )
+        if user_id is not None:
+            where_clause = and_(
+                c.client_id == self.client_id,
+                c.enterprise_id == enterprise_id,
+                c.team_id == team_id,
+                c.user_id == user_id,
+            )
+
+        query = self.installations.select().where(where_clause).order_by(desc(c.installed_at)).limit(1)
+
+        installation: Optional[Installation] = None
+        async with self.engine.connect() as conn:
+            result: object = await conn.execute(query)
+            for row in result.mappings():  # type: ignore[attr-defined]
+                installation = SQLAlchemyInstallationStore.build_installation_entity(row)
+
+        has_user_installation = user_id is not None and installation is not None
+        no_bot_token_installation = installation is not None and installation.bot_token is None
+        should_find_bot_installation = has_user_installation or no_bot_token_installation
+        if should_find_bot_installation:
+            # Retrieve the latest bot token, just in case
+            # See also: https://github.com/slackapi/bolt-python/issues/664
+            latest_bot_installation = await self.async_find_bot(
+                enterprise_id=enterprise_id,
+                team_id=team_id,
+                is_enterprise_install=is_enterprise_install,
+            )
+            if (
+                latest_bot_installation is not None
+                and installation is not None
+                and installation.bot_token != latest_bot_installation.bot_token
+            ):
+                installation.bot_id = latest_bot_installation.bot_id
+                installation.bot_user_id = latest_bot_installation.bot_user_id
+                installation.bot_token = latest_bot_installation.bot_token
+                installation.bot_scopes = latest_bot_installation.bot_scopes
+                installation.bot_refresh_token = latest_bot_installation.bot_refresh_token
+                installation.bot_token_expires_at = latest_bot_installation.bot_token_expires_at
+
+        return installation
+
+    async def async_delete_bot(
+        self,
+        *,
+        enterprise_id: Optional[str],
+        team_id: Optional[str],
+    ) -> None:
+        table = self.bots
+        c = table.c
+        async with self.engine.begin() as conn:
+            deletion = table.delete().where(
+                and_(
+                    c.client_id == self.client_id,
+                    c.enterprise_id == enterprise_id,
+                    c.team_id == team_id,
+                )
+            )
+            await conn.execute(deletion)
+
+    async def async_delete_installation(
+        self,
+        *,
+        enterprise_id: Optional[str],
+        team_id: Optional[str],
+        user_id: Optional[str] = None,
+    ) -> None:
+        table = self.installations
+        c = table.c
+        async with self.engine.begin() as conn:
+            if user_id is not None:
+                deletion = table.delete().where(
+                    and_(
+                        c.client_id == self.client_id,
+                        c.enterprise_id == enterprise_id,
+                        c.team_id == team_id,
+                        c.user_id == user_id,
+                    )
+                )
+                await conn.execute(deletion)
+            else:
+                deletion = table.delete().where(
+                    and_(
+                        c.client_id == self.client_id,
+                        c.enterprise_id == enterprise_id,
+                        c.team_id == team_id,
+                    )
+                )
+                await conn.execute(deletion)
+
+

The installation store interface for asyncio-based apps.

+

The minimum required methods are:

+
    +
  • async_save(installation)
  • +
  • async_find_installation(enterprise_id, team_id, user_id, is_enterprise_install)
  • +
+

If you would like to properly handle app uninstallations and token revocations, +the following methods should be implemented.

+
    +
  • async_delete_installation(enterprise_id, team_id, user_id)
  • +
  • async_delete_all(enterprise_id, team_id)
  • +
+

If your app needs only bot scope installations, the simpler way to implement would be:

+
    +
  • async_save(installation)
  • +
  • async_find_bot(enterprise_id, team_id, is_enterprise_install)
  • +
  • async_delete_bot(enterprise_id, team_id)
  • +
  • async_delete_all(enterprise_id, team_id)
  • +
+

Ancestors

+ +

Class variables

+
+
var client_id : str
+
+

The type of the None singleton.

+
+
var default_bots_table_name : str
+
+

The type of the None singleton.

+
+
var default_installations_table_name : str
+
+

The type of the None singleton.

+
+
var engine : sqlalchemy.ext.asyncio.engine.AsyncEngine
+
+

The type of the None singleton.

+
+
var installations : sqlalchemy.sql.schema.Table
+
+

The type of the None singleton.

+
+
var metadata : sqlalchemy.sql.schema.MetaData
+
+

The type of the None singleton.

+
+
+

Static methods

+
+
+def build_bots_table(metadata: sqlalchemy.sql.schema.MetaData, table_name: str) ‑> sqlalchemy.sql.schema.Table +
+
+
+
+
+def build_installations_table(metadata: sqlalchemy.sql.schema.MetaData, table_name: str) ‑> sqlalchemy.sql.schema.Table +
+
+
+
+
+

Instance variables

+
+
prop logger : logging.Logger
+
+
+ +Expand source code + +
@property
+def logger(self) -> Logger:
+    return self._logger
+
+
+
+
+

Methods

+
+
+async def create_tables(self) +
+
+
+ +Expand source code + +
async def create_tables(self):
+    async with self.engine.begin() as conn:
+        await conn.run_sync(self.metadata.create_all)
+
+
+
+
+

Inherited members

+ +
+
+class SQLAlchemyInstallationStore +(client_id: str,
engine: sqlalchemy.engine.base.Engine,
bots_table_name: str = 'slack_bots',
installations_table_name: str = 'slack_installations',
logger: logging.Logger = <Logger slack_sdk.oauth.installation_store.sqlalchemy (WARNING)>)
+
+
+
+ +Expand source code + +
class SQLAlchemyInstallationStore(InstallationStore):
+    default_bots_table_name: str = "slack_bots"
+    default_installations_table_name: str = "slack_installations"
+
+    client_id: str
+    engine: Engine
+    metadata: MetaData
+    installations: Table
+
+    @classmethod
+    def build_installations_table(cls, metadata: MetaData, table_name: str) -> Table:
+        return sqlalchemy.Table(
+            table_name,
+            metadata,
+            Column("id", Integer, primary_key=True, autoincrement=True),
+            Column("client_id", String(32), nullable=False),
+            Column("app_id", String(32), nullable=False),
+            Column("enterprise_id", String(32)),
+            Column("enterprise_name", String(200)),
+            Column("enterprise_url", String(200)),
+            Column("team_id", String(32)),
+            Column("team_name", String(200)),
+            Column("bot_token", String(200)),
+            Column("bot_id", String(32)),
+            Column("bot_user_id", String(32)),
+            Column("bot_scopes", String(1000)),
+            Column("bot_refresh_token", String(200)),  # added in v3.8.0
+            Column("bot_token_expires_at", DateTime),  # added in v3.8.0
+            Column("user_id", String(32), nullable=False),
+            Column("user_token", String(200)),
+            Column("user_scopes", String(1000)),
+            Column("user_refresh_token", String(200)),  # added in v3.8.0
+            Column("user_token_expires_at", DateTime),  # added in v3.8.0
+            Column("incoming_webhook_url", String(200)),
+            Column("incoming_webhook_channel", String(200)),
+            Column("incoming_webhook_channel_id", String(200)),
+            Column("incoming_webhook_configuration_url", String(200)),
+            Column("is_enterprise_install", Boolean, default=False, nullable=False),
+            Column("token_type", String(32)),
+            Column(
+                "installed_at",
+                DateTime,
+                nullable=False,
+                default=sqlalchemy.sql.func.now(),
+            ),
+            Index(
+                f"{table_name}_idx",
+                "client_id",
+                "enterprise_id",
+                "team_id",
+                "user_id",
+                "installed_at",
+            ),
+        )
+
+    @classmethod
+    def build_bots_table(cls, metadata: MetaData, table_name: str) -> Table:
+        return Table(
+            table_name,
+            metadata,
+            Column("id", Integer, primary_key=True, autoincrement=True),
+            Column("client_id", String(32), nullable=False),
+            Column("app_id", String(32), nullable=False),
+            Column("enterprise_id", String(32)),
+            Column("enterprise_name", String(200)),
+            Column("team_id", String(32)),
+            Column("team_name", String(200)),
+            Column("bot_token", String(200)),
+            Column("bot_id", String(32)),
+            Column("bot_user_id", String(32)),
+            Column("bot_scopes", String(1000)),
+            Column("bot_refresh_token", String(200)),  # added in v3.8.0
+            Column("bot_token_expires_at", DateTime),  # added in v3.8.0
+            Column("is_enterprise_install", Boolean, default=False, nullable=False),
+            Column(
+                "installed_at",
+                DateTime,
+                nullable=False,
+                default=sqlalchemy.sql.func.now(),
+            ),
+            Index(
+                f"{table_name}_idx",
+                "client_id",
+                "enterprise_id",
+                "team_id",
+                "installed_at",
+            ),
+        )
+
+    def __init__(
+        self,
+        client_id: str,
+        engine: Engine,
+        bots_table_name: str = default_bots_table_name,
+        installations_table_name: str = default_installations_table_name,
+        logger: Logger = logging.getLogger(__name__),
+    ):
+        self.metadata = sqlalchemy.MetaData()
+        self.bots = self.build_bots_table(metadata=self.metadata, table_name=bots_table_name)
+        self.installations = self.build_installations_table(metadata=self.metadata, table_name=installations_table_name)
+        self.client_id = client_id
+        self._logger = logger
+        self.engine = engine
+
+    def create_tables(self):
+        self.metadata.create_all(self.engine)
+
+    @property
+    def logger(self) -> Logger:
+        return self._logger
+
+    def save(self, installation: Installation):
+        with self.engine.begin() as conn:
+            i = installation.to_dict()
+            i["client_id"] = self.client_id
+
+            i_column = self.installations.c
+            installations_rows = conn.execute(
+                sqlalchemy.select(i_column.id)
+                .where(
+                    and_(
+                        i_column.client_id == self.client_id,
+                        i_column.enterprise_id == installation.enterprise_id,
+                        i_column.team_id == installation.team_id,
+                        i_column.installed_at == i.get("installed_at"),
+                    )
+                )
+                .limit(1)
+            )
+            installations_row_id: Optional[str] = None
+            for row in installations_rows.mappings():
+                installations_row_id = row["id"]
+            if installations_row_id is None:
+                conn.execute(self.installations.insert(), i)
+            else:
+                update_statement = self.installations.update().where(i_column.id == installations_row_id).values(**i)
+                conn.execute(update_statement, i)
+
+        # bots
+        self.save_bot(installation.to_bot())
+
+    def save_bot(self, bot: Bot):
+        with self.engine.begin() as conn:
+            # bots
+            b = bot.to_dict()
+            b["client_id"] = self.client_id
+
+            b_column = self.bots.c
+            bots_rows = conn.execute(
+                sqlalchemy.select(b_column.id)
+                .where(
+                    and_(
+                        b_column.client_id == self.client_id,
+                        b_column.enterprise_id == bot.enterprise_id,
+                        b_column.team_id == bot.team_id,
+                        b_column.installed_at == b.get("installed_at"),
+                    )
+                )
+                .limit(1)
+            )
+            bots_row_id: Optional[str] = None
+            for row in bots_rows.mappings():
+                bots_row_id = row["id"]
+            if bots_row_id is None:
+                conn.execute(self.bots.insert(), b)
+            else:
+                update_statement = self.bots.update().where(b_column.id == bots_row_id).values(**b)
+                conn.execute(update_statement, b)
+
+    def find_bot(
+        self,
+        *,
+        enterprise_id: Optional[str],
+        team_id: Optional[str],
+        is_enterprise_install: Optional[bool] = False,
+    ) -> Optional[Bot]:
+        if is_enterprise_install or team_id is None:
+            team_id = None
+
+        c = self.bots.c
+        query = (
+            self.bots.select()
+            .where(
+                and_(
+                    c.client_id == self.client_id,
+                    c.enterprise_id == enterprise_id,
+                    c.team_id == team_id,
+                    c.bot_token.is_not(None),  # the latest one that has a bot token
+                )
+            )
+            .order_by(desc(c.installed_at))
+            .limit(1)
+        )
+
+        with self.engine.connect() as conn:
+            result: object = conn.execute(query)
+            for row in result.mappings():  # type: ignore[attr-defined]
+                return self.build_bot_entity(row)
+            return None
+
+    def find_installation(
+        self,
+        *,
+        enterprise_id: Optional[str],
+        team_id: Optional[str],
+        user_id: Optional[str] = None,
+        is_enterprise_install: Optional[bool] = False,
+    ) -> Optional[Installation]:
+        if is_enterprise_install or team_id is None:
+            team_id = None
+
+        c = self.installations.c
+        where_clause = and_(
+            c.client_id == self.client_id,
+            c.enterprise_id == enterprise_id,
+            c.team_id == team_id,
+        )
+        if user_id is not None:
+            where_clause = and_(
+                c.client_id == self.client_id,
+                c.enterprise_id == enterprise_id,
+                c.team_id == team_id,
+                c.user_id == user_id,
+            )
+
+        query = self.installations.select().where(where_clause).order_by(desc(c.installed_at)).limit(1)
+
+        installation: Optional[Installation] = None
+        with self.engine.connect() as conn:
+            result: object = conn.execute(query)
+            for row in result.mappings():  # type: ignore[attr-defined]
+                installation = self.build_installation_entity(row)
+
+        has_user_installation = user_id is not None and installation is not None
+        no_bot_token_installation = installation is not None and installation.bot_token is None
+        should_find_bot_installation = has_user_installation or no_bot_token_installation
+        if should_find_bot_installation:
+            # Retrieve the latest bot token, just in case
+            # See also: https://github.com/slackapi/bolt-python/issues/664
+            latest_bot_installation = self.find_bot(
+                enterprise_id=enterprise_id,
+                team_id=team_id,
+                is_enterprise_install=is_enterprise_install,
+            )
+            if (
+                latest_bot_installation is not None
+                and installation is not None
+                and installation.bot_token != latest_bot_installation.bot_token
+            ):
+                installation.bot_id = latest_bot_installation.bot_id
+                installation.bot_user_id = latest_bot_installation.bot_user_id
+                installation.bot_token = latest_bot_installation.bot_token
+                installation.bot_scopes = latest_bot_installation.bot_scopes
+                installation.bot_refresh_token = latest_bot_installation.bot_refresh_token
+                installation.bot_token_expires_at = latest_bot_installation.bot_token_expires_at
+
+        return installation
+
+    def delete_bot(self, *, enterprise_id: Optional[str], team_id: Optional[str]) -> None:
+        table = self.bots
+        c = table.c
+        with self.engine.begin() as conn:
+            deletion = table.delete().where(
+                and_(
+                    c.client_id == self.client_id,
+                    c.enterprise_id == enterprise_id,
+                    c.team_id == team_id,
+                )
+            )
+            conn.execute(deletion)
+
+    def delete_installation(
+        self,
+        *,
+        enterprise_id: Optional[str],
+        team_id: Optional[str],
+        user_id: Optional[str] = None,
+    ) -> None:
+        table = self.installations
+        c = table.c
+        with self.engine.begin() as conn:
+            if user_id is not None:
+                deletion = table.delete().where(
+                    and_(
+                        c.client_id == self.client_id,
+                        c.enterprise_id == enterprise_id,
+                        c.team_id == team_id,
+                        c.user_id == user_id,
+                    )
+                )
+                conn.execute(deletion)
+            else:
+                deletion = table.delete().where(
+                    and_(
+                        c.client_id == self.client_id,
+                        c.enterprise_id == enterprise_id,
+                        c.team_id == team_id,
+                    )
+                )
+                conn.execute(deletion)
+
+    @classmethod
+    def build_installation_entity(cls, row) -> Installation:
+        return Installation(
+            app_id=row["app_id"],
+            enterprise_id=row["enterprise_id"],
+            enterprise_name=row["enterprise_name"],
+            enterprise_url=row["enterprise_url"],
+            team_id=row["team_id"],
+            team_name=row["team_name"],
+            bot_token=row["bot_token"],
+            bot_id=row["bot_id"],
+            bot_user_id=row["bot_user_id"],
+            bot_scopes=row["bot_scopes"],
+            bot_refresh_token=row["bot_refresh_token"],
+            bot_token_expires_at=row["bot_token_expires_at"],
+            user_id=row["user_id"],
+            user_token=row["user_token"],
+            user_scopes=row["user_scopes"],
+            user_refresh_token=row["user_refresh_token"],
+            user_token_expires_at=row["user_token_expires_at"],
+            # Only the incoming webhook issued in the latest installation is set in this logic
+            incoming_webhook_url=row["incoming_webhook_url"],
+            incoming_webhook_channel=row["incoming_webhook_channel"],
+            incoming_webhook_channel_id=row["incoming_webhook_channel_id"],
+            incoming_webhook_configuration_url=row["incoming_webhook_configuration_url"],
+            is_enterprise_install=row["is_enterprise_install"],
+            token_type=row["token_type"],
+            installed_at=row["installed_at"],
+        )
+
+    @classmethod
+    def build_bot_entity(cls, row) -> Bot:
+        return Bot(
+            app_id=row["app_id"],
+            enterprise_id=row["enterprise_id"],
+            enterprise_name=row["enterprise_name"],
+            team_id=row["team_id"],
+            team_name=row["team_name"],
+            bot_token=row["bot_token"],
+            bot_id=row["bot_id"],
+            bot_user_id=row["bot_user_id"],
+            bot_scopes=row["bot_scopes"],
+            bot_refresh_token=row["bot_refresh_token"],
+            bot_token_expires_at=row["bot_token_expires_at"],
+            is_enterprise_install=row["is_enterprise_install"],
+            installed_at=row["installed_at"],
+        )
+
+

The installation store interface.

+

The minimum required methods are:

+
    +
  • save(installation)
  • +
  • find_installation(enterprise_id, team_id, user_id, is_enterprise_install)
  • +
+

If you would like to properly handle app uninstallations and token revocations, +the following methods should be implemented.

+
    +
  • delete_installation(enterprise_id, team_id, user_id)
  • +
  • delete_all(enterprise_id, team_id)
  • +
+

If your app needs only bot scope installations, the simpler way to implement would be:

+
    +
  • save(installation)
  • +
  • find_bot(enterprise_id, team_id, is_enterprise_install)
  • +
  • delete_bot(enterprise_id, team_id)
  • +
  • delete_all(enterprise_id, team_id)
  • +
+

Ancestors

+ +

Class variables

+
+
var client_id : str
+
+

The type of the None singleton.

+
+
var default_bots_table_name : str
+
+

The type of the None singleton.

+
+
var default_installations_table_name : str
+
+

The type of the None singleton.

+
+
var engine : sqlalchemy.engine.base.Engine
+
+

The type of the None singleton.

+
+
var installations : sqlalchemy.sql.schema.Table
+
+

The type of the None singleton.

+
+
var metadata : sqlalchemy.sql.schema.MetaData
+
+

The type of the None singleton.

+
+
+

Static methods

+
+
+def build_bot_entity(row) ‑> Bot +
+
+
+
+
+def build_bots_table(metadata: sqlalchemy.sql.schema.MetaData, table_name: str) ‑> sqlalchemy.sql.schema.Table +
+
+
+
+
+def build_installation_entity(row) ‑> Installation +
+
+
+
+
+def build_installations_table(metadata: sqlalchemy.sql.schema.MetaData, table_name: str) ‑> sqlalchemy.sql.schema.Table +
+
+
+
+
+

Instance variables

+
+
prop logger : logging.Logger
+
+
+ +Expand source code + +
@property
+def logger(self) -> Logger:
+    return self._logger
+
+
+
+
+

Methods

+
+
+def create_tables(self) +
+
+
+ +Expand source code + +
def create_tables(self):
+    self.metadata.create_all(self.engine)
+
+
+
+
+

Inherited members

+ +
+
+
+
+ +
+ + + diff --git a/docs/reference/oauth/installation_store/sqlite3/index.html b/docs/reference/oauth/installation_store/sqlite3/index.html new file mode 100644 index 000000000..8fc6c563e --- /dev/null +++ b/docs/reference/oauth/installation_store/sqlite3/index.html @@ -0,0 +1,907 @@ + + + + + + +slack_sdk.oauth.installation_store.sqlite3 API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.oauth.installation_store.sqlite3

+
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class SQLite3InstallationStore +(*,
database: str,
client_id: str,
logger: logging.Logger = <Logger slack_sdk.oauth.installation_store.sqlite3 (WARNING)>)
+
+
+
+ +Expand source code + +
class SQLite3InstallationStore(InstallationStore, AsyncInstallationStore):
+    def __init__(
+        self,
+        *,
+        database: str,
+        client_id: str,
+        logger: Logger = logging.getLogger(__name__),
+    ):
+        self.database = database
+        self.client_id = client_id
+        self.init_called = False
+        self._logger = logger
+
+    @property
+    def logger(self) -> Logger:
+        if self._logger is None:
+            self._logger = logging.getLogger(__name__)
+        return self._logger
+
+    def init(self):
+        try:
+            with sqlite3.connect(database=self.database) as conn:
+                cur = conn.execute("select count(1) from slack_installations;")
+                row_num = cur.fetchone()[0]
+                self.logger.debug(f"{row_num} installations are stored in {self.database}")
+        except Exception:
+            self.create_tables()
+        self.init_called = True
+
+    def connect(self) -> Connection:
+        if not self.init_called:
+            self.init()
+        return sqlite3.connect(database=self.database)
+
+    def create_tables(self):
+        with sqlite3.connect(database=self.database) as conn:
+            conn.execute(
+                """
+            create table slack_installations (
+                id integer primary key autoincrement,
+                client_id text not null,
+                app_id text not null,
+                enterprise_id text not null default '',
+                enterprise_name text,
+                enterprise_url text,
+                team_id text not null default '',
+                team_name text,
+                bot_token text,
+                bot_id text,
+                bot_user_id text,
+                bot_scopes text,
+                bot_refresh_token text,  -- since v3.8
+                bot_token_expires_at datetime,  -- since v3.8
+                user_id text not null,
+                user_token text,
+                user_scopes text,
+                user_refresh_token text,  -- since v3.8
+                user_token_expires_at datetime,  -- since v3.8
+                incoming_webhook_url text,
+                incoming_webhook_channel text,
+                incoming_webhook_channel_id text,
+                incoming_webhook_configuration_url text,
+                is_enterprise_install boolean not null default 0,
+                token_type text,
+                installed_at datetime not null default current_timestamp
+            );
+            """
+            )
+            conn.execute(
+                """
+            create index slack_installations_idx on slack_installations (
+                client_id,
+                enterprise_id,
+                team_id,
+                user_id,
+                installed_at
+            );
+            """
+            )
+            conn.execute(
+                """
+            create table slack_bots (
+                id integer primary key autoincrement,
+                client_id text not null,
+                app_id text not null,
+                enterprise_id text not null default '',
+                enterprise_name text,
+                team_id text not null default '',
+                team_name text,
+                bot_token text not null,
+                bot_id text not null,
+                bot_user_id text not null,
+                bot_scopes text,
+                bot_refresh_token text,  -- since v3.8
+                bot_token_expires_at datetime,  -- since v3.8
+                is_enterprise_install boolean not null default 0,
+                installed_at datetime not null default current_timestamp
+            );
+            """
+            )
+            conn.execute(
+                """
+            create index slack_bots_idx on slack_bots (
+                client_id,
+                enterprise_id,
+                team_id,
+                installed_at
+            );
+            """
+            )
+            self.logger.debug(f"Tables have been created (database: {self.database})")
+            conn.commit()
+
+    async def async_save(self, installation: Installation):
+        return self.save(installation)
+
+    async def async_save_bot(self, bot: Bot):
+        return self.save_bot(bot)
+
+    def save(self, installation: Installation):
+        with self.connect() as conn:
+            conn.execute(
+                """
+                insert into slack_installations (
+                    client_id,
+                    app_id,
+                    enterprise_id,
+                    enterprise_name,
+                    enterprise_url,
+                    team_id,
+                    team_name,
+                    bot_token,
+                    bot_id,
+                    bot_user_id,
+                    bot_scopes,
+                    bot_refresh_token,  -- since v3.8
+                    bot_token_expires_at,  -- since v3.8
+                    user_id,
+                    user_token,
+                    user_scopes,
+                    user_refresh_token,  -- since v3.8
+                    user_token_expires_at,  -- since v3.8
+                    incoming_webhook_url,
+                    incoming_webhook_channel,
+                    incoming_webhook_channel_id,
+                    incoming_webhook_configuration_url,
+                    is_enterprise_install,
+                    token_type
+                )
+                values
+                (
+                    ?,
+                    ?,
+                    ?,
+                    ?,
+                    ?,
+                    ?,
+                    ?,
+                    ?,
+                    ?,
+                    ?,
+                    ?,
+                    ?,
+                    ?,
+                    ?,
+                    ?,
+                    ?,
+                    ?,
+                    ?,
+                    ?,
+                    ?,
+                    ?,
+                    ?,
+                    ?,
+                    ?
+                );
+                """,
+                [
+                    self.client_id,
+                    installation.app_id,
+                    installation.enterprise_id or "",
+                    installation.enterprise_name,
+                    installation.enterprise_url,
+                    installation.team_id or "",
+                    installation.team_name,
+                    installation.bot_token,
+                    installation.bot_id,
+                    installation.bot_user_id,
+                    ",".join(installation.bot_scopes),  # type: ignore[arg-type]
+                    installation.bot_refresh_token,
+                    installation.bot_token_expires_at,
+                    installation.user_id,
+                    installation.user_token,
+                    ",".join(installation.user_scopes) if installation.user_scopes else None,
+                    installation.user_refresh_token,
+                    installation.user_token_expires_at,
+                    installation.incoming_webhook_url,
+                    installation.incoming_webhook_channel,
+                    installation.incoming_webhook_channel_id,
+                    installation.incoming_webhook_configuration_url,
+                    1 if installation.is_enterprise_install else 0,
+                    installation.token_type,
+                ],
+            )
+            self.logger.debug(
+                f"New rows in slack_bots and slack_installations have been created (database: {self.database})"
+            )
+            conn.commit()
+
+        self.save_bot(installation.to_bot())
+
+    def save_bot(self, bot: Bot):
+        if bot.bot_token is None:
+            self.logger.debug("Skipped saving a new row because of the absense of bot token in it")
+            return
+
+        with self.connect() as conn:
+            conn.execute(
+                """
+                insert into slack_bots (
+                    client_id,
+                    app_id,
+                    enterprise_id,
+                    enterprise_name,
+                    team_id,
+                    team_name,
+                    bot_token,
+                    bot_id,
+                    bot_user_id,
+                    bot_scopes,
+                    bot_refresh_token,  -- since v3.8
+                    bot_token_expires_at,  -- since v3.8
+                    is_enterprise_install
+                )
+                values
+                (
+                    ?,
+                    ?,
+                    ?,
+                    ?,
+                    ?,
+                    ?,
+                    ?,
+                    ?,
+                    ?,
+                    ?,
+                    ?,
+                    ?,
+                    ?
+                );
+                """,
+                [
+                    self.client_id,
+                    bot.app_id,
+                    bot.enterprise_id or "",
+                    bot.enterprise_name,
+                    bot.team_id or "",
+                    bot.team_name,
+                    bot.bot_token,
+                    bot.bot_id,
+                    bot.bot_user_id,
+                    ",".join(bot.bot_scopes),
+                    bot.bot_refresh_token,
+                    bot.bot_token_expires_at,
+                    bot.is_enterprise_install,
+                ],
+            )
+            conn.commit()
+
+    async def async_find_bot(
+        self,
+        *,
+        enterprise_id: Optional[str],
+        team_id: Optional[str],
+        is_enterprise_install: Optional[bool] = False,
+    ) -> Optional[Bot]:
+        return self.find_bot(
+            enterprise_id=enterprise_id,
+            team_id=team_id,
+            is_enterprise_install=is_enterprise_install,
+        )
+
+    def find_bot(
+        self,
+        *,
+        enterprise_id: Optional[str],
+        team_id: Optional[str],
+        is_enterprise_install: Optional[bool] = False,
+    ) -> Optional[Bot]:
+        if is_enterprise_install or team_id is None:
+            team_id = ""
+
+        try:
+            with self.connect() as conn:
+                cur = conn.execute(
+                    """
+                    select
+                        app_id,
+                        enterprise_id,
+                        enterprise_name,
+                        team_id,
+                        team_name,
+                        bot_token,
+                        bot_id,
+                        bot_user_id,
+                        bot_scopes,
+                        bot_refresh_token,  -- since v3.8
+                        bot_token_expires_at,  -- since v3.8
+                        is_enterprise_install,
+                        installed_at
+                    from
+                        slack_bots
+                    where
+                        client_id = ?
+                        and
+                        enterprise_id = ?
+                        and
+                        team_id = ?
+                    order by installed_at desc
+                    limit 1
+                    """,
+                    [self.client_id, enterprise_id or "", team_id or ""],
+                )
+                row = cur.fetchone()
+                result = "found" if row and len(row) > 0 else "not found"
+                self.logger.debug(f"find_bot's query result: {result} (database: {self.database})")
+                if row and len(row) > 0:
+                    bot = Bot(
+                        app_id=row[0],
+                        enterprise_id=row[1],
+                        enterprise_name=row[2],
+                        team_id=row[3],
+                        team_name=row[4],
+                        bot_token=row[5],
+                        bot_id=row[6],
+                        bot_user_id=row[7],
+                        bot_scopes=row[8],
+                        bot_refresh_token=row[9],
+                        bot_token_expires_at=row[10],
+                        is_enterprise_install=row[11],
+                        installed_at=row[12],
+                    )
+                    return bot
+                return None
+
+        except Exception as e:
+            message = f"Failed to find bot installation data for enterprise: {enterprise_id}, team: {team_id}: {e}"
+            if self.logger.level <= logging.DEBUG:
+                self.logger.exception(message)
+            else:
+                self.logger.warning(message)
+            return None
+
+    async def async_find_installation(
+        self,
+        *,
+        enterprise_id: Optional[str],
+        team_id: Optional[str],
+        user_id: Optional[str] = None,
+        is_enterprise_install: Optional[bool] = False,
+    ) -> Optional[Installation]:
+        return self.find_installation(
+            enterprise_id=enterprise_id,
+            team_id=team_id,
+            user_id=user_id,
+            is_enterprise_install=is_enterprise_install,
+        )
+
+    def find_installation(
+        self,
+        *,
+        enterprise_id: Optional[str],
+        team_id: Optional[str],
+        user_id: Optional[str] = None,
+        is_enterprise_install: Optional[bool] = False,
+    ) -> Optional[Installation]:
+        if is_enterprise_install or team_id is None:
+            team_id = ""
+
+        try:
+            with self.connect() as conn:
+                row = None
+                columns = """
+                    app_id,
+                    enterprise_id,
+                    enterprise_name,
+                    enterprise_url,
+                    team_id,
+                    team_name,
+                    bot_token,
+                    bot_id,
+                    bot_user_id,
+                    bot_scopes,
+                    bot_refresh_token,  -- since v3.8
+                    bot_token_expires_at,  -- since v3.8
+                    user_id,
+                    user_token,
+                    user_scopes,
+                    user_refresh_token,  -- since v3.8
+                    user_token_expires_at,  -- since v3.8
+                    incoming_webhook_url,
+                    incoming_webhook_channel,
+                    incoming_webhook_channel_id,
+                    incoming_webhook_configuration_url,
+                    is_enterprise_install,
+                    token_type,
+                    installed_at
+                """
+                if user_id is None:
+                    cur = conn.execute(
+                        f"""
+                        select
+                            {columns}
+                        from
+                            slack_installations
+                        where
+                            client_id = ?
+                            and
+                            enterprise_id = ?
+                            and
+                            team_id = ?
+                        order by installed_at desc
+                        limit 1
+                        """,
+                        [self.client_id, enterprise_id or "", team_id],
+                    )
+                    row = cur.fetchone()
+                else:
+                    cur = conn.execute(
+                        f"""
+                        select
+                            {columns}
+                        from
+                            slack_installations
+                        where
+                            client_id = ?
+                            and
+                            enterprise_id = ?
+                            and
+                            team_id = ?
+                            and
+                            user_id = ?
+                        order by installed_at desc
+                        limit 1
+                        """,
+                        [self.client_id, enterprise_id or "", team_id, user_id],
+                    )
+                    row = cur.fetchone()
+
+                if row is None:
+                    return None
+
+                result = "found" if row and len(row) > 0 else "not found"
+                self.logger.debug(f"find_installation's query result: {result} (database: {self.database})")
+                if row and len(row) > 0:
+                    installation = Installation(
+                        app_id=row[0],
+                        enterprise_id=row[1],
+                        enterprise_name=row[2],
+                        enterprise_url=row[3],
+                        team_id=row[4],
+                        team_name=row[5],
+                        bot_token=row[6],
+                        bot_id=row[7],
+                        bot_user_id=row[8],
+                        bot_scopes=row[9],
+                        bot_refresh_token=row[10],
+                        bot_token_expires_at=row[11],
+                        user_id=row[12],
+                        user_token=row[13],
+                        user_scopes=row[14],
+                        user_refresh_token=row[15],
+                        user_token_expires_at=row[16],
+                        incoming_webhook_url=row[17],
+                        incoming_webhook_channel=row[18],
+                        incoming_webhook_channel_id=row[19],
+                        incoming_webhook_configuration_url=row[20],
+                        is_enterprise_install=row[21],
+                        token_type=row[22],
+                        installed_at=row[23],
+                    )
+
+                    if user_id is not None:
+                        # Retrieve the latest bot token, just in case
+                        # See also: https://github.com/slackapi/bolt-python/issues/664
+                        cur = conn.execute(
+                            """
+                            select
+                                bot_token,
+                                bot_id,
+                                bot_user_id,
+                                bot_scopes,
+                                bot_refresh_token,
+                                bot_token_expires_at
+                            from
+                                slack_installations
+                            where
+                                client_id = ?
+                                and
+                                enterprise_id = ?
+                                and
+                                team_id = ?
+                                and
+                                bot_token is not null
+                            order by installed_at desc
+                            limit 1
+                            """,
+                            [self.client_id, enterprise_id or "", team_id],
+                        )
+                        row = cur.fetchone()
+                        installation.bot_token = row[0]
+                        installation.bot_id = row[1]
+                        installation.bot_user_id = row[2]
+                        installation.bot_scopes = row[3]
+                        installation.bot_refresh_token = row[4]
+                        installation.bot_token_expires_at = row[5]
+
+                    return installation
+                return None
+
+        except Exception as e:
+            message = f"Failed to find an installation data for enterprise: {enterprise_id}, team: {team_id}: {e}"
+            if self.logger.level <= logging.DEBUG:
+                self.logger.exception(message)
+            else:
+                self.logger.warning(message)
+            return None
+
+    def delete_bot(self, *, enterprise_id: Optional[str], team_id: Optional[str]) -> None:
+        try:
+            with self.connect() as conn:
+                conn.execute(
+                    """
+                    delete
+                    from
+                        slack_bots
+                    where
+                        client_id = ?
+                        and
+                        enterprise_id = ?
+                        and
+                        team_id = ?
+                    """,
+                    [self.client_id, enterprise_id or "", team_id or ""],
+                )
+                conn.commit()
+        except Exception as e:
+            message = f"Failed to delete bot installation data for enterprise: {enterprise_id}, team: {team_id}: {e}"
+            if self.logger.level <= logging.DEBUG:
+                self.logger.exception(message)
+            else:
+                self.logger.warning(message)
+
+    def delete_installation(
+        self,
+        *,
+        enterprise_id: Optional[str],
+        team_id: Optional[str],
+        user_id: Optional[str] = None,
+    ) -> None:
+        try:
+            with self.connect() as conn:
+                if user_id is None:
+                    conn.execute(
+                        """
+                        delete
+                        from
+                            slack_installations
+                        where
+                            client_id = ?
+                            and
+                            enterprise_id = ?
+                            and
+                            team_id = ?
+                        """,
+                        [self.client_id, enterprise_id or "", team_id],
+                    )
+                else:
+                    conn.execute(
+                        """
+                        delete
+                        from
+                            slack_installations
+                        where
+                            client_id = ?
+                            and
+                            enterprise_id = ?
+                            and
+                            team_id = ?
+                            and
+                            user_id = ?
+                        """,
+                        [self.client_id, enterprise_id or "", team_id, user_id],
+                    )
+                conn.commit()
+        except Exception as e:
+            message = f"Failed to delete installation data for enterprise: {enterprise_id}, team: {team_id}: {e}"
+            if self.logger.level <= logging.DEBUG:
+                self.logger.exception(message)
+            else:
+                self.logger.warning(message)
+
+

The installation store interface.

+

The minimum required methods are:

+
    +
  • save(installation)
  • +
  • find_installation(enterprise_id, team_id, user_id, is_enterprise_install)
  • +
+

If you would like to properly handle app uninstallations and token revocations, +the following methods should be implemented.

+
    +
  • delete_installation(enterprise_id, team_id, user_id)
  • +
  • delete_all(enterprise_id, team_id)
  • +
+

If your app needs only bot scope installations, the simpler way to implement would be:

+
    +
  • save(installation)
  • +
  • find_bot(enterprise_id, team_id, is_enterprise_install)
  • +
  • delete_bot(enterprise_id, team_id)
  • +
  • delete_all(enterprise_id, team_id)
  • +
+

Ancestors

+ +

Instance variables

+
+
prop logger : logging.Logger
+
+
+ +Expand source code + +
@property
+def logger(self) -> Logger:
+    if self._logger is None:
+        self._logger = logging.getLogger(__name__)
+    return self._logger
+
+
+
+
+

Methods

+
+
+def connect(self) ‑> sqlite3.Connection +
+
+
+ +Expand source code + +
def connect(self) -> Connection:
+    if not self.init_called:
+        self.init()
+    return sqlite3.connect(database=self.database)
+
+
+
+
+def create_tables(self) +
+
+
+ +Expand source code + +
def create_tables(self):
+    with sqlite3.connect(database=self.database) as conn:
+        conn.execute(
+            """
+        create table slack_installations (
+            id integer primary key autoincrement,
+            client_id text not null,
+            app_id text not null,
+            enterprise_id text not null default '',
+            enterprise_name text,
+            enterprise_url text,
+            team_id text not null default '',
+            team_name text,
+            bot_token text,
+            bot_id text,
+            bot_user_id text,
+            bot_scopes text,
+            bot_refresh_token text,  -- since v3.8
+            bot_token_expires_at datetime,  -- since v3.8
+            user_id text not null,
+            user_token text,
+            user_scopes text,
+            user_refresh_token text,  -- since v3.8
+            user_token_expires_at datetime,  -- since v3.8
+            incoming_webhook_url text,
+            incoming_webhook_channel text,
+            incoming_webhook_channel_id text,
+            incoming_webhook_configuration_url text,
+            is_enterprise_install boolean not null default 0,
+            token_type text,
+            installed_at datetime not null default current_timestamp
+        );
+        """
+        )
+        conn.execute(
+            """
+        create index slack_installations_idx on slack_installations (
+            client_id,
+            enterprise_id,
+            team_id,
+            user_id,
+            installed_at
+        );
+        """
+        )
+        conn.execute(
+            """
+        create table slack_bots (
+            id integer primary key autoincrement,
+            client_id text not null,
+            app_id text not null,
+            enterprise_id text not null default '',
+            enterprise_name text,
+            team_id text not null default '',
+            team_name text,
+            bot_token text not null,
+            bot_id text not null,
+            bot_user_id text not null,
+            bot_scopes text,
+            bot_refresh_token text,  -- since v3.8
+            bot_token_expires_at datetime,  -- since v3.8
+            is_enterprise_install boolean not null default 0,
+            installed_at datetime not null default current_timestamp
+        );
+        """
+        )
+        conn.execute(
+            """
+        create index slack_bots_idx on slack_bots (
+            client_id,
+            enterprise_id,
+            team_id,
+            installed_at
+        );
+        """
+        )
+        self.logger.debug(f"Tables have been created (database: {self.database})")
+        conn.commit()
+
+
+
+
+def init(self) +
+
+
+ +Expand source code + +
def init(self):
+    try:
+        with sqlite3.connect(database=self.database) as conn:
+            cur = conn.execute("select count(1) from slack_installations;")
+            row_num = cur.fetchone()[0]
+            self.logger.debug(f"{row_num} installations are stored in {self.database}")
+    except Exception:
+        self.create_tables()
+    self.init_called = True
+
+
+
+
+

Inherited members

+ +
+
+
+
+ +
+ + + diff --git a/docs/reference/oauth/redirect_uri_page_renderer/index.html b/docs/reference/oauth/redirect_uri_page_renderer/index.html new file mode 100644 index 000000000..627904d44 --- /dev/null +++ b/docs/reference/oauth/redirect_uri_page_renderer/index.html @@ -0,0 +1,238 @@ + + + + + + +slack_sdk.oauth.redirect_uri_page_renderer API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.oauth.redirect_uri_page_renderer

+
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class RedirectUriPageRenderer +(*,
install_path: str,
redirect_uri_path: str,
success_url: str | None = None,
failure_url: str | None = None)
+
+
+
+ +Expand source code + +
class RedirectUriPageRenderer:
+    def __init__(
+        self,
+        *,
+        install_path: str,
+        redirect_uri_path: str,
+        success_url: Optional[str] = None,
+        failure_url: Optional[str] = None,
+    ):
+        self.install_path = install_path
+        self.redirect_uri_path = redirect_uri_path
+        self.success_url = success_url
+        self.failure_url = failure_url
+
+    def render_success_page(
+        self,
+        app_id: str,
+        team_id: Optional[str],
+        is_enterprise_install: Optional[bool] = None,
+        enterprise_url: Optional[str] = None,
+    ) -> str:
+        url = self.success_url
+        if url is None:
+            if is_enterprise_install is True and enterprise_url is not None and app_id is not None:
+                url = f"{enterprise_url}manage/organization/apps/profile/{app_id}/workspaces/add"
+            elif team_id is None or app_id is None:
+                url = "slack://open"
+            else:
+                url = f"slack://app?team={team_id}&id={app_id}"
+        browser_url = f"https://app.slack.com/client/{team_id}"
+
+        return f"""
+<html>
+<head>
+<meta http-equiv="refresh" content="0; URL={html.escape(url)}">
+<style>
+body {{
+  padding: 10px 15px;
+  font-family: verdana;
+  text-align: center;
+}}
+</style>
+</head>
+<body>
+<h2>Thank you!</h2>
+<p>Redirecting to the Slack App... click <a href="{html.escape(url)}">here</a>. If you use the browser version of Slack, click <a href="{html.escape(browser_url)}" target="_blank">this link</a> instead.</p>
+</body>
+</html>
+"""  # noqa: E501
+
+    def render_failure_page(self, reason: str) -> str:
+        return f"""
+<html>
+<head>
+<style>
+body {{
+  padding: 10px 15px;
+  font-family: verdana;
+  text-align: center;
+}}
+</style>
+</head>
+<body>
+<h2>Oops, Something Went Wrong!</h2>
+<p>Please try again from <a href="{html.escape(self.install_path)}">here</a> or contact the app owner (reason: {html.escape(reason)})</p>
+</body>
+</html>
+"""  # noqa: E501
+
+
+

Methods

+
+
+def render_failure_page(self, reason: str) ‑> str +
+
+
+ +Expand source code + +
    def render_failure_page(self, reason: str) -> str:
+        return f"""
+<html>
+<head>
+<style>
+body {{
+  padding: 10px 15px;
+  font-family: verdana;
+  text-align: center;
+}}
+</style>
+</head>
+<body>
+<h2>Oops, Something Went Wrong!</h2>
+<p>Please try again from <a href="{html.escape(self.install_path)}">here</a> or contact the app owner (reason: {html.escape(reason)})</p>
+</body>
+</html>
+"""  # noqa: E501
+
+
+
+
+def render_success_page(self,
app_id: str,
team_id: str | None,
is_enterprise_install: bool | None = None,
enterprise_url: str | None = None) ‑> str
+
+
+
+ +Expand source code + +
    def render_success_page(
+        self,
+        app_id: str,
+        team_id: Optional[str],
+        is_enterprise_install: Optional[bool] = None,
+        enterprise_url: Optional[str] = None,
+    ) -> str:
+        url = self.success_url
+        if url is None:
+            if is_enterprise_install is True and enterprise_url is not None and app_id is not None:
+                url = f"{enterprise_url}manage/organization/apps/profile/{app_id}/workspaces/add"
+            elif team_id is None or app_id is None:
+                url = "slack://open"
+            else:
+                url = f"slack://app?team={team_id}&id={app_id}"
+        browser_url = f"https://app.slack.com/client/{team_id}"
+
+        return f"""
+<html>
+<head>
+<meta http-equiv="refresh" content="0; URL={html.escape(url)}">
+<style>
+body {{
+  padding: 10px 15px;
+  font-family: verdana;
+  text-align: center;
+}}
+</style>
+</head>
+<body>
+<h2>Thank you!</h2>
+<p>Redirecting to the Slack App... click <a href="{html.escape(url)}">here</a>. If you use the browser version of Slack, click <a href="{html.escape(browser_url)}" target="_blank">this link</a> instead.</p>
+</body>
+</html>
+"""  # noqa: E501
+
+
+
+
+
+
+
+
+ +
+ + + diff --git a/docs/reference/oauth/state_store/amazon_s3/index.html b/docs/reference/oauth/state_store/amazon_s3/index.html new file mode 100644 index 000000000..40c6d5c4e --- /dev/null +++ b/docs/reference/oauth/state_store/amazon_s3/index.html @@ -0,0 +1,257 @@ + + + + + + +slack_sdk.oauth.state_store.amazon_s3 API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.oauth.state_store.amazon_s3

+
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class AmazonS3OAuthStateStore +(*,
s3_client: botocore.client.BaseClient,
bucket_name: str,
expiration_seconds: int,
logger: logging.Logger = <Logger slack_sdk.oauth.state_store.amazon_s3 (WARNING)>)
+
+
+
+ +Expand source code + +
class AmazonS3OAuthStateStore(OAuthStateStore, AsyncOAuthStateStore):
+    def __init__(
+        self,
+        *,
+        s3_client: BaseClient,
+        bucket_name: str,
+        expiration_seconds: int,
+        logger: Logger = logging.getLogger(__name__),
+    ):
+        self.s3_client = s3_client
+        self.bucket_name = bucket_name
+        self.expiration_seconds = expiration_seconds
+        self._logger = logger
+
+    @property
+    def logger(self) -> Logger:
+        if self._logger is None:
+            self._logger = logging.getLogger(__name__)
+        return self._logger
+
+    async def async_issue(self, *args, **kwargs) -> str:
+        return self.issue(*args, **kwargs)
+
+    async def async_consume(self, state: str) -> bool:
+        return self.consume(state)
+
+    def issue(self, *args, **kwargs) -> str:
+        state = str(uuid4())
+        response = self.s3_client.put_object(
+            Bucket=self.bucket_name,
+            Body=str(time.time()),
+            Key=state,
+        )
+        self.logger.debug(f"S3 put_object response: {response}")
+        return state
+
+    def consume(self, state: str) -> bool:
+        try:
+            fetch_response = self.s3_client.get_object(
+                Bucket=self.bucket_name,
+                Key=state,
+            )
+            self.logger.debug(f"S3 get_object response: {fetch_response}")
+            body = fetch_response["Body"].read().decode("utf-8")
+            created = float(body)
+            expiration = created + self.expiration_seconds
+            still_valid: bool = time.time() < expiration
+
+            deletion_response = self.s3_client.delete_object(
+                Bucket=self.bucket_name,
+                Key=state,
+            )
+            self.logger.debug(f"S3 delete_object response: {deletion_response}")
+            return still_valid
+        except Exception as e:
+            message = f"Failed to find any persistent data for state: {state} - {e}"
+            self.logger.warning(message)
+            return False
+
+
+

Ancestors

+ +

Instance variables

+
+
prop logger : logging.Logger
+
+
+ +Expand source code + +
@property
+def logger(self) -> Logger:
+    if self._logger is None:
+        self._logger = logging.getLogger(__name__)
+    return self._logger
+
+
+
+
+

Methods

+
+
+async def async_consume(self, state: str) ‑> bool +
+
+
+ +Expand source code + +
async def async_consume(self, state: str) -> bool:
+    return self.consume(state)
+
+
+
+
+async def async_issue(self, *args, **kwargs) ‑> str +
+
+
+ +Expand source code + +
async def async_issue(self, *args, **kwargs) -> str:
+    return self.issue(*args, **kwargs)
+
+
+
+
+def consume(self, state: str) ‑> bool +
+
+
+ +Expand source code + +
def consume(self, state: str) -> bool:
+    try:
+        fetch_response = self.s3_client.get_object(
+            Bucket=self.bucket_name,
+            Key=state,
+        )
+        self.logger.debug(f"S3 get_object response: {fetch_response}")
+        body = fetch_response["Body"].read().decode("utf-8")
+        created = float(body)
+        expiration = created + self.expiration_seconds
+        still_valid: bool = time.time() < expiration
+
+        deletion_response = self.s3_client.delete_object(
+            Bucket=self.bucket_name,
+            Key=state,
+        )
+        self.logger.debug(f"S3 delete_object response: {deletion_response}")
+        return still_valid
+    except Exception as e:
+        message = f"Failed to find any persistent data for state: {state} - {e}"
+        self.logger.warning(message)
+        return False
+
+
+
+
+def issue(self, *args, **kwargs) ‑> str +
+
+
+ +Expand source code + +
def issue(self, *args, **kwargs) -> str:
+    state = str(uuid4())
+    response = self.s3_client.put_object(
+        Bucket=self.bucket_name,
+        Body=str(time.time()),
+        Key=state,
+    )
+    self.logger.debug(f"S3 put_object response: {response}")
+    return state
+
+
+
+
+
+
+
+
+ +
+ + + diff --git a/docs/reference/oauth/state_store/async_state_store.html b/docs/reference/oauth/state_store/async_state_store.html new file mode 100644 index 000000000..3f1fa969b --- /dev/null +++ b/docs/reference/oauth/state_store/async_state_store.html @@ -0,0 +1,153 @@ + + + + + + +slack_sdk.oauth.state_store.async_state_store API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.oauth.state_store.async_state_store

+
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class AsyncOAuthStateStore +
+
+
+ +Expand source code + +
class AsyncOAuthStateStore:
+    @property
+    def logger(self) -> Logger:
+        raise NotImplementedError()
+
+    async def async_issue(self, *args, **kwargs) -> str:
+        raise NotImplementedError()
+
+    async def async_consume(self, state: str) -> bool:
+        raise NotImplementedError()
+
+
+

Subclasses

+ +

Instance variables

+
+
prop logger : logging.Logger
+
+
+ +Expand source code + +
@property
+def logger(self) -> Logger:
+    raise NotImplementedError()
+
+
+
+
+

Methods

+
+
+async def async_consume(self, state: str) ‑> bool +
+
+
+ +Expand source code + +
async def async_consume(self, state: str) -> bool:
+    raise NotImplementedError()
+
+
+
+
+async def async_issue(self, *args, **kwargs) ‑> str +
+
+
+ +Expand source code + +
async def async_issue(self, *args, **kwargs) -> str:
+    raise NotImplementedError()
+
+
+
+
+
+
+
+
+ +
+ + + diff --git a/docs/reference/oauth/state_store/file/index.html b/docs/reference/oauth/state_store/file/index.html new file mode 100644 index 000000000..62e7be40e --- /dev/null +++ b/docs/reference/oauth/state_store/file/index.html @@ -0,0 +1,250 @@ + + + + + + +slack_sdk.oauth.state_store.file API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.oauth.state_store.file

+
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class FileOAuthStateStore +(*,
expiration_seconds: int,
base_dir: str = '$HOME/.bolt-app-oauth-state',
client_id: str | None = None,
logger: logging.Logger = <Logger slack_sdk.oauth.state_store.file (WARNING)>)
+
+
+
+ +Expand source code + +
class FileOAuthStateStore(OAuthStateStore, AsyncOAuthStateStore):
+    def __init__(
+        self,
+        *,
+        expiration_seconds: int,
+        base_dir: str = str(Path.home()) + "/.bolt-app-oauth-state",
+        client_id: Optional[str] = None,
+        logger: Logger = logging.getLogger(__name__),
+    ):
+        self.expiration_seconds = expiration_seconds
+
+        self.base_dir = base_dir
+        self.client_id = client_id
+        if self.client_id is not None:
+            self.base_dir = f"{self.base_dir}/{self.client_id}"
+        self._logger = logger
+
+    @property
+    def logger(self) -> Logger:
+        if self._logger is None:
+            self._logger = logging.getLogger(__name__)
+        return self._logger
+
+    async def async_issue(self, *args, **kwargs) -> str:
+        return self.issue(*args, **kwargs)
+
+    async def async_consume(self, state: str) -> bool:
+        return self.consume(state)
+
+    def issue(self, *args, **kwargs) -> str:
+        state = str(uuid4())
+        self._mkdir(self.base_dir)
+        filepath = f"{self.base_dir}/{state}"
+        with open(filepath, "w") as f:
+            content = str(time.time())
+            f.write(content)
+        return state
+
+    def consume(self, state: str) -> bool:
+        filepath = f"{self.base_dir}/{state}"
+        try:
+            with open(filepath) as f:
+                created = float(f.read())
+                expiration = created + self.expiration_seconds
+                still_valid: bool = time.time() < expiration
+
+            os.remove(filepath)  # consume the file by deleting it
+            return still_valid
+
+        except FileNotFoundError as e:
+            message = f"Failed to find any persistent data for state: {state} - {e}"
+            self.logger.warning(message)
+            return False
+
+    @staticmethod
+    def _mkdir(path: Union[str, Path]):
+        if isinstance(path, str):
+            path = Path(path)
+        path.mkdir(parents=True, exist_ok=True)
+
+
+

Ancestors

+ +

Instance variables

+
+
prop logger : logging.Logger
+
+
+ +Expand source code + +
@property
+def logger(self) -> Logger:
+    if self._logger is None:
+        self._logger = logging.getLogger(__name__)
+    return self._logger
+
+
+
+
+

Methods

+
+
+async def async_consume(self, state: str) ‑> bool +
+
+
+ +Expand source code + +
async def async_consume(self, state: str) -> bool:
+    return self.consume(state)
+
+
+
+
+async def async_issue(self, *args, **kwargs) ‑> str +
+
+
+ +Expand source code + +
async def async_issue(self, *args, **kwargs) -> str:
+    return self.issue(*args, **kwargs)
+
+
+
+
+def consume(self, state: str) ‑> bool +
+
+
+ +Expand source code + +
def consume(self, state: str) -> bool:
+    filepath = f"{self.base_dir}/{state}"
+    try:
+        with open(filepath) as f:
+            created = float(f.read())
+            expiration = created + self.expiration_seconds
+            still_valid: bool = time.time() < expiration
+
+        os.remove(filepath)  # consume the file by deleting it
+        return still_valid
+
+    except FileNotFoundError as e:
+        message = f"Failed to find any persistent data for state: {state} - {e}"
+        self.logger.warning(message)
+        return False
+
+
+
+
+def issue(self, *args, **kwargs) ‑> str +
+
+
+ +Expand source code + +
def issue(self, *args, **kwargs) -> str:
+    state = str(uuid4())
+    self._mkdir(self.base_dir)
+    filepath = f"{self.base_dir}/{state}"
+    with open(filepath, "w") as f:
+        content = str(time.time())
+        f.write(content)
+    return state
+
+
+
+
+
+
+
+
+ +
+ + + diff --git a/docs/reference/oauth/state_store/index.html b/docs/reference/oauth/state_store/index.html new file mode 100644 index 000000000..38a016b32 --- /dev/null +++ b/docs/reference/oauth/state_store/index.html @@ -0,0 +1,369 @@ + + + + + + +slack_sdk.oauth.state_store API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.oauth.state_store

+
+
+

OAuth state parameter data store

+

Refer to https://docs.slack.dev/tools/python-slack-sdk/oauth for details.

+
+
+

Sub-modules

+
+
slack_sdk.oauth.state_store.amazon_s3
+
+
+
+
slack_sdk.oauth.state_store.async_state_store
+
+
+
+
slack_sdk.oauth.state_store.file
+
+
+
+
slack_sdk.oauth.state_store.sqlalchemy
+
+
+
+
slack_sdk.oauth.state_store.sqlite3
+
+
+
+
slack_sdk.oauth.state_store.state_store
+
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class FileOAuthStateStore +(*,
expiration_seconds: int,
base_dir: str = '$HOME/.bolt-app-oauth-state',
client_id: str | None = None,
logger: logging.Logger = <Logger slack_sdk.oauth.state_store.file (WARNING)>)
+
+
+
+ +Expand source code + +
class FileOAuthStateStore(OAuthStateStore, AsyncOAuthStateStore):
+    def __init__(
+        self,
+        *,
+        expiration_seconds: int,
+        base_dir: str = str(Path.home()) + "/.bolt-app-oauth-state",
+        client_id: Optional[str] = None,
+        logger: Logger = logging.getLogger(__name__),
+    ):
+        self.expiration_seconds = expiration_seconds
+
+        self.base_dir = base_dir
+        self.client_id = client_id
+        if self.client_id is not None:
+            self.base_dir = f"{self.base_dir}/{self.client_id}"
+        self._logger = logger
+
+    @property
+    def logger(self) -> Logger:
+        if self._logger is None:
+            self._logger = logging.getLogger(__name__)
+        return self._logger
+
+    async def async_issue(self, *args, **kwargs) -> str:
+        return self.issue(*args, **kwargs)
+
+    async def async_consume(self, state: str) -> bool:
+        return self.consume(state)
+
+    def issue(self, *args, **kwargs) -> str:
+        state = str(uuid4())
+        self._mkdir(self.base_dir)
+        filepath = f"{self.base_dir}/{state}"
+        with open(filepath, "w") as f:
+            content = str(time.time())
+            f.write(content)
+        return state
+
+    def consume(self, state: str) -> bool:
+        filepath = f"{self.base_dir}/{state}"
+        try:
+            with open(filepath) as f:
+                created = float(f.read())
+                expiration = created + self.expiration_seconds
+                still_valid: bool = time.time() < expiration
+
+            os.remove(filepath)  # consume the file by deleting it
+            return still_valid
+
+        except FileNotFoundError as e:
+            message = f"Failed to find any persistent data for state: {state} - {e}"
+            self.logger.warning(message)
+            return False
+
+    @staticmethod
+    def _mkdir(path: Union[str, Path]):
+        if isinstance(path, str):
+            path = Path(path)
+        path.mkdir(parents=True, exist_ok=True)
+
+
+

Ancestors

+ +

Instance variables

+
+
prop logger : logging.Logger
+
+
+ +Expand source code + +
@property
+def logger(self) -> Logger:
+    if self._logger is None:
+        self._logger = logging.getLogger(__name__)
+    return self._logger
+
+
+
+
+

Methods

+
+
+async def async_consume(self, state: str) ‑> bool +
+
+
+ +Expand source code + +
async def async_consume(self, state: str) -> bool:
+    return self.consume(state)
+
+
+
+
+async def async_issue(self, *args, **kwargs) ‑> str +
+
+
+ +Expand source code + +
async def async_issue(self, *args, **kwargs) -> str:
+    return self.issue(*args, **kwargs)
+
+
+
+
+def consume(self, state: str) ‑> bool +
+
+
+ +Expand source code + +
def consume(self, state: str) -> bool:
+    filepath = f"{self.base_dir}/{state}"
+    try:
+        with open(filepath) as f:
+            created = float(f.read())
+            expiration = created + self.expiration_seconds
+            still_valid: bool = time.time() < expiration
+
+        os.remove(filepath)  # consume the file by deleting it
+        return still_valid
+
+    except FileNotFoundError as e:
+        message = f"Failed to find any persistent data for state: {state} - {e}"
+        self.logger.warning(message)
+        return False
+
+
+
+
+def issue(self, *args, **kwargs) ‑> str +
+
+
+ +Expand source code + +
def issue(self, *args, **kwargs) -> str:
+    state = str(uuid4())
+    self._mkdir(self.base_dir)
+    filepath = f"{self.base_dir}/{state}"
+    with open(filepath, "w") as f:
+        content = str(time.time())
+        f.write(content)
+    return state
+
+
+
+
+
+
+class OAuthStateStore +
+
+
+ +Expand source code + +
class OAuthStateStore:
+    @property
+    def logger(self) -> Logger:
+        raise NotImplementedError()
+
+    def issue(self, *args, **kwargs) -> str:
+        raise NotImplementedError()
+
+    def consume(self, state: str) -> bool:
+        raise NotImplementedError()
+
+
+

Subclasses

+ +

Instance variables

+
+
prop logger : logging.Logger
+
+
+ +Expand source code + +
@property
+def logger(self) -> Logger:
+    raise NotImplementedError()
+
+
+
+
+

Methods

+
+
+def consume(self, state: str) ‑> bool +
+
+
+ +Expand source code + +
def consume(self, state: str) -> bool:
+    raise NotImplementedError()
+
+
+
+
+def issue(self, *args, **kwargs) ‑> str +
+
+
+ +Expand source code + +
def issue(self, *args, **kwargs) -> str:
+    raise NotImplementedError()
+
+
+
+
+
+
+
+
+ +
+ + + diff --git a/docs/reference/oauth/state_store/sqlalchemy/index.html b/docs/reference/oauth/state_store/sqlalchemy/index.html new file mode 100644 index 000000000..29fe36884 --- /dev/null +++ b/docs/reference/oauth/state_store/sqlalchemy/index.html @@ -0,0 +1,491 @@ + + + + + + +slack_sdk.oauth.state_store.sqlalchemy API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.oauth.state_store.sqlalchemy

+
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class AsyncSQLAlchemyOAuthStateStore +(expiration_seconds: int,
engine: sqlalchemy.ext.asyncio.engine.AsyncEngine,
logger: logging.Logger = <Logger slack_sdk.oauth.state_store.sqlalchemy (WARNING)>,
table_name: str = 'slack_oauth_states')
+
+
+
+ +Expand source code + +
class AsyncSQLAlchemyOAuthStateStore(AsyncOAuthStateStore):
+    default_table_name: str = "slack_oauth_states"
+
+    expiration_seconds: int
+    engine: AsyncEngine
+    metadata: MetaData
+    oauth_states: Table
+
+    @classmethod
+    def build_oauth_states_table(cls, metadata: MetaData, table_name: str) -> Table:
+        return sqlalchemy.Table(
+            table_name,
+            metadata,
+            metadata,
+            Column("id", Integer, primary_key=True, autoincrement=True),
+            Column("state", String(200), nullable=False),
+            Column("expire_at", DateTime, nullable=False),
+        )
+
+    def __init__(
+        self,
+        expiration_seconds: int,
+        engine: AsyncEngine,
+        logger: Logger = logging.getLogger(__name__),
+        table_name: str = default_table_name,
+    ):
+        self.expiration_seconds = expiration_seconds
+        self._logger = logger
+        self.engine = engine
+        self.metadata = MetaData()
+        self.oauth_states = self.build_oauth_states_table(self.metadata, table_name)
+
+    async def create_tables(self):
+        async with self.engine.begin() as conn:
+            await conn.run_sync(self.metadata.create_all)
+
+    @property
+    def logger(self) -> Logger:
+        if self._logger is None:
+            self._logger = logging.getLogger(__name__)
+        return self._logger
+
+    async def async_issue(self, *args, **kwargs) -> str:
+        state: str = str(uuid4())
+        now = datetime.utcfromtimestamp(time.time() + self.expiration_seconds)
+        async with self.engine.begin() as conn:
+            await conn.execute(
+                self.oauth_states.insert(),
+                {"state": state, "expire_at": now},
+            )
+        return state
+
+    async def async_consume(self, state: str) -> bool:
+        try:
+            async with self.engine.begin() as conn:
+                c = self.oauth_states.c
+                query = self.oauth_states.select().where(and_(c.state == state, c.expire_at > datetime.utcnow()))
+                result = await conn.execute(query)
+                for row in result.mappings():
+                    self.logger.debug(f"consume's query result: {row}")
+                    await conn.execute(self.oauth_states.delete().where(c.id == row["id"]))
+                    return True
+            return False
+        except Exception as e:
+            message = f"Failed to find any persistent data for state: {state} - {e}"
+            self.logger.warning(message)
+            return False
+
+
+

Ancestors

+ +

Class variables

+
+
var default_table_name : str
+
+

The type of the None singleton.

+
+
var engine : sqlalchemy.ext.asyncio.engine.AsyncEngine
+
+

The type of the None singleton.

+
+
var expiration_seconds : int
+
+

The type of the None singleton.

+
+
var metadata : sqlalchemy.sql.schema.MetaData
+
+

The type of the None singleton.

+
+
var oauth_states : sqlalchemy.sql.schema.Table
+
+

The type of the None singleton.

+
+
+

Static methods

+
+
+def build_oauth_states_table(metadata: sqlalchemy.sql.schema.MetaData, table_name: str) ‑> sqlalchemy.sql.schema.Table +
+
+
+
+
+

Instance variables

+
+
prop logger : logging.Logger
+
+
+ +Expand source code + +
@property
+def logger(self) -> Logger:
+    if self._logger is None:
+        self._logger = logging.getLogger(__name__)
+    return self._logger
+
+
+
+
+

Methods

+
+
+async def async_consume(self, state: str) ‑> bool +
+
+
+ +Expand source code + +
async def async_consume(self, state: str) -> bool:
+    try:
+        async with self.engine.begin() as conn:
+            c = self.oauth_states.c
+            query = self.oauth_states.select().where(and_(c.state == state, c.expire_at > datetime.utcnow()))
+            result = await conn.execute(query)
+            for row in result.mappings():
+                self.logger.debug(f"consume's query result: {row}")
+                await conn.execute(self.oauth_states.delete().where(c.id == row["id"]))
+                return True
+        return False
+    except Exception as e:
+        message = f"Failed to find any persistent data for state: {state} - {e}"
+        self.logger.warning(message)
+        return False
+
+
+
+
+async def async_issue(self, *args, **kwargs) ‑> str +
+
+
+ +Expand source code + +
async def async_issue(self, *args, **kwargs) -> str:
+    state: str = str(uuid4())
+    now = datetime.utcfromtimestamp(time.time() + self.expiration_seconds)
+    async with self.engine.begin() as conn:
+        await conn.execute(
+            self.oauth_states.insert(),
+            {"state": state, "expire_at": now},
+        )
+    return state
+
+
+
+
+async def create_tables(self) +
+
+
+ +Expand source code + +
async def create_tables(self):
+    async with self.engine.begin() as conn:
+        await conn.run_sync(self.metadata.create_all)
+
+
+
+
+
+
+class SQLAlchemyOAuthStateStore +(expiration_seconds: int,
engine: sqlalchemy.engine.base.Engine,
logger: logging.Logger = <Logger slack_sdk.oauth.state_store.sqlalchemy (WARNING)>,
table_name: str = 'slack_oauth_states')
+
+
+
+ +Expand source code + +
class SQLAlchemyOAuthStateStore(OAuthStateStore):
+    default_table_name: str = "slack_oauth_states"
+
+    expiration_seconds: int
+    engine: Engine
+    metadata: MetaData
+    oauth_states: Table
+
+    @classmethod
+    def build_oauth_states_table(cls, metadata: MetaData, table_name: str) -> Table:
+        return sqlalchemy.Table(
+            table_name,
+            metadata,
+            metadata,
+            Column("id", Integer, primary_key=True, autoincrement=True),
+            Column("state", String(200), nullable=False),
+            Column("expire_at", DateTime, nullable=False),
+        )
+
+    def __init__(
+        self,
+        expiration_seconds: int,
+        engine: Engine,
+        logger: Logger = logging.getLogger(__name__),
+        table_name: str = default_table_name,
+    ):
+        self.expiration_seconds = expiration_seconds
+        self._logger = logger
+        self.engine = engine
+        self.metadata = MetaData()
+        self.oauth_states = self.build_oauth_states_table(self.metadata, table_name)
+
+    def create_tables(self):
+        self.metadata.create_all(self.engine)
+
+    @property
+    def logger(self) -> Logger:
+        if self._logger is None:
+            self._logger = logging.getLogger(__name__)
+        return self._logger
+
+    def issue(self, *args, **kwargs) -> str:
+        state: str = str(uuid4())
+        now = datetime.utcfromtimestamp(time.time() + self.expiration_seconds)
+        with self.engine.begin() as conn:
+            conn.execute(
+                self.oauth_states.insert(),
+                {"state": state, "expire_at": now},
+            )
+        return state
+
+    def consume(self, state: str) -> bool:
+        try:
+            with self.engine.begin() as conn:
+                c = self.oauth_states.c
+                query = self.oauth_states.select().where(and_(c.state == state, c.expire_at > datetime.utcnow()))
+                result = conn.execute(query)
+                for row in result.mappings():
+                    self.logger.debug(f"consume's query result: {row}")
+                    conn.execute(self.oauth_states.delete().where(c.id == row["id"]))
+                    return True
+            return False
+        except Exception as e:
+            message = f"Failed to find any persistent data for state: {state} - {e}"
+            self.logger.warning(message)
+            return False
+
+
+

Ancestors

+ +

Class variables

+
+
var default_table_name : str
+
+

The type of the None singleton.

+
+
var engine : sqlalchemy.engine.base.Engine
+
+

The type of the None singleton.

+
+
var expiration_seconds : int
+
+

The type of the None singleton.

+
+
var metadata : sqlalchemy.sql.schema.MetaData
+
+

The type of the None singleton.

+
+
var oauth_states : sqlalchemy.sql.schema.Table
+
+

The type of the None singleton.

+
+
+

Static methods

+
+
+def build_oauth_states_table(metadata: sqlalchemy.sql.schema.MetaData, table_name: str) ‑> sqlalchemy.sql.schema.Table +
+
+
+
+
+

Instance variables

+
+
prop logger : logging.Logger
+
+
+ +Expand source code + +
@property
+def logger(self) -> Logger:
+    if self._logger is None:
+        self._logger = logging.getLogger(__name__)
+    return self._logger
+
+
+
+
+

Methods

+
+
+def consume(self, state: str) ‑> bool +
+
+
+ +Expand source code + +
def consume(self, state: str) -> bool:
+    try:
+        with self.engine.begin() as conn:
+            c = self.oauth_states.c
+            query = self.oauth_states.select().where(and_(c.state == state, c.expire_at > datetime.utcnow()))
+            result = conn.execute(query)
+            for row in result.mappings():
+                self.logger.debug(f"consume's query result: {row}")
+                conn.execute(self.oauth_states.delete().where(c.id == row["id"]))
+                return True
+        return False
+    except Exception as e:
+        message = f"Failed to find any persistent data for state: {state} - {e}"
+        self.logger.warning(message)
+        return False
+
+
+
+
+def create_tables(self) +
+
+
+ +Expand source code + +
def create_tables(self):
+    self.metadata.create_all(self.engine)
+
+
+
+
+def issue(self, *args, **kwargs) ‑> str +
+
+
+ +Expand source code + +
def issue(self, *args, **kwargs) -> str:
+    state: str = str(uuid4())
+    now = datetime.utcfromtimestamp(time.time() + self.expiration_seconds)
+    with self.engine.begin() as conn:
+        conn.execute(
+            self.oauth_states.insert(),
+            {"state": state, "expire_at": now},
+        )
+    return state
+
+
+
+
+
+
+
+
+ +
+ + + diff --git a/docs/reference/oauth/state_store/sqlite3/index.html b/docs/reference/oauth/state_store/sqlite3/index.html new file mode 100644 index 000000000..cd0b75b8b --- /dev/null +++ b/docs/reference/oauth/state_store/sqlite3/index.html @@ -0,0 +1,345 @@ + + + + + + +slack_sdk.oauth.state_store.sqlite3 API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.oauth.state_store.sqlite3

+
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class SQLite3OAuthStateStore +(*,
database: str,
expiration_seconds: int,
logger: logging.Logger = <Logger slack_sdk.oauth.state_store.sqlite3 (WARNING)>)
+
+
+
+ +Expand source code + +
class SQLite3OAuthStateStore(OAuthStateStore, AsyncOAuthStateStore):
+    def __init__(
+        self,
+        *,
+        database: str,
+        expiration_seconds: int,
+        logger: Logger = logging.getLogger(__name__),
+    ):
+        self.database = database
+        self.expiration_seconds = expiration_seconds
+        self.init_called = False
+        self._logger = logger
+
+    @property
+    def logger(self) -> Logger:
+        if self._logger is None:
+            self._logger = logging.getLogger(__name__)
+        return self._logger
+
+    def init(self):
+        try:
+            with sqlite3.connect(database=self.database) as conn:
+                cur = conn.execute("select count(1) from oauth_states;")
+                row_num = cur.fetchone()[0]
+                self.logger.debug(f"{row_num} oauth states are stored in {self.database}")
+        except Exception:
+            self.create_tables()
+        self.init_called = True
+
+    def connect(self) -> Connection:
+        if not self.init_called:
+            self.init()
+        return sqlite3.connect(database=self.database)
+
+    def create_tables(self):
+        with sqlite3.connect(database=self.database) as conn:
+            conn.execute(
+                """
+            create table oauth_states (
+                id integer primary key autoincrement,
+                state text not null,
+                expire_at datetime not null
+            );
+            """
+            )
+            self.logger.debug(f"Tables have been created (database: {self.database})")
+            conn.commit()
+
+    async def async_issue(self, *args, **kwargs) -> str:
+        return self.issue(*args, **kwargs)
+
+    async def async_consume(self, state: str) -> bool:
+        return self.consume(state)
+
+    def issue(self, *args, **kwargs) -> str:
+        state: str = str(uuid4())
+        with self.connect() as conn:
+            parameters = [
+                state,
+                time.time() + self.expiration_seconds,
+            ]
+            conn.execute("insert into oauth_states (state, expire_at) values (?, ?);", parameters)
+            self.logger.debug(f"issue's insertion result: {parameters} (database: {self.database})")
+            conn.commit()
+        return state
+
+    def consume(self, state: str) -> bool:
+        try:
+            with self.connect() as conn:
+                cur = conn.execute(
+                    "select id, state from oauth_states where state = ? and expire_at > ?;",
+                    [state, time.time()],
+                )
+                row = cur.fetchone()
+                self.logger.debug(f"consume's query result: {row} (database: {self.database})")
+                if row and len(row) > 0:
+                    id = row[0]
+                    conn.execute("delete from oauth_states where id = ?;", [id])
+                    conn.commit()
+                    return True
+            return False
+        except Exception as e:
+            message = f"Failed to find any persistent data for state: {state} - {e}"
+            self.logger.warning(message)
+            return False
+
+
+

Ancestors

+ +

Instance variables

+
+
prop logger : logging.Logger
+
+
+ +Expand source code + +
@property
+def logger(self) -> Logger:
+    if self._logger is None:
+        self._logger = logging.getLogger(__name__)
+    return self._logger
+
+
+
+
+

Methods

+
+
+async def async_consume(self, state: str) ‑> bool +
+
+
+ +Expand source code + +
async def async_consume(self, state: str) -> bool:
+    return self.consume(state)
+
+
+
+
+async def async_issue(self, *args, **kwargs) ‑> str +
+
+
+ +Expand source code + +
async def async_issue(self, *args, **kwargs) -> str:
+    return self.issue(*args, **kwargs)
+
+
+
+
+def connect(self) ‑> sqlite3.Connection +
+
+
+ +Expand source code + +
def connect(self) -> Connection:
+    if not self.init_called:
+        self.init()
+    return sqlite3.connect(database=self.database)
+
+
+
+
+def consume(self, state: str) ‑> bool +
+
+
+ +Expand source code + +
def consume(self, state: str) -> bool:
+    try:
+        with self.connect() as conn:
+            cur = conn.execute(
+                "select id, state from oauth_states where state = ? and expire_at > ?;",
+                [state, time.time()],
+            )
+            row = cur.fetchone()
+            self.logger.debug(f"consume's query result: {row} (database: {self.database})")
+            if row and len(row) > 0:
+                id = row[0]
+                conn.execute("delete from oauth_states where id = ?;", [id])
+                conn.commit()
+                return True
+        return False
+    except Exception as e:
+        message = f"Failed to find any persistent data for state: {state} - {e}"
+        self.logger.warning(message)
+        return False
+
+
+
+
+def create_tables(self) +
+
+
+ +Expand source code + +
def create_tables(self):
+    with sqlite3.connect(database=self.database) as conn:
+        conn.execute(
+            """
+        create table oauth_states (
+            id integer primary key autoincrement,
+            state text not null,
+            expire_at datetime not null
+        );
+        """
+        )
+        self.logger.debug(f"Tables have been created (database: {self.database})")
+        conn.commit()
+
+
+
+
+def init(self) +
+
+
+ +Expand source code + +
def init(self):
+    try:
+        with sqlite3.connect(database=self.database) as conn:
+            cur = conn.execute("select count(1) from oauth_states;")
+            row_num = cur.fetchone()[0]
+            self.logger.debug(f"{row_num} oauth states are stored in {self.database}")
+    except Exception:
+        self.create_tables()
+    self.init_called = True
+
+
+
+
+def issue(self, *args, **kwargs) ‑> str +
+
+
+ +Expand source code + +
def issue(self, *args, **kwargs) -> str:
+    state: str = str(uuid4())
+    with self.connect() as conn:
+        parameters = [
+            state,
+            time.time() + self.expiration_seconds,
+        ]
+        conn.execute("insert into oauth_states (state, expire_at) values (?, ?);", parameters)
+        self.logger.debug(f"issue's insertion result: {parameters} (database: {self.database})")
+        conn.commit()
+    return state
+
+
+
+
+
+
+
+
+ +
+ + + diff --git a/docs/reference/oauth/state_store/state_store.html b/docs/reference/oauth/state_store/state_store.html new file mode 100644 index 000000000..9734a2bba --- /dev/null +++ b/docs/reference/oauth/state_store/state_store.html @@ -0,0 +1,153 @@ + + + + + + +slack_sdk.oauth.state_store.state_store API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.oauth.state_store.state_store

+
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class OAuthStateStore +
+
+
+ +Expand source code + +
class OAuthStateStore:
+    @property
+    def logger(self) -> Logger:
+        raise NotImplementedError()
+
+    def issue(self, *args, **kwargs) -> str:
+        raise NotImplementedError()
+
+    def consume(self, state: str) -> bool:
+        raise NotImplementedError()
+
+
+

Subclasses

+ +

Instance variables

+
+
prop logger : logging.Logger
+
+
+ +Expand source code + +
@property
+def logger(self) -> Logger:
+    raise NotImplementedError()
+
+
+
+
+

Methods

+
+
+def consume(self, state: str) ‑> bool +
+
+
+ +Expand source code + +
def consume(self, state: str) -> bool:
+    raise NotImplementedError()
+
+
+
+
+def issue(self, *args, **kwargs) ‑> str +
+
+
+ +Expand source code + +
def issue(self, *args, **kwargs) -> str:
+    raise NotImplementedError()
+
+
+
+
+
+
+
+
+ +
+ + + diff --git a/docs/reference/oauth/state_utils/index.html b/docs/reference/oauth/state_utils/index.html new file mode 100644 index 000000000..d732babfd --- /dev/null +++ b/docs/reference/oauth/state_utils/index.html @@ -0,0 +1,212 @@ + + + + + + +slack_sdk.oauth.state_utils API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.oauth.state_utils

+
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class OAuthStateUtils +(*, cookie_name: str = 'slack-app-oauth-state', expiration_seconds: int = 600) +
+
+
+ +Expand source code + +
class OAuthStateUtils:
+    cookie_name: str
+    expiration_seconds: int
+
+    default_cookie_name: str = "slack-app-oauth-state"
+    default_expiration_seconds: int = 60 * 10  # 10 minutes
+
+    def __init__(
+        self,
+        *,
+        cookie_name: str = default_cookie_name,
+        expiration_seconds: int = default_expiration_seconds,
+    ):
+        self.cookie_name = cookie_name
+        self.expiration_seconds = expiration_seconds
+
+    def build_set_cookie_for_new_state(self, state: str) -> str:
+        return f"{self.cookie_name}={state}; " "Secure; " "HttpOnly; " "Path=/; " f"Max-Age={self.expiration_seconds}"
+
+    def build_set_cookie_for_deletion(self) -> str:
+        return f"{self.cookie_name}=deleted; " "Secure; " "HttpOnly; " "Path=/; " "Expires=Thu, 01 Jan 1970 00:00:00 GMT"
+
+    def is_valid_browser(
+        self,
+        state: Optional[str],
+        request_headers: Dict[str, Union[str, Sequence[str]]],
+    ) -> bool:
+        if state is None or request_headers is None or request_headers.get("cookie", None) is None:
+            return False
+        cookies = request_headers["cookie"]
+        if isinstance(cookies, str):
+            cookies = [cookies]
+        for cookie in cookies:
+            values = cookie.split(";")
+            for value in values:
+                # handle quoted cookie values (e.g. due to base64 encoding)
+                if value.strip().replace('"', "").replace("'", "") == f"{self.cookie_name}={state}":
+                    return True
+        return False
+
+
+

Class variables

+
+
var cookie_name : str
+
+

The type of the None singleton.

+
+ +
+

The type of the None singleton.

+
+
var default_expiration_seconds : int
+
+

The type of the None singleton.

+
+
var expiration_seconds : int
+
+

The type of the None singleton.

+
+
+

Methods

+
+ +
+
+ +Expand source code + +
def build_set_cookie_for_deletion(self) -> str:
+    return f"{self.cookie_name}=deleted; " "Secure; " "HttpOnly; " "Path=/; " "Expires=Thu, 01 Jan 1970 00:00:00 GMT"
+
+
+
+ +
+
+ +Expand source code + +
def build_set_cookie_for_new_state(self, state: str) -> str:
+    return f"{self.cookie_name}={state}; " "Secure; " "HttpOnly; " "Path=/; " f"Max-Age={self.expiration_seconds}"
+
+
+
+
+def is_valid_browser(self, state: str | None, request_headers: Dict[str, str | Sequence[str]]) ‑> bool +
+
+
+ +Expand source code + +
def is_valid_browser(
+    self,
+    state: Optional[str],
+    request_headers: Dict[str, Union[str, Sequence[str]]],
+) -> bool:
+    if state is None or request_headers is None or request_headers.get("cookie", None) is None:
+        return False
+    cookies = request_headers["cookie"]
+    if isinstance(cookies, str):
+        cookies = [cookies]
+    for cookie in cookies:
+        values = cookie.split(";")
+        for value in values:
+            # handle quoted cookie values (e.g. due to base64 encoding)
+            if value.strip().replace('"', "").replace("'", "") == f"{self.cookie_name}={state}":
+                return True
+    return False
+
+
+
+
+
+
+
+
+ +
+ + + diff --git a/docs/reference/oauth/token_rotation/async_rotator.html b/docs/reference/oauth/token_rotation/async_rotator.html new file mode 100644 index 000000000..35d64f362 --- /dev/null +++ b/docs/reference/oauth/token_rotation/async_rotator.html @@ -0,0 +1,424 @@ + + + + + + +slack_sdk.oauth.token_rotation.async_rotator API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.oauth.token_rotation.async_rotator

+
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class AsyncTokenRotator +(*,
client_id: str,
client_secret: str,
client: AsyncWebClient | None = None)
+
+
+
+ +Expand source code + +
class AsyncTokenRotator:
+    client: AsyncWebClient
+    client_id: str
+    client_secret: str
+
+    def __init__(
+        self,
+        *,
+        client_id: str,
+        client_secret: str,
+        client: Optional[AsyncWebClient] = None,
+    ):
+        self.client = client if client is not None else AsyncWebClient(token=None)
+        self.client_id = client_id
+        self.client_secret = client_secret
+
+    async def perform_token_rotation(
+        self,
+        *,
+        installation: Installation,
+        minutes_before_expiration: int = 120,  # 2 hours by default
+    ) -> Optional[Installation]:
+        """Performs token rotation if the underlying tokens (bot / user) are expired / expiring.
+
+        Args:
+            installation: the current installation data
+            minutes_before_expiration: the minutes before the token expiration
+
+        Returns:
+            None if no rotation is necessary for now.
+        """
+
+        # TODO: make the following two calls in parallel for better performance
+
+        # bot
+        rotated_bot: Optional[Bot] = await self.perform_bot_token_rotation(
+            bot=installation.to_bot(),
+            minutes_before_expiration=minutes_before_expiration,
+        )
+
+        # user
+        rotated_installation = await self.perform_user_token_rotation(
+            installation=installation,
+            minutes_before_expiration=minutes_before_expiration,
+        )
+
+        if rotated_bot is not None:
+            if rotated_installation is None:
+                rotated_installation = Installation(**installation.to_dict_for_copying())
+            rotated_installation.bot_token = rotated_bot.bot_token
+            rotated_installation.bot_refresh_token = rotated_bot.bot_refresh_token
+            rotated_installation.bot_token_expires_at = rotated_bot.bot_token_expires_at
+
+        return rotated_installation
+
+    async def perform_bot_token_rotation(
+        self,
+        *,
+        bot: Bot,
+        minutes_before_expiration: int = 120,  # 2 hours by default
+    ) -> Optional[Bot]:
+        """Performs bot token rotation if the underlying bot token is expired / expiring.
+
+        Args:
+            bot: the current bot installation data
+            minutes_before_expiration: the minutes before the token expiration
+
+        Returns:
+            None if no rotation is necessary for now.
+        """
+        if bot.bot_token_expires_at is None:
+            return None
+        if bot.bot_token_expires_at > time() + minutes_before_expiration * 60:
+            return None
+
+        try:
+            refresh_response = await self.client.oauth_v2_access(
+                client_id=self.client_id,
+                client_secret=self.client_secret,
+                grant_type="refresh_token",
+                refresh_token=bot.bot_refresh_token,
+            )
+            # TODO: error handling
+
+            if refresh_response.get("token_type") != "bot":
+                return None
+
+            refreshed_bot = Bot(**bot.to_dict_for_copying())
+            refreshed_bot.bot_token = refresh_response["access_token"]
+            refreshed_bot.bot_refresh_token = refresh_response.get("refresh_token")
+            refreshed_bot.bot_token_expires_at = int(time()) + int(refresh_response["expires_in"])
+            return refreshed_bot
+
+        except SlackApiError as e:
+            raise SlackTokenRotationError(e)
+
+    async def perform_user_token_rotation(
+        self,
+        *,
+        installation: Installation,
+        minutes_before_expiration: int = 120,  # 2 hours by default
+    ) -> Optional[Installation]:
+        """Performs user token rotation if the underlying user token is expired / expiring.
+
+        Args:
+            installation: the current installation data
+            minutes_before_expiration: the minutes before the token expiration
+
+        Returns:
+            None if no rotation is necessary for now.
+        """
+        if installation.user_token_expires_at is None:
+            return None
+        if installation.user_token_expires_at > time() + minutes_before_expiration * 60:
+            return None
+
+        try:
+            refresh_response = await self.client.oauth_v2_access(
+                client_id=self.client_id,
+                client_secret=self.client_secret,
+                grant_type="refresh_token",
+                refresh_token=installation.user_refresh_token,
+            )
+            if refresh_response.get("token_type") != "user":
+                return None
+
+            refreshed_installation = Installation(**installation.to_dict_for_copying())
+            refreshed_installation.user_token = refresh_response.get("access_token")
+            refreshed_installation.user_refresh_token = refresh_response.get("refresh_token")
+            refreshed_installation.user_token_expires_at = int(time()) + int(refresh_response.get("expires_in"))  # type: ignore[arg-type] # noqa: E501
+            return refreshed_installation
+
+        except SlackApiError as e:
+            raise SlackTokenRotationError(e)
+
+
+

Class variables

+
+
var clientAsyncWebClient
+
+

The type of the None singleton.

+
+
var client_id : str
+
+

The type of the None singleton.

+
+
var client_secret : str
+
+

The type of the None singleton.

+
+
+

Methods

+
+
+async def perform_bot_token_rotation(self,
*,
bot: Bot,
minutes_before_expiration: int = 120) ‑> Bot | None
+
+
+
+ +Expand source code + +
async def perform_bot_token_rotation(
+    self,
+    *,
+    bot: Bot,
+    minutes_before_expiration: int = 120,  # 2 hours by default
+) -> Optional[Bot]:
+    """Performs bot token rotation if the underlying bot token is expired / expiring.
+
+    Args:
+        bot: the current bot installation data
+        minutes_before_expiration: the minutes before the token expiration
+
+    Returns:
+        None if no rotation is necessary for now.
+    """
+    if bot.bot_token_expires_at is None:
+        return None
+    if bot.bot_token_expires_at > time() + minutes_before_expiration * 60:
+        return None
+
+    try:
+        refresh_response = await self.client.oauth_v2_access(
+            client_id=self.client_id,
+            client_secret=self.client_secret,
+            grant_type="refresh_token",
+            refresh_token=bot.bot_refresh_token,
+        )
+        # TODO: error handling
+
+        if refresh_response.get("token_type") != "bot":
+            return None
+
+        refreshed_bot = Bot(**bot.to_dict_for_copying())
+        refreshed_bot.bot_token = refresh_response["access_token"]
+        refreshed_bot.bot_refresh_token = refresh_response.get("refresh_token")
+        refreshed_bot.bot_token_expires_at = int(time()) + int(refresh_response["expires_in"])
+        return refreshed_bot
+
+    except SlackApiError as e:
+        raise SlackTokenRotationError(e)
+
+

Performs bot token rotation if the underlying bot token is expired / expiring.

+

Args

+
+
bot
+
the current bot installation data
+
minutes_before_expiration
+
the minutes before the token expiration
+
+

Returns

+

None if no rotation is necessary for now.

+
+
+async def perform_token_rotation(self,
*,
installation: Installation,
minutes_before_expiration: int = 120) ‑> Installation | None
+
+
+
+ +Expand source code + +
async def perform_token_rotation(
+    self,
+    *,
+    installation: Installation,
+    minutes_before_expiration: int = 120,  # 2 hours by default
+) -> Optional[Installation]:
+    """Performs token rotation if the underlying tokens (bot / user) are expired / expiring.
+
+    Args:
+        installation: the current installation data
+        minutes_before_expiration: the minutes before the token expiration
+
+    Returns:
+        None if no rotation is necessary for now.
+    """
+
+    # TODO: make the following two calls in parallel for better performance
+
+    # bot
+    rotated_bot: Optional[Bot] = await self.perform_bot_token_rotation(
+        bot=installation.to_bot(),
+        minutes_before_expiration=minutes_before_expiration,
+    )
+
+    # user
+    rotated_installation = await self.perform_user_token_rotation(
+        installation=installation,
+        minutes_before_expiration=minutes_before_expiration,
+    )
+
+    if rotated_bot is not None:
+        if rotated_installation is None:
+            rotated_installation = Installation(**installation.to_dict_for_copying())
+        rotated_installation.bot_token = rotated_bot.bot_token
+        rotated_installation.bot_refresh_token = rotated_bot.bot_refresh_token
+        rotated_installation.bot_token_expires_at = rotated_bot.bot_token_expires_at
+
+    return rotated_installation
+
+

Performs token rotation if the underlying tokens (bot / user) are expired / expiring.

+

Args

+
+
installation
+
the current installation data
+
minutes_before_expiration
+
the minutes before the token expiration
+
+

Returns

+

None if no rotation is necessary for now.

+
+
+async def perform_user_token_rotation(self,
*,
installation: Installation,
minutes_before_expiration: int = 120) ‑> Installation | None
+
+
+
+ +Expand source code + +
async def perform_user_token_rotation(
+    self,
+    *,
+    installation: Installation,
+    minutes_before_expiration: int = 120,  # 2 hours by default
+) -> Optional[Installation]:
+    """Performs user token rotation if the underlying user token is expired / expiring.
+
+    Args:
+        installation: the current installation data
+        minutes_before_expiration: the minutes before the token expiration
+
+    Returns:
+        None if no rotation is necessary for now.
+    """
+    if installation.user_token_expires_at is None:
+        return None
+    if installation.user_token_expires_at > time() + minutes_before_expiration * 60:
+        return None
+
+    try:
+        refresh_response = await self.client.oauth_v2_access(
+            client_id=self.client_id,
+            client_secret=self.client_secret,
+            grant_type="refresh_token",
+            refresh_token=installation.user_refresh_token,
+        )
+        if refresh_response.get("token_type") != "user":
+            return None
+
+        refreshed_installation = Installation(**installation.to_dict_for_copying())
+        refreshed_installation.user_token = refresh_response.get("access_token")
+        refreshed_installation.user_refresh_token = refresh_response.get("refresh_token")
+        refreshed_installation.user_token_expires_at = int(time()) + int(refresh_response.get("expires_in"))  # type: ignore[arg-type] # noqa: E501
+        return refreshed_installation
+
+    except SlackApiError as e:
+        raise SlackTokenRotationError(e)
+
+

Performs user token rotation if the underlying user token is expired / expiring.

+

Args

+
+
installation
+
the current installation data
+
minutes_before_expiration
+
the minutes before the token expiration
+
+

Returns

+

None if no rotation is necessary for now.

+
+
+
+
+
+
+ +
+ + + diff --git a/docs/reference/oauth/token_rotation/index.html b/docs/reference/oauth/token_rotation/index.html new file mode 100644 index 000000000..979f9f80c --- /dev/null +++ b/docs/reference/oauth/token_rotation/index.html @@ -0,0 +1,433 @@ + + + + + + +slack_sdk.oauth.token_rotation API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.oauth.token_rotation

+
+
+
+
+

Sub-modules

+
+
slack_sdk.oauth.token_rotation.async_rotator
+
+
+
+
slack_sdk.oauth.token_rotation.rotator
+
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class TokenRotator +(*,
client_id: str,
client_secret: str,
client: WebClient | None = None)
+
+
+
+ +Expand source code + +
class TokenRotator:
+    client: WebClient
+    client_id: str
+    client_secret: str
+
+    def __init__(self, *, client_id: str, client_secret: str, client: Optional[WebClient] = None):
+        self.client = client if client is not None else WebClient(token=None)
+        self.client_id = client_id
+        self.client_secret = client_secret
+
+    def perform_token_rotation(
+        self,
+        *,
+        installation: Installation,
+        minutes_before_expiration: int = 120,  # 2 hours by default
+    ) -> Optional[Installation]:
+        """Performs token rotation if the underlying tokens (bot / user) are expired / expiring.
+
+        Args:
+            installation: the current installation data
+            minutes_before_expiration: the minutes before the token expiration
+
+        Returns:
+            None if no rotation is necessary for now.
+        """
+
+        # TODO: make the following two calls in parallel for better performance
+
+        # bot
+        rotated_bot: Optional[Bot] = self.perform_bot_token_rotation(
+            bot=installation.to_bot(),
+            minutes_before_expiration=minutes_before_expiration,
+        )
+
+        # user
+        rotated_installation: Optional[Installation] = self.perform_user_token_rotation(
+            installation=installation,
+            minutes_before_expiration=minutes_before_expiration,
+        )
+
+        if rotated_bot is not None:
+            if rotated_installation is None:
+                rotated_installation = Installation(**installation.to_dict_for_copying())
+            rotated_installation.bot_token = rotated_bot.bot_token
+            rotated_installation.bot_refresh_token = rotated_bot.bot_refresh_token
+            rotated_installation.bot_token_expires_at = rotated_bot.bot_token_expires_at
+
+        return rotated_installation
+
+    def perform_bot_token_rotation(
+        self,
+        *,
+        bot: Bot,
+        minutes_before_expiration: int = 120,  # 2 hours by default
+    ) -> Optional[Bot]:
+        """Performs bot token rotation if the underlying bot token is expired / expiring.
+
+        Args:
+            bot: the current bot installation data
+            minutes_before_expiration: the minutes before the token expiration
+
+        Returns:
+            None if no rotation is necessary for now.
+        """
+        if bot.bot_token_expires_at is None:
+            return None
+        if bot.bot_token_expires_at > time() + minutes_before_expiration * 60:
+            return None
+
+        try:
+            refresh_response = self.client.oauth_v2_access(
+                client_id=self.client_id,
+                client_secret=self.client_secret,
+                grant_type="refresh_token",
+                refresh_token=bot.bot_refresh_token,
+            )
+            if refresh_response.get("token_type") != "bot":
+                return None
+
+            refreshed_bot = Bot(**bot.to_dict_for_copying())
+            refreshed_bot.bot_token = refresh_response["access_token"]
+            refreshed_bot.bot_refresh_token = refresh_response.get("refresh_token")
+            refreshed_bot.bot_token_expires_at = int(time()) + int(refresh_response["expires_in"])
+            return refreshed_bot
+
+        except SlackApiError as e:
+            raise SlackTokenRotationError(e)
+
+    def perform_user_token_rotation(
+        self,
+        *,
+        installation: Installation,
+        minutes_before_expiration: int = 120,  # 2 hours by default
+    ) -> Optional[Installation]:
+        """Performs user token rotation if the underlying user token is expired / expiring.
+
+        Args:
+            installation: the current installation data
+            minutes_before_expiration: the minutes before the token expiration
+
+        Returns:
+            None if no rotation is necessary for now.
+        """
+        if installation.user_token_expires_at is None:
+            return None
+        if installation.user_token_expires_at > time() + minutes_before_expiration * 60:
+            return None
+
+        try:
+            refresh_response = self.client.oauth_v2_access(
+                client_id=self.client_id,
+                client_secret=self.client_secret,
+                grant_type="refresh_token",
+                refresh_token=installation.user_refresh_token,
+            )
+
+            if refresh_response.get("token_type") != "user":
+                return None
+
+            refreshed_installation = Installation(**installation.to_dict_for_copying())
+            refreshed_installation.user_token = refresh_response.get("access_token")
+            refreshed_installation.user_refresh_token = refresh_response.get("refresh_token")
+            refreshed_installation.user_token_expires_at = int(time()) + int(refresh_response["expires_in"])
+            return refreshed_installation
+
+        except SlackApiError as e:
+            raise SlackTokenRotationError(e)
+
+
+

Class variables

+
+
var clientWebClient
+
+

The type of the None singleton.

+
+
var client_id : str
+
+

The type of the None singleton.

+
+
var client_secret : str
+
+

The type of the None singleton.

+
+
+

Methods

+
+
+def perform_bot_token_rotation(self,
*,
bot: Bot,
minutes_before_expiration: int = 120) ‑> Bot | None
+
+
+
+ +Expand source code + +
def perform_bot_token_rotation(
+    self,
+    *,
+    bot: Bot,
+    minutes_before_expiration: int = 120,  # 2 hours by default
+) -> Optional[Bot]:
+    """Performs bot token rotation if the underlying bot token is expired / expiring.
+
+    Args:
+        bot: the current bot installation data
+        minutes_before_expiration: the minutes before the token expiration
+
+    Returns:
+        None if no rotation is necessary for now.
+    """
+    if bot.bot_token_expires_at is None:
+        return None
+    if bot.bot_token_expires_at > time() + minutes_before_expiration * 60:
+        return None
+
+    try:
+        refresh_response = self.client.oauth_v2_access(
+            client_id=self.client_id,
+            client_secret=self.client_secret,
+            grant_type="refresh_token",
+            refresh_token=bot.bot_refresh_token,
+        )
+        if refresh_response.get("token_type") != "bot":
+            return None
+
+        refreshed_bot = Bot(**bot.to_dict_for_copying())
+        refreshed_bot.bot_token = refresh_response["access_token"]
+        refreshed_bot.bot_refresh_token = refresh_response.get("refresh_token")
+        refreshed_bot.bot_token_expires_at = int(time()) + int(refresh_response["expires_in"])
+        return refreshed_bot
+
+    except SlackApiError as e:
+        raise SlackTokenRotationError(e)
+
+

Performs bot token rotation if the underlying bot token is expired / expiring.

+

Args

+
+
bot
+
the current bot installation data
+
minutes_before_expiration
+
the minutes before the token expiration
+
+

Returns

+

None if no rotation is necessary for now.

+
+
+def perform_token_rotation(self,
*,
installation: Installation,
minutes_before_expiration: int = 120) ‑> Installation | None
+
+
+
+ +Expand source code + +
def perform_token_rotation(
+    self,
+    *,
+    installation: Installation,
+    minutes_before_expiration: int = 120,  # 2 hours by default
+) -> Optional[Installation]:
+    """Performs token rotation if the underlying tokens (bot / user) are expired / expiring.
+
+    Args:
+        installation: the current installation data
+        minutes_before_expiration: the minutes before the token expiration
+
+    Returns:
+        None if no rotation is necessary for now.
+    """
+
+    # TODO: make the following two calls in parallel for better performance
+
+    # bot
+    rotated_bot: Optional[Bot] = self.perform_bot_token_rotation(
+        bot=installation.to_bot(),
+        minutes_before_expiration=minutes_before_expiration,
+    )
+
+    # user
+    rotated_installation: Optional[Installation] = self.perform_user_token_rotation(
+        installation=installation,
+        minutes_before_expiration=minutes_before_expiration,
+    )
+
+    if rotated_bot is not None:
+        if rotated_installation is None:
+            rotated_installation = Installation(**installation.to_dict_for_copying())
+        rotated_installation.bot_token = rotated_bot.bot_token
+        rotated_installation.bot_refresh_token = rotated_bot.bot_refresh_token
+        rotated_installation.bot_token_expires_at = rotated_bot.bot_token_expires_at
+
+    return rotated_installation
+
+

Performs token rotation if the underlying tokens (bot / user) are expired / expiring.

+

Args

+
+
installation
+
the current installation data
+
minutes_before_expiration
+
the minutes before the token expiration
+
+

Returns

+

None if no rotation is necessary for now.

+
+
+def perform_user_token_rotation(self,
*,
installation: Installation,
minutes_before_expiration: int = 120) ‑> Installation | None
+
+
+
+ +Expand source code + +
def perform_user_token_rotation(
+    self,
+    *,
+    installation: Installation,
+    minutes_before_expiration: int = 120,  # 2 hours by default
+) -> Optional[Installation]:
+    """Performs user token rotation if the underlying user token is expired / expiring.
+
+    Args:
+        installation: the current installation data
+        minutes_before_expiration: the minutes before the token expiration
+
+    Returns:
+        None if no rotation is necessary for now.
+    """
+    if installation.user_token_expires_at is None:
+        return None
+    if installation.user_token_expires_at > time() + minutes_before_expiration * 60:
+        return None
+
+    try:
+        refresh_response = self.client.oauth_v2_access(
+            client_id=self.client_id,
+            client_secret=self.client_secret,
+            grant_type="refresh_token",
+            refresh_token=installation.user_refresh_token,
+        )
+
+        if refresh_response.get("token_type") != "user":
+            return None
+
+        refreshed_installation = Installation(**installation.to_dict_for_copying())
+        refreshed_installation.user_token = refresh_response.get("access_token")
+        refreshed_installation.user_refresh_token = refresh_response.get("refresh_token")
+        refreshed_installation.user_token_expires_at = int(time()) + int(refresh_response["expires_in"])
+        return refreshed_installation
+
+    except SlackApiError as e:
+        raise SlackTokenRotationError(e)
+
+

Performs user token rotation if the underlying user token is expired / expiring.

+

Args

+
+
installation
+
the current installation data
+
minutes_before_expiration
+
the minutes before the token expiration
+
+

Returns

+

None if no rotation is necessary for now.

+
+
+
+
+
+
+ +
+ + + diff --git a/docs/reference/oauth/token_rotation/rotator.html b/docs/reference/oauth/token_rotation/rotator.html new file mode 100644 index 000000000..afcccb48c --- /dev/null +++ b/docs/reference/oauth/token_rotation/rotator.html @@ -0,0 +1,416 @@ + + + + + + +slack_sdk.oauth.token_rotation.rotator API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.oauth.token_rotation.rotator

+
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class TokenRotator +(*,
client_id: str,
client_secret: str,
client: WebClient | None = None)
+
+
+
+ +Expand source code + +
class TokenRotator:
+    client: WebClient
+    client_id: str
+    client_secret: str
+
+    def __init__(self, *, client_id: str, client_secret: str, client: Optional[WebClient] = None):
+        self.client = client if client is not None else WebClient(token=None)
+        self.client_id = client_id
+        self.client_secret = client_secret
+
+    def perform_token_rotation(
+        self,
+        *,
+        installation: Installation,
+        minutes_before_expiration: int = 120,  # 2 hours by default
+    ) -> Optional[Installation]:
+        """Performs token rotation if the underlying tokens (bot / user) are expired / expiring.
+
+        Args:
+            installation: the current installation data
+            minutes_before_expiration: the minutes before the token expiration
+
+        Returns:
+            None if no rotation is necessary for now.
+        """
+
+        # TODO: make the following two calls in parallel for better performance
+
+        # bot
+        rotated_bot: Optional[Bot] = self.perform_bot_token_rotation(
+            bot=installation.to_bot(),
+            minutes_before_expiration=minutes_before_expiration,
+        )
+
+        # user
+        rotated_installation: Optional[Installation] = self.perform_user_token_rotation(
+            installation=installation,
+            minutes_before_expiration=minutes_before_expiration,
+        )
+
+        if rotated_bot is not None:
+            if rotated_installation is None:
+                rotated_installation = Installation(**installation.to_dict_for_copying())
+            rotated_installation.bot_token = rotated_bot.bot_token
+            rotated_installation.bot_refresh_token = rotated_bot.bot_refresh_token
+            rotated_installation.bot_token_expires_at = rotated_bot.bot_token_expires_at
+
+        return rotated_installation
+
+    def perform_bot_token_rotation(
+        self,
+        *,
+        bot: Bot,
+        minutes_before_expiration: int = 120,  # 2 hours by default
+    ) -> Optional[Bot]:
+        """Performs bot token rotation if the underlying bot token is expired / expiring.
+
+        Args:
+            bot: the current bot installation data
+            minutes_before_expiration: the minutes before the token expiration
+
+        Returns:
+            None if no rotation is necessary for now.
+        """
+        if bot.bot_token_expires_at is None:
+            return None
+        if bot.bot_token_expires_at > time() + minutes_before_expiration * 60:
+            return None
+
+        try:
+            refresh_response = self.client.oauth_v2_access(
+                client_id=self.client_id,
+                client_secret=self.client_secret,
+                grant_type="refresh_token",
+                refresh_token=bot.bot_refresh_token,
+            )
+            if refresh_response.get("token_type") != "bot":
+                return None
+
+            refreshed_bot = Bot(**bot.to_dict_for_copying())
+            refreshed_bot.bot_token = refresh_response["access_token"]
+            refreshed_bot.bot_refresh_token = refresh_response.get("refresh_token")
+            refreshed_bot.bot_token_expires_at = int(time()) + int(refresh_response["expires_in"])
+            return refreshed_bot
+
+        except SlackApiError as e:
+            raise SlackTokenRotationError(e)
+
+    def perform_user_token_rotation(
+        self,
+        *,
+        installation: Installation,
+        minutes_before_expiration: int = 120,  # 2 hours by default
+    ) -> Optional[Installation]:
+        """Performs user token rotation if the underlying user token is expired / expiring.
+
+        Args:
+            installation: the current installation data
+            minutes_before_expiration: the minutes before the token expiration
+
+        Returns:
+            None if no rotation is necessary for now.
+        """
+        if installation.user_token_expires_at is None:
+            return None
+        if installation.user_token_expires_at > time() + minutes_before_expiration * 60:
+            return None
+
+        try:
+            refresh_response = self.client.oauth_v2_access(
+                client_id=self.client_id,
+                client_secret=self.client_secret,
+                grant_type="refresh_token",
+                refresh_token=installation.user_refresh_token,
+            )
+
+            if refresh_response.get("token_type") != "user":
+                return None
+
+            refreshed_installation = Installation(**installation.to_dict_for_copying())
+            refreshed_installation.user_token = refresh_response.get("access_token")
+            refreshed_installation.user_refresh_token = refresh_response.get("refresh_token")
+            refreshed_installation.user_token_expires_at = int(time()) + int(refresh_response["expires_in"])
+            return refreshed_installation
+
+        except SlackApiError as e:
+            raise SlackTokenRotationError(e)
+
+
+

Class variables

+
+
var clientWebClient
+
+

The type of the None singleton.

+
+
var client_id : str
+
+

The type of the None singleton.

+
+
var client_secret : str
+
+

The type of the None singleton.

+
+
+

Methods

+
+
+def perform_bot_token_rotation(self,
*,
bot: Bot,
minutes_before_expiration: int = 120) ‑> Bot | None
+
+
+
+ +Expand source code + +
def perform_bot_token_rotation(
+    self,
+    *,
+    bot: Bot,
+    minutes_before_expiration: int = 120,  # 2 hours by default
+) -> Optional[Bot]:
+    """Performs bot token rotation if the underlying bot token is expired / expiring.
+
+    Args:
+        bot: the current bot installation data
+        minutes_before_expiration: the minutes before the token expiration
+
+    Returns:
+        None if no rotation is necessary for now.
+    """
+    if bot.bot_token_expires_at is None:
+        return None
+    if bot.bot_token_expires_at > time() + minutes_before_expiration * 60:
+        return None
+
+    try:
+        refresh_response = self.client.oauth_v2_access(
+            client_id=self.client_id,
+            client_secret=self.client_secret,
+            grant_type="refresh_token",
+            refresh_token=bot.bot_refresh_token,
+        )
+        if refresh_response.get("token_type") != "bot":
+            return None
+
+        refreshed_bot = Bot(**bot.to_dict_for_copying())
+        refreshed_bot.bot_token = refresh_response["access_token"]
+        refreshed_bot.bot_refresh_token = refresh_response.get("refresh_token")
+        refreshed_bot.bot_token_expires_at = int(time()) + int(refresh_response["expires_in"])
+        return refreshed_bot
+
+    except SlackApiError as e:
+        raise SlackTokenRotationError(e)
+
+

Performs bot token rotation if the underlying bot token is expired / expiring.

+

Args

+
+
bot
+
the current bot installation data
+
minutes_before_expiration
+
the minutes before the token expiration
+
+

Returns

+

None if no rotation is necessary for now.

+
+
+def perform_token_rotation(self,
*,
installation: Installation,
minutes_before_expiration: int = 120) ‑> Installation | None
+
+
+
+ +Expand source code + +
def perform_token_rotation(
+    self,
+    *,
+    installation: Installation,
+    minutes_before_expiration: int = 120,  # 2 hours by default
+) -> Optional[Installation]:
+    """Performs token rotation if the underlying tokens (bot / user) are expired / expiring.
+
+    Args:
+        installation: the current installation data
+        minutes_before_expiration: the minutes before the token expiration
+
+    Returns:
+        None if no rotation is necessary for now.
+    """
+
+    # TODO: make the following two calls in parallel for better performance
+
+    # bot
+    rotated_bot: Optional[Bot] = self.perform_bot_token_rotation(
+        bot=installation.to_bot(),
+        minutes_before_expiration=minutes_before_expiration,
+    )
+
+    # user
+    rotated_installation: Optional[Installation] = self.perform_user_token_rotation(
+        installation=installation,
+        minutes_before_expiration=minutes_before_expiration,
+    )
+
+    if rotated_bot is not None:
+        if rotated_installation is None:
+            rotated_installation = Installation(**installation.to_dict_for_copying())
+        rotated_installation.bot_token = rotated_bot.bot_token
+        rotated_installation.bot_refresh_token = rotated_bot.bot_refresh_token
+        rotated_installation.bot_token_expires_at = rotated_bot.bot_token_expires_at
+
+    return rotated_installation
+
+

Performs token rotation if the underlying tokens (bot / user) are expired / expiring.

+

Args

+
+
installation
+
the current installation data
+
minutes_before_expiration
+
the minutes before the token expiration
+
+

Returns

+

None if no rotation is necessary for now.

+
+
+def perform_user_token_rotation(self,
*,
installation: Installation,
minutes_before_expiration: int = 120) ‑> Installation | None
+
+
+
+ +Expand source code + +
def perform_user_token_rotation(
+    self,
+    *,
+    installation: Installation,
+    minutes_before_expiration: int = 120,  # 2 hours by default
+) -> Optional[Installation]:
+    """Performs user token rotation if the underlying user token is expired / expiring.
+
+    Args:
+        installation: the current installation data
+        minutes_before_expiration: the minutes before the token expiration
+
+    Returns:
+        None if no rotation is necessary for now.
+    """
+    if installation.user_token_expires_at is None:
+        return None
+    if installation.user_token_expires_at > time() + minutes_before_expiration * 60:
+        return None
+
+    try:
+        refresh_response = self.client.oauth_v2_access(
+            client_id=self.client_id,
+            client_secret=self.client_secret,
+            grant_type="refresh_token",
+            refresh_token=installation.user_refresh_token,
+        )
+
+        if refresh_response.get("token_type") != "user":
+            return None
+
+        refreshed_installation = Installation(**installation.to_dict_for_copying())
+        refreshed_installation.user_token = refresh_response.get("access_token")
+        refreshed_installation.user_refresh_token = refresh_response.get("refresh_token")
+        refreshed_installation.user_token_expires_at = int(time()) + int(refresh_response["expires_in"])
+        return refreshed_installation
+
+    except SlackApiError as e:
+        raise SlackTokenRotationError(e)
+
+

Performs user token rotation if the underlying user token is expired / expiring.

+

Args

+
+
installation
+
the current installation data
+
minutes_before_expiration
+
the minutes before the token expiration
+
+

Returns

+

None if no rotation is necessary for now.

+
+
+
+
+
+
+ +
+ + + diff --git a/docs/reference/proxy_env_variable_loader.html b/docs/reference/proxy_env_variable_loader.html new file mode 100644 index 000000000..f1eee3af3 --- /dev/null +++ b/docs/reference/proxy_env_variable_loader.html @@ -0,0 +1,102 @@ + + + + + + +slack_sdk.proxy_env_variable_loader API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.proxy_env_variable_loader

+
+
+

Internal module for loading proxy-related env variables

+
+
+
+
+
+
+

Functions

+
+
+def load_http_proxy_from_env(logger: logging.Logger = <Logger slack_sdk.proxy_env_variable_loader (WARNING)>) ‑> str | None +
+
+
+ +Expand source code + +
def load_http_proxy_from_env(logger: logging.Logger = _default_logger) -> Optional[str]:
+    proxy_url = (
+        os.environ.get("HTTPS_PROXY")
+        or os.environ.get("https_proxy")
+        or os.environ.get("HTTP_PROXY")
+        or os.environ.get("http_proxy")
+    )
+    if proxy_url is None:
+        return None
+    if len(proxy_url.strip()) == 0:
+        # If the value is an empty string, the intention should be unsetting it
+        logger.debug("The Slack SDK ignored the proxy env variable as an empty value is set.")
+        return None
+
+    logger.debug(f"HTTP proxy URL has been loaded from an env variable: {proxy_url}")
+    return proxy_url
+
+
+
+
+
+
+
+
+ +
+ + + diff --git a/docs/reference/rtm/index.html b/docs/reference/rtm/index.html new file mode 100644 index 000000000..870fa3797 --- /dev/null +++ b/docs/reference/rtm/index.html @@ -0,0 +1,1004 @@ + + + + + + +slack_sdk.rtm API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.rtm

+
+
+

A Python module for interacting with Slack's RTM API.

+
+
+

Sub-modules

+
+
slack_sdk.rtm.v2
+
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class RTMClient +(*,
token: str,
run_async: bool | None = False,
auto_reconnect: bool | None = True,
ssl: ssl.SSLContext | None = None,
proxy: str | None = None,
timeout: int | None = 30,
base_url: str | None = 'https://slack.com/api/',
connect_method: str | None = None,
ping_interval: int | None = 30,
loop: asyncio.events.AbstractEventLoop | None = None,
headers: dict | None = {})
+
+
+
+ +Expand source code + +
class RTMClient(object):
+    """An RTMClient allows apps to communicate with the Slack Platform's RTM API.
+
+    The event-driven architecture of this client allows you to simply
+    link callbacks to their corresponding events. When an event occurs
+    this client executes your callback while passing along any
+    information it receives.
+
+    Attributes:
+        token (str): A string specifying an xoxp or xoxb token.
+        run_async (bool): A boolean specifying if the client should
+            be run in async mode. Default is False.
+        auto_reconnect (bool): When true the client will automatically
+            reconnect when (not manually) disconnected. Default is True.
+        ssl (SSLContext): To use SSL support, pass an SSLContext object here.
+            Default is None.
+        proxy (str): To use proxy support, pass the string of the proxy server.
+            e.g. "http://proxy.com"
+            Authentication credentials can be passed in proxy URL.
+            e.g. "http://user:pass@some.proxy.com"
+            Default is None.
+        timeout (int): The amount of seconds the session should wait before timing out.
+            Default is 30.
+        base_url (str): The base url for all HTTP requests.
+            Note: This is only used in the WebClient.
+            Default is "https://slack.com/api/".
+        connect_method (str): An string specifying if the client
+            will connect with `rtm.connect` or `rtm.start`.
+            Default is `rtm.connect`.
+        ping_interval (int): automatically send "ping" command every
+            specified period of seconds. If set to 0, do not send automatically.
+            Default is 30.
+        loop (AbstractEventLoop): An event loop provided by asyncio.
+            If None is specified we attempt to use the current loop
+            with `get_event_loop`. Default is None.
+
+    Methods:
+        ping: Sends a ping message over the websocket to Slack.
+        typing: Sends a typing indicator to the specified channel.
+        on: Stores and links callbacks to websocket and Slack events.
+        run_on: Decorator that stores and links callbacks to websocket and Slack events.
+        start: Starts an RTM Session with Slack.
+        stop: Closes the websocket connection and ensures it won't reconnect.
+
+    Example:
+    ```python
+    import os
+    from slack import RTMClient
+
+    @RTMClient.run_on(event="message")
+    def say_hello(**payload):
+        data = payload['data']
+        web_client = payload['web_client']
+        if 'Hello' in data['text']:
+            channel_id = data['channel']
+            thread_ts = data['ts']
+            user = data['user']
+
+            web_client.chat_postMessage(
+                channel=channel_id,
+                text=f"Hi <@{user}>!",
+                thread_ts=thread_ts
+            )
+
+    slack_token = os.environ["SLACK_API_TOKEN"]
+    rtm_client = RTMClient(token=slack_token)
+    rtm_client.start()
+    ```
+
+    Note:
+        The initial state returned when establishing an RTM connection will
+        be available as the data in payload for the 'open' event. This data is not and
+        will not be stored on the RTM Client.
+
+        Any attributes or methods prefixed with _underscores are
+        intended to be "private" internal use only. They may be changed or
+        removed at anytime.
+    """
+
+    _callbacks: DefaultDict = collections.defaultdict(list)
+
+    def __init__(
+        self,
+        *,
+        token: str,
+        run_async: Optional[bool] = False,
+        auto_reconnect: Optional[bool] = True,
+        ssl: Optional[SSLContext] = None,
+        proxy: Optional[str] = None,
+        timeout: Optional[int] = 30,
+        base_url: Optional[str] = WebClient.BASE_URL,
+        connect_method: Optional[str] = None,
+        ping_interval: Optional[int] = 30,
+        loop: Optional[asyncio.AbstractEventLoop] = None,
+        headers: Optional[dict] = {},
+    ):
+        self.token = token.strip()
+        self.run_async = run_async
+        self.auto_reconnect = auto_reconnect
+        self.ssl = ssl
+        self.proxy = proxy
+        self.timeout = timeout
+        self.base_url = base_url
+        self.connect_method = connect_method
+        self.ping_interval = ping_interval
+        self.headers = headers
+        self._event_loop = loop or asyncio.get_event_loop()
+        self._web_client = None
+        self._websocket = None
+        self._session = None
+        self._logger = logging.getLogger(__name__)
+        self._last_message_id = 0
+        self._connection_attempts = 0
+        self._stopped = False
+        self._web_client = WebClient(
+            token=self.token,
+            base_url=self.base_url,  # type: ignore[arg-type]
+            timeout=self.timeout,  # type: ignore[arg-type]
+            ssl=self.ssl,
+            proxy=self.proxy,
+            run_async=self.run_async,  # type: ignore[arg-type]
+            loop=self._event_loop,
+            session=self._session,
+            headers=self.headers,
+        )
+
+    @staticmethod
+    def run_on(*, event: str):
+        """A decorator to store and link a callback to an event."""
+
+        def decorator(callback):
+            RTMClient.on(event=event, callback=callback)
+            return callback
+
+        return decorator
+
+    @classmethod
+    def on(cls, *, event: str, callback: Callable):
+        """Stores and links the callback(s) to the event.
+
+        Args:
+            event (str): A string that specifies a Slack or websocket event.
+                e.g. 'channel_joined' or 'open'
+            callback (Callable): Any object or a list of objects that can be called.
+                e.g. <function say_hello at 0x101234567> or
+                [<function say_hello at 0x10123>,<function say_bye at 0x10456>]
+
+        Raises:
+            SlackClientError: The specified callback is not callable.
+            SlackClientError: The callback must accept keyword arguments (**kwargs).
+        """
+        if isinstance(callback, list):
+            for cb in callback:
+                cls._validate_callback(cb)
+            previous_callbacks = cls._callbacks[event]
+            cls._callbacks[event] = list(set(previous_callbacks + callback))
+        else:
+            cls._validate_callback(callback)
+            cls._callbacks[event].append(callback)
+
+    def start(self) -> Union[asyncio.Future, Any]:
+        """Starts an RTM Session with Slack.
+
+        Makes an authenticated call to Slack's RTM API to retrieve
+        a websocket URL and then connects to the message server.
+        As events stream-in we run any associated callbacks stored
+        on the client.
+
+        If 'auto_reconnect' is specified we
+        retrieve a new url and reconnect any time the connection
+        is lost unintentionally or an exception is thrown.
+
+        Raises:
+            SlackApiError: Unable to retrieve RTM URL from Slack.
+        """
+        # Not yet implemented: Add Windows support for graceful shutdowns.
+        if os.name != "nt" and current_thread() == main_thread():
+            signals = (signal.SIGHUP, signal.SIGTERM, signal.SIGINT)
+            for s in signals:
+                self._event_loop.add_signal_handler(s, self.stop)
+
+        future: Future[Any] = asyncio.ensure_future(self._connect_and_read(), loop=self._event_loop)
+
+        if self.run_async:
+            return future
+        return self._event_loop.run_until_complete(future)
+
+    def stop(self):
+        """Closes the websocket connection and ensures it won't reconnect.
+
+        If your application outputs the following errors,
+        call #async_stop() instead and await for the completion on your application side.
+
+        asyncio/base_events.py:641: RuntimeWarning:
+          coroutine 'ClientWebSocketResponse.close' was never awaited self._ready.clear()
+        """
+        self._logger.debug("The Slack RTMClient is shutting down.")
+        self._stopped = True
+        self._close_websocket()
+
+    async def async_stop(self):
+        """Closes the websocket connection and ensures it won't reconnect."""
+        self._logger.debug("The Slack RTMClient is shutting down.")
+        remaining_futures = self._close_websocket()
+        for future in remaining_futures:
+            await future
+        self._stopped = True
+
+    def send_over_websocket(self, *, payload: dict):
+        """Sends a message to Slack over the WebSocket connection.
+
+        Note:
+            The RTM API only supports posting simple messages formatted using
+            our default message formatting mode. It does not support
+            attachments or other message formatting modes. For this reason
+            we recommend users send messages via the Web API methods.
+            e.g. web_client.chat_postMessage()
+
+            If the message "id" is not specified in the payload, it'll be added.
+
+        Args:
+            payload (dict): The message to send over the wesocket.
+            e.g.
+            {
+                "id": 1,
+                "type": "typing",
+                "channel": "C024BE91L"
+            }
+
+        Raises:
+            SlackClientNotConnectedError: Websocket connection is closed.
+        """
+        return asyncio.ensure_future(self._send_json(payload), loop=self._event_loop)
+
+    async def _send_json(self, payload):
+        if self._websocket is None or self._event_loop is None:
+            raise client_err.SlackClientNotConnectedError("Websocket connection is closed.")
+        if "id" not in payload:
+            payload["id"] = self._next_msg_id()
+
+        return await self._websocket.send_json(payload)
+
+    async def ping(self):
+        """Sends a ping message over the websocket to Slack.
+
+        Not all web browsers support the WebSocket ping spec,
+        so the RTM protocol also supports ping/pong messages.
+
+        Raises:
+            SlackClientNotConnectedError: Websocket connection is closed.
+        """
+        payload = {"id": self._next_msg_id(), "type": "ping"}
+        await self._send_json(payload=payload)
+
+    async def typing(self, *, channel: str):
+        """Sends a typing indicator to the specified channel.
+
+        This indicates that this app is currently
+        writing a message to send to a channel.
+
+        Args:
+            channel (str): The channel id. e.g. 'C024BE91L'
+
+        Raises:
+            SlackClientNotConnectedError: Websocket connection is closed.
+        """
+        payload = {"id": self._next_msg_id(), "type": "typing", "channel": channel}
+        await self._send_json(payload=payload)
+
+    @staticmethod
+    def _validate_callback(callback):
+        """Checks if the specified callback is callable and accepts a kwargs param.
+
+        Args:
+            callback (obj): Any object or a list of objects that can be called.
+                e.g. <function say_hello at 0x101234567>
+
+        Raises:
+            SlackClientError: The specified callback is not callable.
+            SlackClientError: The callback must accept keyword arguments (**kwargs).
+        """
+
+        cb_name = callback.__name__ if hasattr(callback, "__name__") else callback
+        if not callable(callback):
+            msg = "The specified callback '{}' is not callable.".format(cb_name)
+            raise client_err.SlackClientError(msg)
+        callback_params = inspect.signature(callback).parameters.values()
+        if not any(param for param in callback_params if param.kind == param.VAR_KEYWORD):
+            msg = "The callback '{}' must accept keyword arguments (**kwargs).".format(cb_name)
+            raise client_err.SlackClientError(msg)
+
+    def _next_msg_id(self):
+        """Retrieves the next message id.
+
+        When sending messages to Slack every event should
+        have a unique (for that connection) positive integer ID.
+
+        Returns:
+            An integer representing the message id. e.g. 98
+        """
+        self._last_message_id += 1
+        return self._last_message_id
+
+    async def _connect_and_read(self):
+        """Retrieves the WS url and connects to Slack's RTM API.
+
+        Makes an authenticated call to Slack's Web API to retrieve
+        a websocket URL. Then connects to the message server and
+        reads event messages as they come in.
+
+        If 'auto_reconnect' is specified we
+        retrieve a new url and reconnect any time the connection
+        is lost unintentionally or an exception is thrown.
+
+        Raises:
+            SlackApiError: Unable to retrieve RTM URL from Slack.
+            websockets.exceptions: Errors thrown by the 'websockets' library.
+        """
+        while not self._stopped:
+            try:
+                self._connection_attempts += 1
+                async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=self.timeout)) as session:
+                    self._session = session
+                    url, data = await self._retrieve_websocket_info()
+                    async with session.ws_connect(
+                        url,
+                        heartbeat=self.ping_interval,
+                        ssl=self.ssl,
+                        proxy=self.proxy,
+                    ) as websocket:
+                        self._logger.debug("The Websocket connection has been opened.")
+                        self._websocket = websocket
+                        await self._dispatch_event(event="open", data=data)
+                        await self._read_messages()
+                        # The websocket has been disconnected, or self._stopped is True
+                        if not self._stopped and not self.auto_reconnect:
+                            self._logger.warning("Not reconnecting the Websocket because auto_reconnect is False")
+                            return
+                        # No need to wait exponentially here, since the connection was
+                        # established OK, but timed out, or was closed remotely
+            except (
+                client_err.SlackClientNotConnectedError,
+                client_err.SlackApiError,
+                # Not yet implemented: Catch websocket exceptions thrown by aiohttp.
+            ) as exception:
+                await self._dispatch_event(event="error", data=exception)
+                error_code = exception.response.get("error", None) if hasattr(exception, "response") else None
+                if (
+                    self.auto_reconnect
+                    and not self._stopped
+                    and error_code != "invalid_auth"  # "invalid_auth" is unrecoverable
+                ):
+                    await self._wait_exponentially(exception)
+                    continue
+                self._logger.exception("The Websocket encountered an error. Closing the connection...")
+                self._close_websocket()
+                raise
+
+    async def _read_messages(self):
+        """Process messages received on the WebSocket connection."""
+        while not self._stopped and self._websocket is not None:
+            try:
+                # Wait for a message to be received, but timeout after a second so that
+                # we can check if the socket has been closed, or if self._stopped is
+                # True
+                message = await self._websocket.receive(timeout=1)
+            except asyncio.TimeoutError:
+                if not self._websocket.closed:
+                    # We didn't receive a message within the timeout interval, but
+                    # aiohttp hasn't closed the socket, so ping responses must still be
+                    # returning
+                    continue
+                self._logger.warning(
+                    "Websocket was closed (%s).",
+                    self._websocket.close_code if self._websocket else "",
+                )
+                await self._dispatch_event(
+                    event="error",
+                    data=self._websocket.exception() if self._websocket else "",
+                )
+                self._websocket = None
+                await self._dispatch_event(event="close")
+                return
+
+            if message.type == aiohttp.WSMsgType.TEXT:
+                try:
+                    payload = message.json()
+                    event = payload.pop("type", "Unknown")
+                    await self._dispatch_event(event, data=payload)
+                except Exception as err:
+                    data = message.data if message else message
+                    self._logger.info(f"Caught a raised exception ({err}) while dispatching a TEXT message ({data})")
+                    # Raised exceptions here happen in users' code and were just unhandled.
+                    # As they're not intended for closing current WebSocket connection,
+                    # this exception should not be propagated to higher level (#_connect_and_read()).
+                    continue
+            elif message.type == aiohttp.WSMsgType.ERROR:
+                self._logger.error("Received an error on the websocket: %r", message)
+                await self._dispatch_event(event="error", data=message)
+            elif message.type in (
+                aiohttp.WSMsgType.CLOSE,
+                aiohttp.WSMsgType.CLOSING,
+                aiohttp.WSMsgType.CLOSED,
+            ):
+                self._logger.warning("Websocket was closed.")
+                self._websocket = None
+                await self._dispatch_event(event="close")
+            else:
+                self._logger.debug("Received unhandled message type: %r", message)
+
+    async def _dispatch_event(self, event, data=None):
+        """Dispatches the event and executes any associated callbacks.
+
+        Note: To prevent the app from crashing due to callback errors. We
+        catch all exceptions and send all data to the logger.
+
+        Args:
+            event (str): The type of event. e.g. 'bot_added'
+            data (dict): The data Slack sent. e.g.
+            {
+                "type": "bot_added",
+                "bot": {
+                    "id": "B024BE7LH",
+                    "app_id": "A4H1JB4AZ",
+                    "name": "hugbot"
+                }
+            }
+        """
+        if self._logger.level <= logging.DEBUG:
+            self._logger.debug("Received an event: '%s' - %s", event, data)
+        for callback in self._callbacks[event]:
+            self._logger.debug(
+                "Running %s callbacks for event: '%s'",
+                len(self._callbacks[event]),
+                event,
+            )
+            try:
+                if self._stopped and event not in ["close", "error"]:
+                    # Don't run callbacks if client was stopped unless they're
+                    # close/error callbacks.
+                    break
+
+                if inspect.iscoroutinefunction(callback):
+                    await callback(rtm_client=self, web_client=self._web_client, data=data)
+                else:
+                    if self.run_async is True:
+                        raise client_err.SlackRequestError(
+                            f'The callback "{callback.__name__}" is NOT a coroutine. '
+                            "Running such with run_async=True is unsupported. "
+                            "Consider adding async/await to the method "
+                            "or going with run_async=False if your app is not really non-blocking."
+                        )
+                    payload = {
+                        "rtm_client": self,
+                        "web_client": self._web_client,
+                        "data": data,
+                    }
+                    callback(**payload)
+            except Exception as err:
+                name = callback.__name__
+                module = callback.__module__
+                msg = f"When calling '#{name}()' in the '{module}' module the following error was raised: {err}"
+                self._logger.error(msg)
+                raise
+
+    async def _retrieve_websocket_info(self):
+        """Retrieves the WebSocket info from Slack.
+
+        Returns:
+            A tuple of websocket information.
+            e.g.
+            (
+                "wss://...",
+                {
+                    "self": {"id": "U01234ABC","name": "robotoverlord"},
+                    "team": {
+                        "domain": "exampledomain",
+                        "id": "T123450FP",
+                        "name": "ExampleName"
+                    }
+                }
+            )
+
+        Raises:
+            SlackApiError: Unable to retrieve RTM URL from Slack.
+        """
+        if self._web_client is None:
+            self._web_client = WebClient(
+                token=self.token,
+                base_url=self.base_url,
+                timeout=self.timeout,
+                ssl=self.ssl,
+                proxy=self.proxy,
+                run_async=True,
+                loop=self._event_loop,
+                session=self._session,
+                headers=self.headers,
+            )
+        self._logger.debug("Retrieving websocket info.")
+        use_rtm_start = self.connect_method in ["rtm.start", "rtm_start"]
+        if self.run_async:
+            if use_rtm_start:
+                resp = await self._web_client.rtm_start()
+            else:
+                resp = await self._web_client.rtm_connect()
+        else:
+            if use_rtm_start:
+                resp = self._web_client.rtm_start()
+            else:
+                resp = self._web_client.rtm_connect()
+
+        url = resp.get("url")
+        if url is None:
+            msg = "Unable to retrieve RTM URL from Slack."
+            raise client_err.SlackApiError(message=msg, response=resp)
+        return url, resp.data
+
+    async def _wait_exponentially(self, exception, max_wait_time=300):
+        """Wait exponentially longer for each connection attempt.
+
+        Calculate the number of seconds to wait and then add
+        a random number of milliseconds to avoid coincidental
+        synchronized client retries. Wait up to the maximum amount
+        of wait time specified via 'max_wait_time'. However,
+        if Slack returned how long to wait use that.
+        """
+        if hasattr(exception, "response"):
+            wait_time = exception.response.get("headers", {}).get(
+                "Retry-After",
+                min((2**self._connection_attempts) + random.random(), max_wait_time),
+            )
+            self._logger.debug("Waiting %s seconds before reconnecting.", wait_time)
+            await asyncio.sleep(float(wait_time))
+
+    def _close_websocket(self) -> Sequence[Future]:
+        """Closes the websocket connection."""
+        futures = []
+        close_method = getattr(self._websocket, "close", None)
+        if callable(close_method):
+            future = asyncio.ensure_future(close_method(), loop=self._event_loop)
+            futures.append(future)
+        self._websocket = None
+        event_f = asyncio.ensure_future(self._dispatch_event(event="close"), loop=self._event_loop)
+        futures.append(event_f)
+        return futures
+
+

An RTMClient allows apps to communicate with the Slack Platform's RTM API.

+

The event-driven architecture of this client allows you to simply +link callbacks to their corresponding events. When an event occurs +this client executes your callback while passing along any +information it receives.

+

Attributes

+
+
token : str
+
A string specifying an xoxp or xoxb token.
+
run_async : bool
+
A boolean specifying if the client should +be run in async mode. Default is False.
+
auto_reconnect : bool
+
When true the client will automatically +reconnect when (not manually) disconnected. Default is True.
+
ssl : SSLContext
+
To use SSL support, pass an SSLContext object here. +Default is None.
+
proxy : str
+
To use proxy support, pass the string of the proxy server. +e.g. "http://proxy.com" +Authentication credentials can be passed in proxy URL. +e.g. "http://user:pass@some.proxy.com" +Default is None.
+
timeout : int
+
The amount of seconds the session should wait before timing out. +Default is 30.
+
base_url : str
+
The base url for all HTTP requests. +Note: This is only used in the WebClient. +Default is "https://slack.com/api/".
+
connect_method : str
+
An string specifying if the client +will connect with rtm.connect or rtm.start. +Default is rtm.connect.
+
ping_interval : int
+
automatically send "ping" command every +specified period of seconds. If set to 0, do not send automatically. +Default is 30.
+
loop : AbstractEventLoop
+
An event loop provided by asyncio. +If None is specified we attempt to use the current loop +with get_event_loop. Default is None.
+
+

Methods

+

ping: Sends a ping message over the websocket to Slack. +typing: Sends a typing indicator to the specified channel. +on: Stores and links callbacks to websocket and Slack events. +run_on: Decorator that stores and links callbacks to websocket and Slack events. +start: Starts an RTM Session with Slack. +stop: Closes the websocket connection and ensures it won't reconnect.

+

Example:

+
import os
+from slack import RTMClient
+
+@RTMClient.run_on(event="message")
+def say_hello(**payload):
+    data = payload['data']
+    web_client = payload['web_client']
+    if 'Hello' in data['text']:
+        channel_id = data['channel']
+        thread_ts = data['ts']
+        user = data['user']
+
+        web_client.chat_postMessage(
+            channel=channel_id,
+            text=f"Hi <@{user}>!",
+            thread_ts=thread_ts
+        )
+
+slack_token = os.environ["SLACK_API_TOKEN"]
+rtm_client = RTMClient(token=slack_token)
+rtm_client.start()
+
+

Note

+

The initial state returned when establishing an RTM connection will +be available as the data in payload for the 'open' event. This data is not and +will not be stored on the RTM Client.

+

Any attributes or methods prefixed with _underscores are +intended to be "private" internal use only. They may be changed or +removed at anytime.

+

Static methods

+
+
+def on(*, event: str, callback: Callable) +
+
+

Stores and links the callback(s) to the event.

+

Args

+
+
event : str
+
A string that specifies a Slack or websocket event. +e.g. 'channel_joined' or 'open'
+
callback : Callable
+
Any object or a list of objects that can be called. +e.g. or +[,]
+
+

Raises

+
+
SlackClientError
+
The specified callback is not callable.
+
SlackClientError
+
The callback must accept keyword arguments (**kwargs).
+
+
+
+def run_on(*, event: str) +
+
+
+ +Expand source code + +
@staticmethod
+def run_on(*, event: str):
+    """A decorator to store and link a callback to an event."""
+
+    def decorator(callback):
+        RTMClient.on(event=event, callback=callback)
+        return callback
+
+    return decorator
+
+

A decorator to store and link a callback to an event.

+
+
+

Methods

+
+
+async def async_stop(self) +
+
+
+ +Expand source code + +
async def async_stop(self):
+    """Closes the websocket connection and ensures it won't reconnect."""
+    self._logger.debug("The Slack RTMClient is shutting down.")
+    remaining_futures = self._close_websocket()
+    for future in remaining_futures:
+        await future
+    self._stopped = True
+
+

Closes the websocket connection and ensures it won't reconnect.

+
+
+async def ping(self) +
+
+
+ +Expand source code + +
async def ping(self):
+    """Sends a ping message over the websocket to Slack.
+
+    Not all web browsers support the WebSocket ping spec,
+    so the RTM protocol also supports ping/pong messages.
+
+    Raises:
+        SlackClientNotConnectedError: Websocket connection is closed.
+    """
+    payload = {"id": self._next_msg_id(), "type": "ping"}
+    await self._send_json(payload=payload)
+
+

Sends a ping message over the websocket to Slack.

+

Not all web browsers support the WebSocket ping spec, +so the RTM protocol also supports ping/pong messages.

+

Raises

+
+
SlackClientNotConnectedError
+
Websocket connection is closed.
+
+
+
+def send_over_websocket(self, *, payload: dict) +
+
+
+ +Expand source code + +
def send_over_websocket(self, *, payload: dict):
+    """Sends a message to Slack over the WebSocket connection.
+
+    Note:
+        The RTM API only supports posting simple messages formatted using
+        our default message formatting mode. It does not support
+        attachments or other message formatting modes. For this reason
+        we recommend users send messages via the Web API methods.
+        e.g. web_client.chat_postMessage()
+
+        If the message "id" is not specified in the payload, it'll be added.
+
+    Args:
+        payload (dict): The message to send over the wesocket.
+        e.g.
+        {
+            "id": 1,
+            "type": "typing",
+            "channel": "C024BE91L"
+        }
+
+    Raises:
+        SlackClientNotConnectedError: Websocket connection is closed.
+    """
+    return asyncio.ensure_future(self._send_json(payload), loop=self._event_loop)
+
+

Sends a message to Slack over the WebSocket connection.

+

Note

+

The RTM API only supports posting simple messages formatted using +our default message formatting mode. It does not support +attachments or other message formatting modes. For this reason +we recommend users send messages via the Web API methods. +e.g. web_client.chat_postMessage()

+

If the message "id" is not specified in the payload, it'll be added.

+

Args

+
+
payload : dict
+
The message to send over the wesocket.
+
+

e.g. +{ +"id": 1, +"type": "typing", +"channel": "C024BE91L" +}

+

Raises

+
+
SlackClientNotConnectedError
+
Websocket connection is closed.
+
+
+
+def start(self) ‑> _asyncio.Future | Any +
+
+
+ +Expand source code + +
def start(self) -> Union[asyncio.Future, Any]:
+    """Starts an RTM Session with Slack.
+
+    Makes an authenticated call to Slack's RTM API to retrieve
+    a websocket URL and then connects to the message server.
+    As events stream-in we run any associated callbacks stored
+    on the client.
+
+    If 'auto_reconnect' is specified we
+    retrieve a new url and reconnect any time the connection
+    is lost unintentionally or an exception is thrown.
+
+    Raises:
+        SlackApiError: Unable to retrieve RTM URL from Slack.
+    """
+    # Not yet implemented: Add Windows support for graceful shutdowns.
+    if os.name != "nt" and current_thread() == main_thread():
+        signals = (signal.SIGHUP, signal.SIGTERM, signal.SIGINT)
+        for s in signals:
+            self._event_loop.add_signal_handler(s, self.stop)
+
+    future: Future[Any] = asyncio.ensure_future(self._connect_and_read(), loop=self._event_loop)
+
+    if self.run_async:
+        return future
+    return self._event_loop.run_until_complete(future)
+
+

Starts an RTM Session with Slack.

+

Makes an authenticated call to Slack's RTM API to retrieve +a websocket URL and then connects to the message server. +As events stream-in we run any associated callbacks stored +on the client.

+

If 'auto_reconnect' is specified we +retrieve a new url and reconnect any time the connection +is lost unintentionally or an exception is thrown.

+

Raises

+
+
SlackApiError
+
Unable to retrieve RTM URL from Slack.
+
+
+
+def stop(self) +
+
+
+ +Expand source code + +
def stop(self):
+    """Closes the websocket connection and ensures it won't reconnect.
+
+    If your application outputs the following errors,
+    call #async_stop() instead and await for the completion on your application side.
+
+    asyncio/base_events.py:641: RuntimeWarning:
+      coroutine 'ClientWebSocketResponse.close' was never awaited self._ready.clear()
+    """
+    self._logger.debug("The Slack RTMClient is shutting down.")
+    self._stopped = True
+    self._close_websocket()
+
+

Closes the websocket connection and ensures it won't reconnect.

+

If your application outputs the following errors, +call #async_stop() instead and await for the completion on your application side.

+

asyncio/base_events.py:641: RuntimeWarning: +coroutine 'ClientWebSocketResponse.close' was never awaited self._ready.clear()

+
+
+async def typing(self, *, channel: str) +
+
+
+ +Expand source code + +
async def typing(self, *, channel: str):
+    """Sends a typing indicator to the specified channel.
+
+    This indicates that this app is currently
+    writing a message to send to a channel.
+
+    Args:
+        channel (str): The channel id. e.g. 'C024BE91L'
+
+    Raises:
+        SlackClientNotConnectedError: Websocket connection is closed.
+    """
+    payload = {"id": self._next_msg_id(), "type": "typing", "channel": channel}
+    await self._send_json(payload=payload)
+
+

Sends a typing indicator to the specified channel.

+

This indicates that this app is currently +writing a message to send to a channel.

+

Args

+
+
channel : str
+
The channel id. e.g. 'C024BE91L'
+
+

Raises

+
+
SlackClientNotConnectedError
+
Websocket connection is closed.
+
+
+
+
+
+
+
+ +
+ + + diff --git a/docs/reference/rtm/v2/index.html b/docs/reference/rtm/v2/index.html new file mode 100644 index 000000000..d0c0591ec --- /dev/null +++ b/docs/reference/rtm/v2/index.html @@ -0,0 +1,990 @@ + + + + + + +slack_sdk.rtm.v2 API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.rtm.v2

+
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class RTMClient +(*,
token: str | None = None,
web_client: WebClient | None = None,
auto_reconnect_enabled: bool = True,
ssl: ssl.SSLContext | None = None,
proxy: str | None = None,
timeout: int = 30,
base_url: str = 'https://slack.com/api/',
headers: dict | None = None,
ping_interval: int = 5,
concurrency: int = 10,
logger: logging.Logger | None = None,
on_message_listeners: List[Callable[[str], None]] | None = None,
on_error_listeners: List[Callable[[Exception], None]] | None = None,
on_close_listeners: List[Callable[[int, str | None], None]] | None = None,
trace_enabled: bool = False,
all_message_trace_enabled: bool = False,
ping_pong_trace_enabled: bool = False)
+
+
+
+ +Expand source code + +
class RTMClient:
+    token: Optional[str]
+    bot_id: Optional[str]
+    default_auto_reconnect_enabled: bool
+    auto_reconnect_enabled: bool
+    ssl: Optional[SSLContext]
+    proxy: Optional[str]
+    timeout: int
+    base_url: str
+    ping_interval: int
+    logger: Logger
+    web_client: WebClient
+
+    current_session: Optional[Connection]
+    current_session_state: Optional[ConnectionState]
+    wss_uri: Optional[str]
+
+    message_queue: Queue
+    message_listeners: List[Callable[["RTMClient", dict], None]]
+    message_processor: IntervalRunner
+    message_workers: ThreadPoolExecutor
+
+    closed: bool
+    connect_operation_lock: Lock
+
+    on_message_listeners: List[Callable[[str], None]]
+    on_error_listeners: List[Callable[[Exception], None]]
+    on_close_listeners: List[Callable[[int, Optional[str]], None]]
+
+    def __init__(
+        self,
+        *,
+        token: Optional[str] = None,
+        web_client: Optional[WebClient] = None,
+        auto_reconnect_enabled: bool = True,
+        ssl: Optional[SSLContext] = None,
+        proxy: Optional[str] = None,
+        timeout: int = 30,
+        base_url: str = WebClient.BASE_URL,
+        headers: Optional[dict] = None,
+        ping_interval: int = 5,
+        concurrency: int = 10,
+        logger: Optional[logging.Logger] = None,
+        on_message_listeners: Optional[List[Callable[[str], None]]] = None,
+        on_error_listeners: Optional[List[Callable[[Exception], None]]] = None,
+        on_close_listeners: Optional[List[Callable[[int, Optional[str]], None]]] = None,
+        trace_enabled: bool = False,
+        all_message_trace_enabled: bool = False,
+        ping_pong_trace_enabled: bool = False,
+    ):
+        self.token = token.strip() if token is not None else None
+        self.bot_id = None
+        self.default_auto_reconnect_enabled = auto_reconnect_enabled
+        # You may want temporarily turn off the auto_reconnect as necessary
+        self.auto_reconnect_enabled = self.default_auto_reconnect_enabled
+        self.ssl = ssl
+        self.proxy = proxy
+        self.timeout = timeout
+        self.base_url = base_url
+        self.headers = headers
+        self.ping_interval = ping_interval
+        self.logger = logger or logging.getLogger(__name__)
+        if self.proxy is None or len(self.proxy.strip()) == 0:
+            env_variable = load_http_proxy_from_env(self.logger)
+            if env_variable is not None:
+                self.proxy = env_variable
+
+        self.web_client = web_client or WebClient(
+            token=self.token,
+            base_url=self.base_url,
+            timeout=self.timeout,
+            ssl=self.ssl,
+            proxy=self.proxy,
+            headers=self.headers,
+            logger=logger,
+        )
+
+        self.on_message_listeners = on_message_listeners or []
+
+        self.on_error_listeners = on_error_listeners or []
+        self.on_close_listeners = on_close_listeners or []
+
+        self.trace_enabled = trace_enabled
+        self.all_message_trace_enabled = all_message_trace_enabled
+        self.ping_pong_trace_enabled = ping_pong_trace_enabled
+
+        self.message_queue = Queue()
+
+        def goodbye_listener(_self, event: dict):
+            if event.get("type") == "goodbye":
+                message = "Got a goodbye message. Reconnecting to the server ..."
+                self.logger.info(message)
+                self.connect_to_new_endpoint(force=True)
+
+        self.message_listeners = [goodbye_listener]
+        self.socket_mode_request_listeners = []
+
+        self.current_session = None
+        self.current_session_state = ConnectionState()
+        self.current_session_runner = IntervalRunner(self._run_current_session, 0.1).start()
+        self.wss_uri = None
+
+        self.current_app_monitor_started = False
+        self.current_app_monitor = IntervalRunner(
+            self._monitor_current_session,
+            self.ping_interval,
+        )
+
+        self.closed = False
+        self.connect_operation_lock = Lock()
+
+        self.message_processor = IntervalRunner(self.process_messages, 0.001).start()
+        self.message_workers = ThreadPoolExecutor(max_workers=concurrency)
+
+    # --------------------------------------------------------------
+    # Decorator to register listeners
+    # --------------------------------------------------------------
+
+    def on(self, event_type: str) -> Callable:
+        """Registers a new event listener.
+
+        Args:
+            event_type: str representing an event's type (e.g., message, reaction_added)
+        """
+
+        def __call__(*args, **kwargs):
+            func = args[0]
+            if func is not None:
+                if isinstance(func, Callable):
+                    name = (
+                        func.__name__
+                        if hasattr(func, "__name__")
+                        else f"{func.__class__.__module__}.{func.__class__.__name__}"
+                    )
+                    inspect_result: inspect.FullArgSpec = inspect.getfullargspec(func)
+                    if inspect_result is not None and len(inspect_result.args) != 2:
+                        actual_args = ", ".join(inspect_result.args)
+                        error = f"The listener '{name}' must accept two args: client, event (actual: {actual_args})"
+                        raise SlackClientError(error)
+
+                    def new_message_listener(_self, event: dict):
+                        actual_event_type = event.get("type")
+                        if event.get("bot_id") == self.bot_id:
+                            # SKip the events generated by this bot user
+                            return
+                        # https://github.com/slackapi/python-slack-sdk/issues/533
+                        if event_type == "*" or (actual_event_type is not None and actual_event_type == event_type):
+                            func(_self, event)
+
+                    self.message_listeners.append(new_message_listener)
+                else:
+                    error = f"The listener '{func}' is not a Callable (actual: {type(func).__name__})"
+                    raise SlackClientError(error)
+            # Not to cause modification to the decorated method
+            return func
+
+        return __call__
+
+    # --------------------------------------------------------------
+    # Connections
+    # --------------------------------------------------------------
+
+    def is_connected(self) -> bool:
+        """Returns True if this client is connected."""
+        return self.current_session is not None and self.current_session.is_active()
+
+    def issue_new_wss_url(self) -> str:
+        """Acquires a new WSS URL using rtm.connect API method"""
+        try:
+            api_response = self.web_client.rtm_connect()
+            return api_response["url"]
+        except SlackApiError as e:
+            if e.response["error"] == "ratelimited":
+                delay = int(e.response.headers.get("Retry-After", "30"))  # Tier1
+                self.logger.info(f"Rate limited. Retrying in {delay} seconds...")
+                time.sleep(delay)
+                # Retry to issue a new WSS URL
+                return self.issue_new_wss_url()
+            else:
+                # other errors
+                self.logger.error(f"Failed to retrieve WSS URL: {e}")
+                raise e
+
+    def connect_to_new_endpoint(self, force: bool = False):
+        """Acquires a new WSS URL and tries to connect to the endpoint."""
+        with self.connect_operation_lock:
+            if force or not self.is_connected():
+                self.logger.info("Connecting to a new endpoint...")
+                self.wss_uri = self.issue_new_wss_url()
+                self.connect()
+                self.logger.info("Connected to a new endpoint...")
+
+    def connect(self):
+        """Starts talking to the RTM server through a WebSocket connection"""
+        if self.bot_id is None:
+            self.bot_id = self.web_client.auth_test()["bot_id"]
+
+        old_session: Optional[Connection] = self.current_session
+        old_current_session_state: ConnectionState = self.current_session_state
+
+        if self.wss_uri is None:
+            self.wss_uri = self.issue_new_wss_url()
+
+        current_session = Connection(
+            url=self.wss_uri,
+            logger=self.logger,
+            ping_interval=self.ping_interval,
+            trace_enabled=self.trace_enabled,
+            all_message_trace_enabled=self.all_message_trace_enabled,
+            ping_pong_trace_enabled=self.ping_pong_trace_enabled,
+            receive_buffer_size=1024,
+            proxy=self.proxy,
+            on_message_listener=self.run_all_message_listeners,
+            on_error_listener=self.run_all_error_listeners,
+            on_close_listener=self.run_all_close_listeners,
+            connection_type_name="RTM",
+        )
+        current_session.connect()
+
+        if old_current_session_state is not None:
+            old_current_session_state.terminated = True
+        if old_session is not None:
+            old_session.close()
+
+        self.current_session = current_session
+        self.current_session_state = ConnectionState()
+        self.auto_reconnect_enabled = self.default_auto_reconnect_enabled
+
+        if not self.current_app_monitor_started:
+            self.current_app_monitor_started = True
+            self.current_app_monitor.start()
+
+        self.logger.info(f"A new session has been established (session id: {self.session_id()})")
+
+    def disconnect(self):
+        """Disconnects the current session."""
+        self.current_session.disconnect()
+
+    def close(self) -> None:
+        """
+        Closes this instance and cleans up underlying resources.
+        After calling this method, this instance is no longer usable.
+        """
+        self.closed = True
+        self.disconnect()
+        self.current_session.close()
+
+    def start(self) -> None:
+        """Establishes an RTM connection and blocks the current thread."""
+        self.connect()
+        Event().wait()
+
+    def send(self, payload: Union[dict, str]) -> None:
+        if payload is None:
+            return
+        if self.current_session is None or not self.current_session.is_active():
+            raise SlackClientError("The RTM client is not connected to the Slack servers")
+        if isinstance(payload, str):
+            self.current_session.send(payload)
+        else:
+            self.current_session.send(json.dumps(payload))
+
+    # --------------------------------------------------------------
+    # WS Message Processor
+    # --------------------------------------------------------------
+
+    def enqueue_message(self, message: str):
+        self.message_queue.put(message)
+        if self.logger.level <= logging.DEBUG:
+            self.logger.debug(f"A new message enqueued (current queue size: {self.message_queue.qsize()})")
+
+    def process_message(self):
+        try:
+            raw_message = self.message_queue.get(timeout=1)
+            if self.logger.level <= logging.DEBUG:
+                self.logger.debug(f"A message dequeued (current queue size: {self.message_queue.qsize()})")
+
+            if raw_message is not None:
+                message: dict = {}
+                if raw_message.startswith("{"):
+                    message = json.loads(raw_message)
+
+                def _run_message_listeners():
+                    self.run_message_listeners(message)
+
+                self.message_workers.submit(_run_message_listeners)
+        except Empty:
+            pass
+
+    def process_messages(self) -> None:
+        while not self.closed:
+            try:
+                self.process_message()
+            except Exception as e:
+                self.logger.exception(f"Failed to process a message: {e}")
+
+    def run_message_listeners(self, message: dict) -> None:
+        type = message.get("type")
+        if self.logger.level <= logging.DEBUG:
+            self.logger.debug(f"Message processing started (type: {type})")
+        try:
+            for listener in self.message_listeners:
+                try:
+                    listener(self, message)
+                except Exception as e:
+                    self.logger.exception(f"Failed to run a message listener: {e}")
+        except Exception as e:
+            self.logger.exception(f"Failed to run message listeners: {e}")
+        finally:
+            if self.logger.level <= logging.DEBUG:
+                self.logger.debug(f"Message processing completed (type: {type})")
+
+    # --------------------------------------------------------------
+    # Internals
+    # --------------------------------------------------------------
+
+    def session_id(self) -> Optional[str]:
+        if self.current_session is not None:
+            return self.current_session.session_id
+        return None
+
+    def run_all_message_listeners(self, message: str):
+        if self.logger.level <= logging.DEBUG:
+            self.logger.debug(f"on_message invoked: (message: {message})")
+        self.enqueue_message(message)
+        for listener in self.on_message_listeners:
+            listener(message)
+
+    def run_all_error_listeners(self, error: Exception):
+        self.logger.exception(
+            f"on_error invoked (session id: {self.session_id()}, " f"error: {type(error).__name__}, message: {error})"
+        )
+        for listener in self.on_error_listeners:
+            listener(error)
+
+    def run_all_close_listeners(self, code: int, reason: Optional[str] = None):
+        if self.logger.level <= logging.DEBUG:
+            self.logger.debug(f"on_close invoked (session id: {self.session_id()})")
+        if self.auto_reconnect_enabled:
+            self.logger.info("Received CLOSE event. Going to reconnect... " f"(session id: {self.session_id()})")
+            self.connect_to_new_endpoint()
+        for listener in self.on_close_listeners:
+            listener(code, reason)
+
+    def _run_current_session(self):
+        if self.current_session is not None and self.current_session.is_active():
+            session_id = self.session_id()
+            try:
+                self.logger.info("Starting to receive messages from a new connection" f" (session id: {session_id})")
+                self.current_session_state.terminated = False
+                self.current_session.run_until_completion(self.current_session_state)
+                self.logger.info("Stopped receiving messages from a connection" f" (session id: {session_id})")
+            except Exception as e:
+                self.logger.exception(
+                    "Failed to start or stop the current session" f" (session id: {session_id}, error: {e})"
+                )
+
+    def _monitor_current_session(self):
+        if self.current_app_monitor_started:
+            try:
+                self.current_session.check_state()
+
+                if self.auto_reconnect_enabled and (self.current_session is None or not self.current_session.is_active()):
+                    self.logger.info(
+                        "The session seems to be already closed. Going to reconnect... " f"(session id: {self.session_id()})"
+                    )
+                    self.connect_to_new_endpoint()
+            except Exception as e:
+                self.logger.error(
+                    "Failed to check the current session or reconnect to the server "
+                    f"(session id: {self.session_id()}, error: {type(e).__name__}, message: {e})"
+                )
+
+
+

Class variables

+
+
var auto_reconnect_enabled : bool
+
+

The type of the None singleton.

+
+
var base_url : str
+
+

The type of the None singleton.

+
+
var bot_id : str | None
+
+

The type of the None singleton.

+
+
var closed : bool
+
+

The type of the None singleton.

+
+
var connect_operation_lock : _thread.lock
+
+

The type of the None singleton.

+
+
var current_sessionConnection | None
+
+

The type of the None singleton.

+
+
var current_session_stateConnectionState | None
+
+

The type of the None singleton.

+
+
var default_auto_reconnect_enabled : bool
+
+

The type of the None singleton.

+
+
var logger : logging.Logger
+
+

The type of the None singleton.

+
+
var message_listeners : List[Callable[[RTMClient, dict], None]]
+
+

The type of the None singleton.

+
+
var message_processorIntervalRunner
+
+

The type of the None singleton.

+
+
var message_queue : queue.Queue
+
+

The type of the None singleton.

+
+
var message_workers : concurrent.futures.thread.ThreadPoolExecutor
+
+

The type of the None singleton.

+
+
var on_close_listeners : List[Callable[[int, str | None], None]]
+
+

The type of the None singleton.

+
+
var on_error_listeners : List[Callable[[Exception], None]]
+
+

The type of the None singleton.

+
+
var on_message_listeners : List[Callable[[str], None]]
+
+

The type of the None singleton.

+
+
var ping_interval : int
+
+

The type of the None singleton.

+
+
var proxy : str | None
+
+

The type of the None singleton.

+
+
var ssl : ssl.SSLContext | None
+
+

The type of the None singleton.

+
+
var timeout : int
+
+

The type of the None singleton.

+
+
var token : str | None
+
+

The type of the None singleton.

+
+
var web_clientWebClient
+
+

The type of the None singleton.

+
+
var wss_uri : str | None
+
+

The type of the None singleton.

+
+
+

Methods

+
+
+def close(self) ‑> None +
+
+
+ +Expand source code + +
def close(self) -> None:
+    """
+    Closes this instance and cleans up underlying resources.
+    After calling this method, this instance is no longer usable.
+    """
+    self.closed = True
+    self.disconnect()
+    self.current_session.close()
+
+

Closes this instance and cleans up underlying resources. +After calling this method, this instance is no longer usable.

+
+
+def connect(self) +
+
+
+ +Expand source code + +
def connect(self):
+    """Starts talking to the RTM server through a WebSocket connection"""
+    if self.bot_id is None:
+        self.bot_id = self.web_client.auth_test()["bot_id"]
+
+    old_session: Optional[Connection] = self.current_session
+    old_current_session_state: ConnectionState = self.current_session_state
+
+    if self.wss_uri is None:
+        self.wss_uri = self.issue_new_wss_url()
+
+    current_session = Connection(
+        url=self.wss_uri,
+        logger=self.logger,
+        ping_interval=self.ping_interval,
+        trace_enabled=self.trace_enabled,
+        all_message_trace_enabled=self.all_message_trace_enabled,
+        ping_pong_trace_enabled=self.ping_pong_trace_enabled,
+        receive_buffer_size=1024,
+        proxy=self.proxy,
+        on_message_listener=self.run_all_message_listeners,
+        on_error_listener=self.run_all_error_listeners,
+        on_close_listener=self.run_all_close_listeners,
+        connection_type_name="RTM",
+    )
+    current_session.connect()
+
+    if old_current_session_state is not None:
+        old_current_session_state.terminated = True
+    if old_session is not None:
+        old_session.close()
+
+    self.current_session = current_session
+    self.current_session_state = ConnectionState()
+    self.auto_reconnect_enabled = self.default_auto_reconnect_enabled
+
+    if not self.current_app_monitor_started:
+        self.current_app_monitor_started = True
+        self.current_app_monitor.start()
+
+    self.logger.info(f"A new session has been established (session id: {self.session_id()})")
+
+

Starts talking to the RTM server through a WebSocket connection

+
+
+def connect_to_new_endpoint(self, force: bool = False) +
+
+
+ +Expand source code + +
def connect_to_new_endpoint(self, force: bool = False):
+    """Acquires a new WSS URL and tries to connect to the endpoint."""
+    with self.connect_operation_lock:
+        if force or not self.is_connected():
+            self.logger.info("Connecting to a new endpoint...")
+            self.wss_uri = self.issue_new_wss_url()
+            self.connect()
+            self.logger.info("Connected to a new endpoint...")
+
+

Acquires a new WSS URL and tries to connect to the endpoint.

+
+
+def disconnect(self) +
+
+
+ +Expand source code + +
def disconnect(self):
+    """Disconnects the current session."""
+    self.current_session.disconnect()
+
+

Disconnects the current session.

+
+
+def enqueue_message(self, message: str) +
+
+
+ +Expand source code + +
def enqueue_message(self, message: str):
+    self.message_queue.put(message)
+    if self.logger.level <= logging.DEBUG:
+        self.logger.debug(f"A new message enqueued (current queue size: {self.message_queue.qsize()})")
+
+
+
+
+def is_connected(self) ‑> bool +
+
+
+ +Expand source code + +
def is_connected(self) -> bool:
+    """Returns True if this client is connected."""
+    return self.current_session is not None and self.current_session.is_active()
+
+

Returns True if this client is connected.

+
+
+def issue_new_wss_url(self) ‑> str +
+
+
+ +Expand source code + +
def issue_new_wss_url(self) -> str:
+    """Acquires a new WSS URL using rtm.connect API method"""
+    try:
+        api_response = self.web_client.rtm_connect()
+        return api_response["url"]
+    except SlackApiError as e:
+        if e.response["error"] == "ratelimited":
+            delay = int(e.response.headers.get("Retry-After", "30"))  # Tier1
+            self.logger.info(f"Rate limited. Retrying in {delay} seconds...")
+            time.sleep(delay)
+            # Retry to issue a new WSS URL
+            return self.issue_new_wss_url()
+        else:
+            # other errors
+            self.logger.error(f"Failed to retrieve WSS URL: {e}")
+            raise e
+
+

Acquires a new WSS URL using rtm.connect API method

+
+
+def on(self, event_type: str) ‑> Callable +
+
+
+ +Expand source code + +
def on(self, event_type: str) -> Callable:
+    """Registers a new event listener.
+
+    Args:
+        event_type: str representing an event's type (e.g., message, reaction_added)
+    """
+
+    def __call__(*args, **kwargs):
+        func = args[0]
+        if func is not None:
+            if isinstance(func, Callable):
+                name = (
+                    func.__name__
+                    if hasattr(func, "__name__")
+                    else f"{func.__class__.__module__}.{func.__class__.__name__}"
+                )
+                inspect_result: inspect.FullArgSpec = inspect.getfullargspec(func)
+                if inspect_result is not None and len(inspect_result.args) != 2:
+                    actual_args = ", ".join(inspect_result.args)
+                    error = f"The listener '{name}' must accept two args: client, event (actual: {actual_args})"
+                    raise SlackClientError(error)
+
+                def new_message_listener(_self, event: dict):
+                    actual_event_type = event.get("type")
+                    if event.get("bot_id") == self.bot_id:
+                        # SKip the events generated by this bot user
+                        return
+                    # https://github.com/slackapi/python-slack-sdk/issues/533
+                    if event_type == "*" or (actual_event_type is not None and actual_event_type == event_type):
+                        func(_self, event)
+
+                self.message_listeners.append(new_message_listener)
+            else:
+                error = f"The listener '{func}' is not a Callable (actual: {type(func).__name__})"
+                raise SlackClientError(error)
+        # Not to cause modification to the decorated method
+        return func
+
+    return __call__
+
+

Registers a new event listener.

+

Args

+
+
event_type
+
str representing an event's type (e.g., message, reaction_added)
+
+
+
+def process_message(self) +
+
+
+ +Expand source code + +
def process_message(self):
+    try:
+        raw_message = self.message_queue.get(timeout=1)
+        if self.logger.level <= logging.DEBUG:
+            self.logger.debug(f"A message dequeued (current queue size: {self.message_queue.qsize()})")
+
+        if raw_message is not None:
+            message: dict = {}
+            if raw_message.startswith("{"):
+                message = json.loads(raw_message)
+
+            def _run_message_listeners():
+                self.run_message_listeners(message)
+
+            self.message_workers.submit(_run_message_listeners)
+    except Empty:
+        pass
+
+
+
+
+def process_messages(self) ‑> None +
+
+
+ +Expand source code + +
def process_messages(self) -> None:
+    while not self.closed:
+        try:
+            self.process_message()
+        except Exception as e:
+            self.logger.exception(f"Failed to process a message: {e}")
+
+
+
+
+def run_all_close_listeners(self, code: int, reason: str | None = None) +
+
+
+ +Expand source code + +
def run_all_close_listeners(self, code: int, reason: Optional[str] = None):
+    if self.logger.level <= logging.DEBUG:
+        self.logger.debug(f"on_close invoked (session id: {self.session_id()})")
+    if self.auto_reconnect_enabled:
+        self.logger.info("Received CLOSE event. Going to reconnect... " f"(session id: {self.session_id()})")
+        self.connect_to_new_endpoint()
+    for listener in self.on_close_listeners:
+        listener(code, reason)
+
+
+
+
+def run_all_error_listeners(self, error: Exception) +
+
+
+ +Expand source code + +
def run_all_error_listeners(self, error: Exception):
+    self.logger.exception(
+        f"on_error invoked (session id: {self.session_id()}, " f"error: {type(error).__name__}, message: {error})"
+    )
+    for listener in self.on_error_listeners:
+        listener(error)
+
+
+
+
+def run_all_message_listeners(self, message: str) +
+
+
+ +Expand source code + +
def run_all_message_listeners(self, message: str):
+    if self.logger.level <= logging.DEBUG:
+        self.logger.debug(f"on_message invoked: (message: {message})")
+    self.enqueue_message(message)
+    for listener in self.on_message_listeners:
+        listener(message)
+
+
+
+
+def run_message_listeners(self, message: dict) ‑> None +
+
+
+ +Expand source code + +
def run_message_listeners(self, message: dict) -> None:
+    type = message.get("type")
+    if self.logger.level <= logging.DEBUG:
+        self.logger.debug(f"Message processing started (type: {type})")
+    try:
+        for listener in self.message_listeners:
+            try:
+                listener(self, message)
+            except Exception as e:
+                self.logger.exception(f"Failed to run a message listener: {e}")
+    except Exception as e:
+        self.logger.exception(f"Failed to run message listeners: {e}")
+    finally:
+        if self.logger.level <= logging.DEBUG:
+            self.logger.debug(f"Message processing completed (type: {type})")
+
+
+
+
+def send(self, payload: dict | str) ‑> None +
+
+
+ +Expand source code + +
def send(self, payload: Union[dict, str]) -> None:
+    if payload is None:
+        return
+    if self.current_session is None or not self.current_session.is_active():
+        raise SlackClientError("The RTM client is not connected to the Slack servers")
+    if isinstance(payload, str):
+        self.current_session.send(payload)
+    else:
+        self.current_session.send(json.dumps(payload))
+
+
+
+
+def session_id(self) ‑> str | None +
+
+
+ +Expand source code + +
def session_id(self) -> Optional[str]:
+    if self.current_session is not None:
+        return self.current_session.session_id
+    return None
+
+
+
+
+def start(self) ‑> None +
+
+
+ +Expand source code + +
def start(self) -> None:
+    """Establishes an RTM connection and blocks the current thread."""
+    self.connect()
+    Event().wait()
+
+

Establishes an RTM connection and blocks the current thread.

+
+
+
+
+
+
+ +
+ + + diff --git a/docs/reference/rtm_v2/index.html b/docs/reference/rtm_v2/index.html new file mode 100644 index 000000000..5d65bdc74 --- /dev/null +++ b/docs/reference/rtm_v2/index.html @@ -0,0 +1,991 @@ + + + + + + +slack_sdk.rtm_v2 API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.rtm_v2

+
+
+

A Python module for interacting with Slack's RTM API.

+
+
+
+
+
+
+
+
+

Classes

+
+
+class RTMClient +(*,
token: str | None = None,
web_client: WebClient | None = None,
auto_reconnect_enabled: bool = True,
ssl: ssl.SSLContext | None = None,
proxy: str | None = None,
timeout: int = 30,
base_url: str = 'https://slack.com/api/',
headers: dict | None = None,
ping_interval: int = 5,
concurrency: int = 10,
logger: logging.Logger | None = None,
on_message_listeners: List[Callable[[str], None]] | None = None,
on_error_listeners: List[Callable[[Exception], None]] | None = None,
on_close_listeners: List[Callable[[int, str | None], None]] | None = None,
trace_enabled: bool = False,
all_message_trace_enabled: bool = False,
ping_pong_trace_enabled: bool = False)
+
+
+
+ +Expand source code + +
class RTMClient:
+    token: Optional[str]
+    bot_id: Optional[str]
+    default_auto_reconnect_enabled: bool
+    auto_reconnect_enabled: bool
+    ssl: Optional[SSLContext]
+    proxy: Optional[str]
+    timeout: int
+    base_url: str
+    ping_interval: int
+    logger: Logger
+    web_client: WebClient
+
+    current_session: Optional[Connection]
+    current_session_state: Optional[ConnectionState]
+    wss_uri: Optional[str]
+
+    message_queue: Queue
+    message_listeners: List[Callable[["RTMClient", dict], None]]
+    message_processor: IntervalRunner
+    message_workers: ThreadPoolExecutor
+
+    closed: bool
+    connect_operation_lock: Lock
+
+    on_message_listeners: List[Callable[[str], None]]
+    on_error_listeners: List[Callable[[Exception], None]]
+    on_close_listeners: List[Callable[[int, Optional[str]], None]]
+
+    def __init__(
+        self,
+        *,
+        token: Optional[str] = None,
+        web_client: Optional[WebClient] = None,
+        auto_reconnect_enabled: bool = True,
+        ssl: Optional[SSLContext] = None,
+        proxy: Optional[str] = None,
+        timeout: int = 30,
+        base_url: str = WebClient.BASE_URL,
+        headers: Optional[dict] = None,
+        ping_interval: int = 5,
+        concurrency: int = 10,
+        logger: Optional[logging.Logger] = None,
+        on_message_listeners: Optional[List[Callable[[str], None]]] = None,
+        on_error_listeners: Optional[List[Callable[[Exception], None]]] = None,
+        on_close_listeners: Optional[List[Callable[[int, Optional[str]], None]]] = None,
+        trace_enabled: bool = False,
+        all_message_trace_enabled: bool = False,
+        ping_pong_trace_enabled: bool = False,
+    ):
+        self.token = token.strip() if token is not None else None
+        self.bot_id = None
+        self.default_auto_reconnect_enabled = auto_reconnect_enabled
+        # You may want temporarily turn off the auto_reconnect as necessary
+        self.auto_reconnect_enabled = self.default_auto_reconnect_enabled
+        self.ssl = ssl
+        self.proxy = proxy
+        self.timeout = timeout
+        self.base_url = base_url
+        self.headers = headers
+        self.ping_interval = ping_interval
+        self.logger = logger or logging.getLogger(__name__)
+        if self.proxy is None or len(self.proxy.strip()) == 0:
+            env_variable = load_http_proxy_from_env(self.logger)
+            if env_variable is not None:
+                self.proxy = env_variable
+
+        self.web_client = web_client or WebClient(
+            token=self.token,
+            base_url=self.base_url,
+            timeout=self.timeout,
+            ssl=self.ssl,
+            proxy=self.proxy,
+            headers=self.headers,
+            logger=logger,
+        )
+
+        self.on_message_listeners = on_message_listeners or []
+
+        self.on_error_listeners = on_error_listeners or []
+        self.on_close_listeners = on_close_listeners or []
+
+        self.trace_enabled = trace_enabled
+        self.all_message_trace_enabled = all_message_trace_enabled
+        self.ping_pong_trace_enabled = ping_pong_trace_enabled
+
+        self.message_queue = Queue()
+
+        def goodbye_listener(_self, event: dict):
+            if event.get("type") == "goodbye":
+                message = "Got a goodbye message. Reconnecting to the server ..."
+                self.logger.info(message)
+                self.connect_to_new_endpoint(force=True)
+
+        self.message_listeners = [goodbye_listener]
+        self.socket_mode_request_listeners = []
+
+        self.current_session = None
+        self.current_session_state = ConnectionState()
+        self.current_session_runner = IntervalRunner(self._run_current_session, 0.1).start()
+        self.wss_uri = None
+
+        self.current_app_monitor_started = False
+        self.current_app_monitor = IntervalRunner(
+            self._monitor_current_session,
+            self.ping_interval,
+        )
+
+        self.closed = False
+        self.connect_operation_lock = Lock()
+
+        self.message_processor = IntervalRunner(self.process_messages, 0.001).start()
+        self.message_workers = ThreadPoolExecutor(max_workers=concurrency)
+
+    # --------------------------------------------------------------
+    # Decorator to register listeners
+    # --------------------------------------------------------------
+
+    def on(self, event_type: str) -> Callable:
+        """Registers a new event listener.
+
+        Args:
+            event_type: str representing an event's type (e.g., message, reaction_added)
+        """
+
+        def __call__(*args, **kwargs):
+            func = args[0]
+            if func is not None:
+                if isinstance(func, Callable):
+                    name = (
+                        func.__name__
+                        if hasattr(func, "__name__")
+                        else f"{func.__class__.__module__}.{func.__class__.__name__}"
+                    )
+                    inspect_result: inspect.FullArgSpec = inspect.getfullargspec(func)
+                    if inspect_result is not None and len(inspect_result.args) != 2:
+                        actual_args = ", ".join(inspect_result.args)
+                        error = f"The listener '{name}' must accept two args: client, event (actual: {actual_args})"
+                        raise SlackClientError(error)
+
+                    def new_message_listener(_self, event: dict):
+                        actual_event_type = event.get("type")
+                        if event.get("bot_id") == self.bot_id:
+                            # SKip the events generated by this bot user
+                            return
+                        # https://github.com/slackapi/python-slack-sdk/issues/533
+                        if event_type == "*" or (actual_event_type is not None and actual_event_type == event_type):
+                            func(_self, event)
+
+                    self.message_listeners.append(new_message_listener)
+                else:
+                    error = f"The listener '{func}' is not a Callable (actual: {type(func).__name__})"
+                    raise SlackClientError(error)
+            # Not to cause modification to the decorated method
+            return func
+
+        return __call__
+
+    # --------------------------------------------------------------
+    # Connections
+    # --------------------------------------------------------------
+
+    def is_connected(self) -> bool:
+        """Returns True if this client is connected."""
+        return self.current_session is not None and self.current_session.is_active()
+
+    def issue_new_wss_url(self) -> str:
+        """Acquires a new WSS URL using rtm.connect API method"""
+        try:
+            api_response = self.web_client.rtm_connect()
+            return api_response["url"]
+        except SlackApiError as e:
+            if e.response["error"] == "ratelimited":
+                delay = int(e.response.headers.get("Retry-After", "30"))  # Tier1
+                self.logger.info(f"Rate limited. Retrying in {delay} seconds...")
+                time.sleep(delay)
+                # Retry to issue a new WSS URL
+                return self.issue_new_wss_url()
+            else:
+                # other errors
+                self.logger.error(f"Failed to retrieve WSS URL: {e}")
+                raise e
+
+    def connect_to_new_endpoint(self, force: bool = False):
+        """Acquires a new WSS URL and tries to connect to the endpoint."""
+        with self.connect_operation_lock:
+            if force or not self.is_connected():
+                self.logger.info("Connecting to a new endpoint...")
+                self.wss_uri = self.issue_new_wss_url()
+                self.connect()
+                self.logger.info("Connected to a new endpoint...")
+
+    def connect(self):
+        """Starts talking to the RTM server through a WebSocket connection"""
+        if self.bot_id is None:
+            self.bot_id = self.web_client.auth_test()["bot_id"]
+
+        old_session: Optional[Connection] = self.current_session
+        old_current_session_state: ConnectionState = self.current_session_state
+
+        if self.wss_uri is None:
+            self.wss_uri = self.issue_new_wss_url()
+
+        current_session = Connection(
+            url=self.wss_uri,
+            logger=self.logger,
+            ping_interval=self.ping_interval,
+            trace_enabled=self.trace_enabled,
+            all_message_trace_enabled=self.all_message_trace_enabled,
+            ping_pong_trace_enabled=self.ping_pong_trace_enabled,
+            receive_buffer_size=1024,
+            proxy=self.proxy,
+            on_message_listener=self.run_all_message_listeners,
+            on_error_listener=self.run_all_error_listeners,
+            on_close_listener=self.run_all_close_listeners,
+            connection_type_name="RTM",
+        )
+        current_session.connect()
+
+        if old_current_session_state is not None:
+            old_current_session_state.terminated = True
+        if old_session is not None:
+            old_session.close()
+
+        self.current_session = current_session
+        self.current_session_state = ConnectionState()
+        self.auto_reconnect_enabled = self.default_auto_reconnect_enabled
+
+        if not self.current_app_monitor_started:
+            self.current_app_monitor_started = True
+            self.current_app_monitor.start()
+
+        self.logger.info(f"A new session has been established (session id: {self.session_id()})")
+
+    def disconnect(self):
+        """Disconnects the current session."""
+        self.current_session.disconnect()
+
+    def close(self) -> None:
+        """
+        Closes this instance and cleans up underlying resources.
+        After calling this method, this instance is no longer usable.
+        """
+        self.closed = True
+        self.disconnect()
+        self.current_session.close()
+
+    def start(self) -> None:
+        """Establishes an RTM connection and blocks the current thread."""
+        self.connect()
+        Event().wait()
+
+    def send(self, payload: Union[dict, str]) -> None:
+        if payload is None:
+            return
+        if self.current_session is None or not self.current_session.is_active():
+            raise SlackClientError("The RTM client is not connected to the Slack servers")
+        if isinstance(payload, str):
+            self.current_session.send(payload)
+        else:
+            self.current_session.send(json.dumps(payload))
+
+    # --------------------------------------------------------------
+    # WS Message Processor
+    # --------------------------------------------------------------
+
+    def enqueue_message(self, message: str):
+        self.message_queue.put(message)
+        if self.logger.level <= logging.DEBUG:
+            self.logger.debug(f"A new message enqueued (current queue size: {self.message_queue.qsize()})")
+
+    def process_message(self):
+        try:
+            raw_message = self.message_queue.get(timeout=1)
+            if self.logger.level <= logging.DEBUG:
+                self.logger.debug(f"A message dequeued (current queue size: {self.message_queue.qsize()})")
+
+            if raw_message is not None:
+                message: dict = {}
+                if raw_message.startswith("{"):
+                    message = json.loads(raw_message)
+
+                def _run_message_listeners():
+                    self.run_message_listeners(message)
+
+                self.message_workers.submit(_run_message_listeners)
+        except Empty:
+            pass
+
+    def process_messages(self) -> None:
+        while not self.closed:
+            try:
+                self.process_message()
+            except Exception as e:
+                self.logger.exception(f"Failed to process a message: {e}")
+
+    def run_message_listeners(self, message: dict) -> None:
+        type = message.get("type")
+        if self.logger.level <= logging.DEBUG:
+            self.logger.debug(f"Message processing started (type: {type})")
+        try:
+            for listener in self.message_listeners:
+                try:
+                    listener(self, message)
+                except Exception as e:
+                    self.logger.exception(f"Failed to run a message listener: {e}")
+        except Exception as e:
+            self.logger.exception(f"Failed to run message listeners: {e}")
+        finally:
+            if self.logger.level <= logging.DEBUG:
+                self.logger.debug(f"Message processing completed (type: {type})")
+
+    # --------------------------------------------------------------
+    # Internals
+    # --------------------------------------------------------------
+
+    def session_id(self) -> Optional[str]:
+        if self.current_session is not None:
+            return self.current_session.session_id
+        return None
+
+    def run_all_message_listeners(self, message: str):
+        if self.logger.level <= logging.DEBUG:
+            self.logger.debug(f"on_message invoked: (message: {message})")
+        self.enqueue_message(message)
+        for listener in self.on_message_listeners:
+            listener(message)
+
+    def run_all_error_listeners(self, error: Exception):
+        self.logger.exception(
+            f"on_error invoked (session id: {self.session_id()}, " f"error: {type(error).__name__}, message: {error})"
+        )
+        for listener in self.on_error_listeners:
+            listener(error)
+
+    def run_all_close_listeners(self, code: int, reason: Optional[str] = None):
+        if self.logger.level <= logging.DEBUG:
+            self.logger.debug(f"on_close invoked (session id: {self.session_id()})")
+        if self.auto_reconnect_enabled:
+            self.logger.info("Received CLOSE event. Going to reconnect... " f"(session id: {self.session_id()})")
+            self.connect_to_new_endpoint()
+        for listener in self.on_close_listeners:
+            listener(code, reason)
+
+    def _run_current_session(self):
+        if self.current_session is not None and self.current_session.is_active():
+            session_id = self.session_id()
+            try:
+                self.logger.info("Starting to receive messages from a new connection" f" (session id: {session_id})")
+                self.current_session_state.terminated = False
+                self.current_session.run_until_completion(self.current_session_state)
+                self.logger.info("Stopped receiving messages from a connection" f" (session id: {session_id})")
+            except Exception as e:
+                self.logger.exception(
+                    "Failed to start or stop the current session" f" (session id: {session_id}, error: {e})"
+                )
+
+    def _monitor_current_session(self):
+        if self.current_app_monitor_started:
+            try:
+                self.current_session.check_state()
+
+                if self.auto_reconnect_enabled and (self.current_session is None or not self.current_session.is_active()):
+                    self.logger.info(
+                        "The session seems to be already closed. Going to reconnect... " f"(session id: {self.session_id()})"
+                    )
+                    self.connect_to_new_endpoint()
+            except Exception as e:
+                self.logger.error(
+                    "Failed to check the current session or reconnect to the server "
+                    f"(session id: {self.session_id()}, error: {type(e).__name__}, message: {e})"
+                )
+
+
+

Class variables

+
+
var auto_reconnect_enabled : bool
+
+

The type of the None singleton.

+
+
var base_url : str
+
+

The type of the None singleton.

+
+
var bot_id : str | None
+
+

The type of the None singleton.

+
+
var closed : bool
+
+

The type of the None singleton.

+
+
var connect_operation_lock : _thread.lock
+
+

The type of the None singleton.

+
+
var current_sessionConnection | None
+
+

The type of the None singleton.

+
+
var current_session_stateConnectionState | None
+
+

The type of the None singleton.

+
+
var default_auto_reconnect_enabled : bool
+
+

The type of the None singleton.

+
+
var logger : logging.Logger
+
+

The type of the None singleton.

+
+
var message_listeners : List[Callable[[RTMClient, dict], None]]
+
+

The type of the None singleton.

+
+
var message_processorIntervalRunner
+
+

The type of the None singleton.

+
+
var message_queue : queue.Queue
+
+

The type of the None singleton.

+
+
var message_workers : concurrent.futures.thread.ThreadPoolExecutor
+
+

The type of the None singleton.

+
+
var on_close_listeners : List[Callable[[int, str | None], None]]
+
+

The type of the None singleton.

+
+
var on_error_listeners : List[Callable[[Exception], None]]
+
+

The type of the None singleton.

+
+
var on_message_listeners : List[Callable[[str], None]]
+
+

The type of the None singleton.

+
+
var ping_interval : int
+
+

The type of the None singleton.

+
+
var proxy : str | None
+
+

The type of the None singleton.

+
+
var ssl : ssl.SSLContext | None
+
+

The type of the None singleton.

+
+
var timeout : int
+
+

The type of the None singleton.

+
+
var token : str | None
+
+

The type of the None singleton.

+
+
var web_clientWebClient
+
+

The type of the None singleton.

+
+
var wss_uri : str | None
+
+

The type of the None singleton.

+
+
+

Methods

+
+
+def close(self) ‑> None +
+
+
+ +Expand source code + +
def close(self) -> None:
+    """
+    Closes this instance and cleans up underlying resources.
+    After calling this method, this instance is no longer usable.
+    """
+    self.closed = True
+    self.disconnect()
+    self.current_session.close()
+
+

Closes this instance and cleans up underlying resources. +After calling this method, this instance is no longer usable.

+
+
+def connect(self) +
+
+
+ +Expand source code + +
def connect(self):
+    """Starts talking to the RTM server through a WebSocket connection"""
+    if self.bot_id is None:
+        self.bot_id = self.web_client.auth_test()["bot_id"]
+
+    old_session: Optional[Connection] = self.current_session
+    old_current_session_state: ConnectionState = self.current_session_state
+
+    if self.wss_uri is None:
+        self.wss_uri = self.issue_new_wss_url()
+
+    current_session = Connection(
+        url=self.wss_uri,
+        logger=self.logger,
+        ping_interval=self.ping_interval,
+        trace_enabled=self.trace_enabled,
+        all_message_trace_enabled=self.all_message_trace_enabled,
+        ping_pong_trace_enabled=self.ping_pong_trace_enabled,
+        receive_buffer_size=1024,
+        proxy=self.proxy,
+        on_message_listener=self.run_all_message_listeners,
+        on_error_listener=self.run_all_error_listeners,
+        on_close_listener=self.run_all_close_listeners,
+        connection_type_name="RTM",
+    )
+    current_session.connect()
+
+    if old_current_session_state is not None:
+        old_current_session_state.terminated = True
+    if old_session is not None:
+        old_session.close()
+
+    self.current_session = current_session
+    self.current_session_state = ConnectionState()
+    self.auto_reconnect_enabled = self.default_auto_reconnect_enabled
+
+    if not self.current_app_monitor_started:
+        self.current_app_monitor_started = True
+        self.current_app_monitor.start()
+
+    self.logger.info(f"A new session has been established (session id: {self.session_id()})")
+
+

Starts talking to the RTM server through a WebSocket connection

+
+
+def connect_to_new_endpoint(self, force: bool = False) +
+
+
+ +Expand source code + +
def connect_to_new_endpoint(self, force: bool = False):
+    """Acquires a new WSS URL and tries to connect to the endpoint."""
+    with self.connect_operation_lock:
+        if force or not self.is_connected():
+            self.logger.info("Connecting to a new endpoint...")
+            self.wss_uri = self.issue_new_wss_url()
+            self.connect()
+            self.logger.info("Connected to a new endpoint...")
+
+

Acquires a new WSS URL and tries to connect to the endpoint.

+
+
+def disconnect(self) +
+
+
+ +Expand source code + +
def disconnect(self):
+    """Disconnects the current session."""
+    self.current_session.disconnect()
+
+

Disconnects the current session.

+
+
+def enqueue_message(self, message: str) +
+
+
+ +Expand source code + +
def enqueue_message(self, message: str):
+    self.message_queue.put(message)
+    if self.logger.level <= logging.DEBUG:
+        self.logger.debug(f"A new message enqueued (current queue size: {self.message_queue.qsize()})")
+
+
+
+
+def is_connected(self) ‑> bool +
+
+
+ +Expand source code + +
def is_connected(self) -> bool:
+    """Returns True if this client is connected."""
+    return self.current_session is not None and self.current_session.is_active()
+
+

Returns True if this client is connected.

+
+
+def issue_new_wss_url(self) ‑> str +
+
+
+ +Expand source code + +
def issue_new_wss_url(self) -> str:
+    """Acquires a new WSS URL using rtm.connect API method"""
+    try:
+        api_response = self.web_client.rtm_connect()
+        return api_response["url"]
+    except SlackApiError as e:
+        if e.response["error"] == "ratelimited":
+            delay = int(e.response.headers.get("Retry-After", "30"))  # Tier1
+            self.logger.info(f"Rate limited. Retrying in {delay} seconds...")
+            time.sleep(delay)
+            # Retry to issue a new WSS URL
+            return self.issue_new_wss_url()
+        else:
+            # other errors
+            self.logger.error(f"Failed to retrieve WSS URL: {e}")
+            raise e
+
+

Acquires a new WSS URL using rtm.connect API method

+
+
+def on(self, event_type: str) ‑> Callable +
+
+
+ +Expand source code + +
def on(self, event_type: str) -> Callable:
+    """Registers a new event listener.
+
+    Args:
+        event_type: str representing an event's type (e.g., message, reaction_added)
+    """
+
+    def __call__(*args, **kwargs):
+        func = args[0]
+        if func is not None:
+            if isinstance(func, Callable):
+                name = (
+                    func.__name__
+                    if hasattr(func, "__name__")
+                    else f"{func.__class__.__module__}.{func.__class__.__name__}"
+                )
+                inspect_result: inspect.FullArgSpec = inspect.getfullargspec(func)
+                if inspect_result is not None and len(inspect_result.args) != 2:
+                    actual_args = ", ".join(inspect_result.args)
+                    error = f"The listener '{name}' must accept two args: client, event (actual: {actual_args})"
+                    raise SlackClientError(error)
+
+                def new_message_listener(_self, event: dict):
+                    actual_event_type = event.get("type")
+                    if event.get("bot_id") == self.bot_id:
+                        # SKip the events generated by this bot user
+                        return
+                    # https://github.com/slackapi/python-slack-sdk/issues/533
+                    if event_type == "*" or (actual_event_type is not None and actual_event_type == event_type):
+                        func(_self, event)
+
+                self.message_listeners.append(new_message_listener)
+            else:
+                error = f"The listener '{func}' is not a Callable (actual: {type(func).__name__})"
+                raise SlackClientError(error)
+        # Not to cause modification to the decorated method
+        return func
+
+    return __call__
+
+

Registers a new event listener.

+

Args

+
+
event_type
+
str representing an event's type (e.g., message, reaction_added)
+
+
+
+def process_message(self) +
+
+
+ +Expand source code + +
def process_message(self):
+    try:
+        raw_message = self.message_queue.get(timeout=1)
+        if self.logger.level <= logging.DEBUG:
+            self.logger.debug(f"A message dequeued (current queue size: {self.message_queue.qsize()})")
+
+        if raw_message is not None:
+            message: dict = {}
+            if raw_message.startswith("{"):
+                message = json.loads(raw_message)
+
+            def _run_message_listeners():
+                self.run_message_listeners(message)
+
+            self.message_workers.submit(_run_message_listeners)
+    except Empty:
+        pass
+
+
+
+
+def process_messages(self) ‑> None +
+
+
+ +Expand source code + +
def process_messages(self) -> None:
+    while not self.closed:
+        try:
+            self.process_message()
+        except Exception as e:
+            self.logger.exception(f"Failed to process a message: {e}")
+
+
+
+
+def run_all_close_listeners(self, code: int, reason: str | None = None) +
+
+
+ +Expand source code + +
def run_all_close_listeners(self, code: int, reason: Optional[str] = None):
+    if self.logger.level <= logging.DEBUG:
+        self.logger.debug(f"on_close invoked (session id: {self.session_id()})")
+    if self.auto_reconnect_enabled:
+        self.logger.info("Received CLOSE event. Going to reconnect... " f"(session id: {self.session_id()})")
+        self.connect_to_new_endpoint()
+    for listener in self.on_close_listeners:
+        listener(code, reason)
+
+
+
+
+def run_all_error_listeners(self, error: Exception) +
+
+
+ +Expand source code + +
def run_all_error_listeners(self, error: Exception):
+    self.logger.exception(
+        f"on_error invoked (session id: {self.session_id()}, " f"error: {type(error).__name__}, message: {error})"
+    )
+    for listener in self.on_error_listeners:
+        listener(error)
+
+
+
+
+def run_all_message_listeners(self, message: str) +
+
+
+ +Expand source code + +
def run_all_message_listeners(self, message: str):
+    if self.logger.level <= logging.DEBUG:
+        self.logger.debug(f"on_message invoked: (message: {message})")
+    self.enqueue_message(message)
+    for listener in self.on_message_listeners:
+        listener(message)
+
+
+
+
+def run_message_listeners(self, message: dict) ‑> None +
+
+
+ +Expand source code + +
def run_message_listeners(self, message: dict) -> None:
+    type = message.get("type")
+    if self.logger.level <= logging.DEBUG:
+        self.logger.debug(f"Message processing started (type: {type})")
+    try:
+        for listener in self.message_listeners:
+            try:
+                listener(self, message)
+            except Exception as e:
+                self.logger.exception(f"Failed to run a message listener: {e}")
+    except Exception as e:
+        self.logger.exception(f"Failed to run message listeners: {e}")
+    finally:
+        if self.logger.level <= logging.DEBUG:
+            self.logger.debug(f"Message processing completed (type: {type})")
+
+
+
+
+def send(self, payload: dict | str) ‑> None +
+
+
+ +Expand source code + +
def send(self, payload: Union[dict, str]) -> None:
+    if payload is None:
+        return
+    if self.current_session is None or not self.current_session.is_active():
+        raise SlackClientError("The RTM client is not connected to the Slack servers")
+    if isinstance(payload, str):
+        self.current_session.send(payload)
+    else:
+        self.current_session.send(json.dumps(payload))
+
+
+
+
+def session_id(self) ‑> str | None +
+
+
+ +Expand source code + +
def session_id(self) -> Optional[str]:
+    if self.current_session is not None:
+        return self.current_session.session_id
+    return None
+
+
+
+
+def start(self) ‑> None +
+
+
+ +Expand source code + +
def start(self) -> None:
+    """Establishes an RTM connection and blocks the current thread."""
+    self.connect()
+    Event().wait()
+
+

Establishes an RTM connection and blocks the current thread.

+
+
+
+
+
+
+ +
+ + + diff --git a/docs/reference/scim/async_client.html b/docs/reference/scim/async_client.html new file mode 100644 index 000000000..c91441c29 --- /dev/null +++ b/docs/reference/scim/async_client.html @@ -0,0 +1,838 @@ + + + + + + +slack_sdk.scim.async_client API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.scim.async_client

+
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class AsyncSCIMClient +(token: str,
timeout: int = 30,
ssl: ssl.SSLContext | None = None,
proxy: str | None = None,
base_url: str = 'https://api.slack.com/scim/v1/',
session: aiohttp.client.ClientSession | None = None,
trust_env_in_session: bool = False,
auth: aiohttp.helpers.BasicAuth | None = None,
default_headers: Dict[str, str] | None = None,
user_agent_prefix: str | None = None,
user_agent_suffix: str | None = None,
logger: logging.Logger | None = None,
retry_handlers: List[slack_sdk.http_retry.async_handler.AsyncRetryHandler] | None = None)
+
+
+
+ +Expand source code + +
class AsyncSCIMClient:
+    BASE_URL = "https://api.slack.com/scim/v1/"
+
+    token: str
+    timeout: int
+    ssl: Optional[SSLContext]
+    proxy: Optional[str]
+    base_url: str
+    session: Optional[ClientSession]
+    trust_env_in_session: bool
+    auth: Optional[BasicAuth]
+    default_headers: Dict[str, str]
+    logger: logging.Logger
+    retry_handlers: List[AsyncRetryHandler]
+
+    def __init__(
+        self,
+        token: str,
+        timeout: int = 30,
+        ssl: Optional[SSLContext] = None,
+        proxy: Optional[str] = None,
+        base_url: str = BASE_URL,
+        session: Optional[ClientSession] = None,
+        trust_env_in_session: bool = False,
+        auth: Optional[BasicAuth] = None,
+        default_headers: Optional[Dict[str, str]] = None,
+        user_agent_prefix: Optional[str] = None,
+        user_agent_suffix: Optional[str] = None,
+        logger: Optional[logging.Logger] = None,
+        retry_handlers: Optional[List[AsyncRetryHandler]] = None,
+    ):
+        """API client for SCIM API
+        See https://docs.slack.dev/admins/scim-api/ for more details
+
+        Args:
+            token: An admin user's token, which starts with `xoxp-`
+            timeout: Request timeout (in seconds)
+            ssl: `ssl.SSLContext` to use for requests
+            proxy: Proxy URL (e.g., `localhost:9000`, `http://localhost:9000`)
+            base_url: The base URL for API calls
+            session: `aiohttp.ClientSession` instance
+            trust_env_in_session: True/False for `aiohttp.ClientSession`
+            auth: Basic auth info for `aiohttp.ClientSession`
+            default_headers: Request headers to add to all requests
+            user_agent_prefix: Prefix for User-Agent header value
+            user_agent_suffix: Suffix for User-Agent header value
+            logger: Custom logger
+            retry_handlers: Retry handlers
+        """
+        self.token = token
+        self.timeout = timeout
+        self.ssl = ssl
+        self.proxy = proxy
+        self.base_url = base_url
+        self.session = session
+        self.trust_env_in_session = trust_env_in_session
+        self.auth = auth
+        self.default_headers = default_headers if default_headers else {}
+        self.default_headers["User-Agent"] = get_user_agent(user_agent_prefix, user_agent_suffix)
+        self.logger = logger if logger is not None else logging.getLogger(__name__)
+        self.retry_handlers = retry_handlers if retry_handlers is not None else async_default_handlers()
+
+        if self.proxy is None or len(self.proxy.strip()) == 0:
+            env_variable = load_http_proxy_from_env(self.logger)
+            if env_variable is not None:
+                self.proxy = env_variable
+
+    # -------------------------
+    # Users
+    # -------------------------
+
+    async def search_users(
+        self,
+        *,
+        # Pagination required as of August 30, 2019.
+        count: int,
+        start_index: int,
+        filter: Optional[str] = None,
+    ) -> SearchUsersResponse:
+        return SearchUsersResponse(
+            await self.api_call(
+                http_verb="GET",
+                path="Users",
+                query_params={
+                    "filter": filter,
+                    "count": count,
+                    "startIndex": start_index,
+                },
+            )
+        )
+
+    async def read_user(self, id: str) -> ReadUserResponse:
+        return ReadUserResponse(await self.api_call(http_verb="GET", path=f"Users/{quote(id)}"))
+
+    async def create_user(self, user: Union[Dict[str, Any], User]) -> UserCreateResponse:
+        return UserCreateResponse(
+            await self.api_call(
+                http_verb="POST",
+                path="Users",
+                body_params=user.to_dict() if isinstance(user, User) else _to_dict_without_not_given(user),
+            )
+        )
+
+    async def patch_user(self, id: str, partial_user: Union[Dict[str, Any], User]) -> UserPatchResponse:
+        return UserPatchResponse(
+            await self.api_call(
+                http_verb="PATCH",
+                path=f"Users/{quote(id)}",
+                body_params=(
+                    partial_user.to_dict() if isinstance(partial_user, User) else _to_dict_without_not_given(partial_user)
+                ),
+            )
+        )
+
+    async def update_user(self, user: Union[Dict[str, Any], User]) -> UserUpdateResponse:
+        user_id = user.id if isinstance(user, User) else user["id"]
+        return UserUpdateResponse(
+            await self.api_call(
+                http_verb="PUT",
+                path=f"Users/{quote(user_id)}",
+                body_params=user.to_dict() if isinstance(user, User) else _to_dict_without_not_given(user),
+            )
+        )
+
+    async def delete_user(self, id: str) -> UserDeleteResponse:
+        return UserDeleteResponse(
+            await self.api_call(
+                http_verb="DELETE",
+                path=f"Users/{quote(id)}",
+            )
+        )
+
+    # -------------------------
+    # Groups
+    # -------------------------
+
+    async def search_groups(
+        self,
+        *,
+        # Pagination required as of August 30, 2019.
+        count: int,
+        start_index: int,
+        filter: Optional[str] = None,
+    ) -> SearchGroupsResponse:
+        return SearchGroupsResponse(
+            await self.api_call(
+                http_verb="GET",
+                path="Groups",
+                query_params={
+                    "filter": filter,
+                    "count": count,
+                    "startIndex": start_index,
+                },
+            )
+        )
+
+    async def read_group(self, id: str) -> ReadGroupResponse:
+        return ReadGroupResponse(await self.api_call(http_verb="GET", path=f"Groups/{quote(id)}"))
+
+    async def create_group(self, group: Union[Dict[str, Any], Group]) -> GroupCreateResponse:
+        return GroupCreateResponse(
+            await self.api_call(
+                http_verb="POST",
+                path="Groups",
+                body_params=group.to_dict() if isinstance(group, Group) else _to_dict_without_not_given(group),
+            )
+        )
+
+    async def patch_group(self, id: str, partial_group: Union[Dict[str, Any], Group]) -> GroupPatchResponse:
+        return GroupPatchResponse(
+            await self.api_call(
+                http_verb="PATCH",
+                path=f"Groups/{quote(id)}",
+                body_params=(
+                    partial_group.to_dict()
+                    if isinstance(partial_group, Group)
+                    else _to_dict_without_not_given(partial_group)
+                ),
+            )
+        )
+
+    async def update_group(self, group: Union[Dict[str, Any], Group]) -> GroupUpdateResponse:
+        group_id = group.id if isinstance(group, Group) else group["id"]
+        return GroupUpdateResponse(
+            await self.api_call(
+                http_verb="PUT",
+                path=f"Groups/{quote(group_id)}",
+                body_params=group.to_dict() if isinstance(group, Group) else _to_dict_without_not_given(group),
+            )
+        )
+
+    async def delete_group(self, id: str) -> GroupDeleteResponse:
+        return GroupDeleteResponse(
+            await self.api_call(
+                http_verb="DELETE",
+                path=f"Groups/{quote(id)}",
+            )
+        )
+
+    # -------------------------
+
+    async def api_call(
+        self,
+        *,
+        http_verb: str,
+        path: str,
+        query_params: Optional[Dict[str, Any]] = None,
+        body_params: Optional[Dict[str, Any]] = None,
+        headers: Optional[Dict[str, str]] = None,
+    ) -> SCIMResponse:
+        url = f"{self.base_url}{path}"
+        query = _build_query(query_params)
+        if len(query) > 0:
+            url += f"?{query}"
+        return await self._perform_http_request(
+            http_verb=http_verb,
+            url=url,
+            body_params=body_params,
+            headers=_build_request_headers(
+                token=self.token,
+                default_headers=self.default_headers,
+                additional_headers=headers,
+            ),
+        )
+
+    async def _perform_http_request(
+        self,
+        *,
+        http_verb: str,
+        url: str,
+        body_params: Optional[Dict[str, Any]],
+        headers: Dict[str, str],
+    ) -> SCIMResponse:
+        if body_params is not None:
+            if body_params.get("schemas") is None:
+                body_params["schemas"] = ["urn:scim:schemas:core:1.0"]
+            body_params = json.dumps(body_params)
+        headers["Content-Type"] = "application/json;charset=utf-8"
+
+        session: Optional[ClientSession] = None
+        use_running_session = self.session and not self.session.closed
+        if use_running_session:
+            session = self.session
+        else:
+            session = aiohttp.ClientSession(
+                timeout=aiohttp.ClientTimeout(total=self.timeout),
+                auth=self.auth,
+                trust_env=self.trust_env_in_session,
+            )
+
+        last_error: Optional[Exception] = None
+        resp: Optional[SCIMResponse] = None
+        try:
+            request_kwargs = {
+                "headers": headers,
+                "data": body_params,
+                "ssl": self.ssl,
+                "proxy": self.proxy,
+            }
+            retry_request = RetryHttpRequest(
+                method=http_verb,
+                url=url,
+                headers=headers,
+                body_params=body_params,
+            )
+
+            retry_state = RetryState()
+            counter_for_safety = 0
+            while counter_for_safety < 100:
+                counter_for_safety += 1
+                # If this is a retry, the next try started here. We can reset the flag.
+                retry_state.next_attempt_requested = False
+                retry_response: Optional[RetryHttpResponse] = None
+                response_body = ""
+
+                if self.logger.level <= logging.DEBUG:
+                    headers_for_logging = {
+                        k: "(redacted)" if k.lower() == "authorization" else v for k, v in headers.items()
+                    }
+                    self.logger.debug(
+                        f"Sending a request - url: {url}, params: {body_params}, headers: {headers_for_logging}"
+                    )
+
+                try:
+                    async with session.request(http_verb, url, **request_kwargs) as res:
+                        try:
+                            response_body = await res.text()
+                            retry_response = RetryHttpResponse(
+                                status_code=res.status,
+                                headers=res.headers,
+                                data=response_body.encode("utf-8") if response_body is not None else None,
+                            )
+                        except aiohttp.ContentTypeError:
+                            self.logger.debug(f"No response data returned from the following API call: {url}.")
+                            retry_response = RetryHttpResponse(
+                                status_code=res.status,
+                                headers=res.headers,
+                            )
+
+                        if res.status == 429:
+                            for handler in self.retry_handlers:
+                                if await handler.can_retry_async(
+                                    state=retry_state,
+                                    request=retry_request,
+                                    response=retry_response,
+                                ):
+                                    if self.logger.level <= logging.DEBUG:
+                                        self.logger.info(
+                                            f"A retry handler found: {type(handler).__name__} "
+                                            f"for {http_verb} {url} - rate_limited"
+                                        )
+                                    await handler.prepare_for_next_attempt_async(
+                                        state=retry_state,
+                                        request=retry_request,
+                                        response=retry_response,
+                                    )
+                                    break
+
+                        if retry_state.next_attempt_requested is False:
+                            resp = SCIMResponse(
+                                url=url,
+                                status_code=res.status,
+                                raw_body=response_body,
+                                headers=res.headers,
+                            )
+                            _debug_log_response(self.logger, resp)
+                            return resp
+
+                except Exception as e:
+                    last_error = e
+                    for handler in self.retry_handlers:
+                        if await handler.can_retry_async(
+                            state=retry_state,
+                            request=retry_request,
+                            response=retry_response,
+                            error=e,
+                        ):
+                            if self.logger.level <= logging.DEBUG:
+                                self.logger.info(
+                                    f"A retry handler found: {type(handler).__name__} " f"for {http_verb} {url} - {e}"
+                                )
+                            await handler.prepare_for_next_attempt_async(
+                                state=retry_state,
+                                request=retry_request,
+                                response=retry_response,
+                                error=e,
+                            )
+                            break
+
+                    if retry_state.next_attempt_requested is False:
+                        raise last_error
+
+            if resp is not None:
+                return resp
+            raise last_error
+
+        finally:
+            if not use_running_session:
+                await session.close()
+
+        return resp
+
+

API client for SCIM API +See https://docs.slack.dev/admins/scim-api/ for more details

+

Args

+
+
token
+
An admin user's token, which starts with xoxp-
+
timeout
+
Request timeout (in seconds)
+
ssl
+
ssl.SSLContext to use for requests
+
proxy
+
Proxy URL (e.g., localhost:9000, http://localhost:9000)
+
base_url
+
The base URL for API calls
+
session
+
aiohttp.ClientSession instance
+
trust_env_in_session
+
True/False for aiohttp.ClientSession
+
auth
+
Basic auth info for aiohttp.ClientSession
+
default_headers
+
Request headers to add to all requests
+
user_agent_prefix
+
Prefix for User-Agent header value
+
user_agent_suffix
+
Suffix for User-Agent header value
+
logger
+
Custom logger
+
retry_handlers
+
Retry handlers
+
+

Class variables

+
+
var BASE_URL
+
+

The type of the None singleton.

+
+
var auth : aiohttp.helpers.BasicAuth | None
+
+

The type of the None singleton.

+
+
var base_url : str
+
+

The type of the None singleton.

+
+
var default_headers : Dict[str, str]
+
+

The type of the None singleton.

+
+
var logger : logging.Logger
+
+

The type of the None singleton.

+
+
var proxy : str | None
+
+

The type of the None singleton.

+
+
var retry_handlers : List[slack_sdk.http_retry.async_handler.AsyncRetryHandler]
+
+

The type of the None singleton.

+
+
var session : aiohttp.client.ClientSession | None
+
+

The type of the None singleton.

+
+
var ssl : ssl.SSLContext | None
+
+

The type of the None singleton.

+
+
var timeout : int
+
+

The type of the None singleton.

+
+
var token : str
+
+

The type of the None singleton.

+
+
var trust_env_in_session : bool
+
+

The type of the None singleton.

+
+
+

Methods

+
+
+async def api_call(self,
*,
http_verb: str,
path: str,
query_params: Dict[str, Any] | None = None,
body_params: Dict[str, Any] | None = None,
headers: Dict[str, str] | None = None) ‑> SCIMResponse
+
+
+
+ +Expand source code + +
async def api_call(
+    self,
+    *,
+    http_verb: str,
+    path: str,
+    query_params: Optional[Dict[str, Any]] = None,
+    body_params: Optional[Dict[str, Any]] = None,
+    headers: Optional[Dict[str, str]] = None,
+) -> SCIMResponse:
+    url = f"{self.base_url}{path}"
+    query = _build_query(query_params)
+    if len(query) > 0:
+        url += f"?{query}"
+    return await self._perform_http_request(
+        http_verb=http_verb,
+        url=url,
+        body_params=body_params,
+        headers=_build_request_headers(
+            token=self.token,
+            default_headers=self.default_headers,
+            additional_headers=headers,
+        ),
+    )
+
+
+
+
+async def create_group(self,
group: Dict[str, Any] | Group) ‑> GroupCreateResponse
+
+
+
+ +Expand source code + +
async def create_group(self, group: Union[Dict[str, Any], Group]) -> GroupCreateResponse:
+    return GroupCreateResponse(
+        await self.api_call(
+            http_verb="POST",
+            path="Groups",
+            body_params=group.to_dict() if isinstance(group, Group) else _to_dict_without_not_given(group),
+        )
+    )
+
+
+
+
+async def create_user(self,
user: Dict[str, Any] | User) ‑> UserCreateResponse
+
+
+
+ +Expand source code + +
async def create_user(self, user: Union[Dict[str, Any], User]) -> UserCreateResponse:
+    return UserCreateResponse(
+        await self.api_call(
+            http_verb="POST",
+            path="Users",
+            body_params=user.to_dict() if isinstance(user, User) else _to_dict_without_not_given(user),
+        )
+    )
+
+
+
+
+async def delete_group(self, id: str) ‑> GroupDeleteResponse +
+
+
+ +Expand source code + +
async def delete_group(self, id: str) -> GroupDeleteResponse:
+    return GroupDeleteResponse(
+        await self.api_call(
+            http_verb="DELETE",
+            path=f"Groups/{quote(id)}",
+        )
+    )
+
+
+
+
+async def delete_user(self, id: str) ‑> UserDeleteResponse +
+
+
+ +Expand source code + +
async def delete_user(self, id: str) -> UserDeleteResponse:
+    return UserDeleteResponse(
+        await self.api_call(
+            http_verb="DELETE",
+            path=f"Users/{quote(id)}",
+        )
+    )
+
+
+
+
+async def patch_group(self,
id: str,
partial_group: Dict[str, Any] | Group) ‑> GroupPatchResponse
+
+
+
+ +Expand source code + +
async def patch_group(self, id: str, partial_group: Union[Dict[str, Any], Group]) -> GroupPatchResponse:
+    return GroupPatchResponse(
+        await self.api_call(
+            http_verb="PATCH",
+            path=f"Groups/{quote(id)}",
+            body_params=(
+                partial_group.to_dict()
+                if isinstance(partial_group, Group)
+                else _to_dict_without_not_given(partial_group)
+            ),
+        )
+    )
+
+
+
+
+async def patch_user(self,
id: str,
partial_user: Dict[str, Any] | User) ‑> UserPatchResponse
+
+
+
+ +Expand source code + +
async def patch_user(self, id: str, partial_user: Union[Dict[str, Any], User]) -> UserPatchResponse:
+    return UserPatchResponse(
+        await self.api_call(
+            http_verb="PATCH",
+            path=f"Users/{quote(id)}",
+            body_params=(
+                partial_user.to_dict() if isinstance(partial_user, User) else _to_dict_without_not_given(partial_user)
+            ),
+        )
+    )
+
+
+
+
+async def read_group(self, id: str) ‑> ReadGroupResponse +
+
+
+ +Expand source code + +
async def read_group(self, id: str) -> ReadGroupResponse:
+    return ReadGroupResponse(await self.api_call(http_verb="GET", path=f"Groups/{quote(id)}"))
+
+
+
+
+async def read_user(self, id: str) ‑> ReadUserResponse +
+
+
+ +Expand source code + +
async def read_user(self, id: str) -> ReadUserResponse:
+    return ReadUserResponse(await self.api_call(http_verb="GET", path=f"Users/{quote(id)}"))
+
+
+
+
+async def search_groups(self, *, count: int, start_index: int, filter: str | None = None) ‑> SearchGroupsResponse +
+
+
+ +Expand source code + +
async def search_groups(
+    self,
+    *,
+    # Pagination required as of August 30, 2019.
+    count: int,
+    start_index: int,
+    filter: Optional[str] = None,
+) -> SearchGroupsResponse:
+    return SearchGroupsResponse(
+        await self.api_call(
+            http_verb="GET",
+            path="Groups",
+            query_params={
+                "filter": filter,
+                "count": count,
+                "startIndex": start_index,
+            },
+        )
+    )
+
+
+
+
+async def search_users(self, *, count: int, start_index: int, filter: str | None = None) ‑> SearchUsersResponse +
+
+
+ +Expand source code + +
async def search_users(
+    self,
+    *,
+    # Pagination required as of August 30, 2019.
+    count: int,
+    start_index: int,
+    filter: Optional[str] = None,
+) -> SearchUsersResponse:
+    return SearchUsersResponse(
+        await self.api_call(
+            http_verb="GET",
+            path="Users",
+            query_params={
+                "filter": filter,
+                "count": count,
+                "startIndex": start_index,
+            },
+        )
+    )
+
+
+
+
+async def update_group(self,
group: Dict[str, Any] | Group) ‑> GroupUpdateResponse
+
+
+
+ +Expand source code + +
async def update_group(self, group: Union[Dict[str, Any], Group]) -> GroupUpdateResponse:
+    group_id = group.id if isinstance(group, Group) else group["id"]
+    return GroupUpdateResponse(
+        await self.api_call(
+            http_verb="PUT",
+            path=f"Groups/{quote(group_id)}",
+            body_params=group.to_dict() if isinstance(group, Group) else _to_dict_without_not_given(group),
+        )
+    )
+
+
+
+
+async def update_user(self,
user: Dict[str, Any] | User) ‑> UserUpdateResponse
+
+
+
+ +Expand source code + +
async def update_user(self, user: Union[Dict[str, Any], User]) -> UserUpdateResponse:
+    user_id = user.id if isinstance(user, User) else user["id"]
+    return UserUpdateResponse(
+        await self.api_call(
+            http_verb="PUT",
+            path=f"Users/{quote(user_id)}",
+            body_params=user.to_dict() if isinstance(user, User) else _to_dict_without_not_given(user),
+        )
+    )
+
+
+
+
+
+
+
+
+ +
+ + + diff --git a/docs/reference/scim/index.html b/docs/reference/scim/index.html new file mode 100644 index 000000000..1e6e1e228 --- /dev/null +++ b/docs/reference/scim/index.html @@ -0,0 +1,1572 @@ + + + + + + +slack_sdk.scim API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.scim

+
+
+

SCIM API is a set of APIs for provisioning and managing user accounts and groups. +SCIM is used by Single Sign-On (SSO) services and identity providers to manage people across a variety of tools, +including Slack.

+

Refer to https://docs.slack.dev/tools/python-slack-sdk/scim for details.

+
+
+

Sub-modules

+
+
slack_sdk.scim.async_client
+
+
+
+
slack_sdk.scim.v1
+
+

SCIM API is a set of APIs for provisioning and managing user accounts and groups. +SCIM is used by Single Sign-On (SSO) services and identity providers …

+
+
+
+
+
+
+
+
+

Classes

+
+
+class Group +(*,
display_name: str | DefaultArg | None = <slack_sdk.scim.v1.default_arg.DefaultArg object>,
id: str | DefaultArg | None = <slack_sdk.scim.v1.default_arg.DefaultArg object>,
members: List[GroupMember] | DefaultArg | None = <slack_sdk.scim.v1.default_arg.DefaultArg object>,
meta: GroupMeta | DefaultArg | None = <slack_sdk.scim.v1.default_arg.DefaultArg object>,
schemas: List[str] | DefaultArg | None = <slack_sdk.scim.v1.default_arg.DefaultArg object>,
**kwargs)
+
+
+
+ +Expand source code + +
class Group:
+    display_name: Union[Optional[str], DefaultArg]
+    id: Union[Optional[str], DefaultArg]
+    members: Union[Optional[List[GroupMember]], DefaultArg]
+    meta: Union[Optional[GroupMeta], DefaultArg]
+    schemas: Union[Optional[List[str]], DefaultArg]
+    unknown_fields: Dict[str, Any]
+
+    def __init__(
+        self,
+        *,
+        display_name: Union[Optional[str], DefaultArg] = NotGiven,
+        id: Union[Optional[str], DefaultArg] = NotGiven,
+        members: Union[Optional[List[GroupMember]], DefaultArg] = NotGiven,
+        meta: Union[Optional[GroupMeta], DefaultArg] = NotGiven,
+        schemas: Union[Optional[List[str]], DefaultArg] = NotGiven,
+        **kwargs,
+    ) -> None:
+        self.display_name = display_name
+        self.id = id
+        self.members = (
+            [a if isinstance(a, GroupMember) else GroupMember(**a) for a in members] if _is_iterable(members) else members
+        )
+        self.meta = GroupMeta(**meta) if meta is not None and isinstance(meta, dict) else meta
+        self.schemas = schemas
+        self.unknown_fields = kwargs
+
+    def to_dict(self):
+        return _to_dict_without_not_given(self)
+
+    def __repr__(self):
+        return f"<slack_sdk.scim.{self.__class__.__name__}: {self.to_dict()}>"
+
+
+

Class variables

+
+
var display_name : str | DefaultArg | None
+
+

The type of the None singleton.

+
+
var id : str | DefaultArg | None
+
+

The type of the None singleton.

+
+
var members : List[GroupMember] | DefaultArg | None
+
+

The type of the None singleton.

+
+
var metaGroupMeta | DefaultArg | None
+
+

The type of the None singleton.

+
+
var schemas : List[str] | DefaultArg | None
+
+

The type of the None singleton.

+
+
var unknown_fields : Dict[str, Any]
+
+

The type of the None singleton.

+
+
+

Methods

+
+
+def to_dict(self) +
+
+
+ +Expand source code + +
def to_dict(self):
+    return _to_dict_without_not_given(self)
+
+
+
+
+
+
+class ReadGroupResponse +(underlying: SCIMResponse) +
+
+
+ +Expand source code + +
class ReadGroupResponse(SCIMResponse):
+    group: Group
+
+    @property
+    def group(self) -> Group:
+        return Group(**self.snake_cased_body)
+
+    def __init__(self, underlying: SCIMResponse):
+        self.underlying = underlying
+        self.url = underlying.url
+        self.status_code = underlying.status_code
+        self.headers = underlying.headers
+        self.raw_body = underlying.raw_body
+        self.body = underlying.body
+        self._snake_cased_body = None
+
+
+

Ancestors

+ +

Instance variables

+
+
prop groupGroup
+
+
+ +Expand source code + +
@property
+def group(self) -> Group:
+    return Group(**self.snake_cased_body)
+
+
+
+
+

Inherited members

+ +
+
+class ReadUserResponse +(underlying: SCIMResponse) +
+
+
+ +Expand source code + +
class ReadUserResponse(SCIMResponse):
+    user: User
+
+    @property
+    def user(self) -> User:
+        return User(**self.snake_cased_body)
+
+    def __init__(self, underlying: SCIMResponse):
+        self.underlying = underlying
+        self.url = underlying.url
+        self.status_code = underlying.status_code
+        self.headers = underlying.headers
+        self.raw_body = underlying.raw_body
+        self.body = underlying.body
+        self._snake_cased_body = None
+
+
+

Ancestors

+ +

Instance variables

+
+
prop userUser
+
+
+ +Expand source code + +
@property
+def user(self) -> User:
+    return User(**self.snake_cased_body)
+
+
+
+
+

Inherited members

+ +
+
+class SCIMClient +(token: str,
timeout: int = 30,
ssl: ssl.SSLContext | None = None,
proxy: str | None = None,
base_url: str = 'https://api.slack.com/scim/v1/',
default_headers: Dict[str, str] | None = None,
user_agent_prefix: str | None = None,
user_agent_suffix: str | None = None,
logger: logging.Logger | None = None,
retry_handlers: List[RetryHandler] | None = None)
+
+
+
+ +Expand source code + +
class SCIMClient:
+    BASE_URL = "https://api.slack.com/scim/v1/"
+
+    token: str
+    timeout: int
+    ssl: Optional[SSLContext]
+    proxy: Optional[str]
+    base_url: str
+    default_headers: Dict[str, str]
+    logger: logging.Logger
+    retry_handlers: List[RetryHandler]
+
+    def __init__(
+        self,
+        token: str,
+        timeout: int = 30,
+        ssl: Optional[SSLContext] = None,
+        proxy: Optional[str] = None,
+        base_url: str = BASE_URL,
+        default_headers: Optional[Dict[str, str]] = None,
+        user_agent_prefix: Optional[str] = None,
+        user_agent_suffix: Optional[str] = None,
+        logger: Optional[logging.Logger] = None,
+        retry_handlers: Optional[List[RetryHandler]] = None,
+    ):
+        """API client for SCIM API
+        See https://docs.slack.dev/admins/scim-api/ for more details
+
+        Args:
+            token: An admin user's token, which starts with `xoxp-`
+            timeout: Request timeout (in seconds)
+            ssl: `ssl.SSLContext` to use for requests
+            proxy: Proxy URL (e.g., `localhost:9000`, `http://localhost:9000`)
+            base_url: The base URL for API calls
+            default_headers: Request headers to add to all requests
+            user_agent_prefix: Prefix for User-Agent header value
+            user_agent_suffix: Suffix for User-Agent header value
+            logger: Custom logger
+            retry_handlers: Retry handlers
+        """
+        self.token = token
+        self.timeout = timeout
+        self.ssl = ssl
+        self.proxy = proxy
+        self.base_url = base_url
+        self.default_headers = default_headers if default_headers else {}
+        self.default_headers["User-Agent"] = get_user_agent(user_agent_prefix, user_agent_suffix)
+        self.logger = logger if logger is not None else logging.getLogger(__name__)
+        self.retry_handlers = retry_handlers if retry_handlers is not None else default_retry_handlers()
+
+        if self.proxy is None or len(self.proxy.strip()) == 0:
+            env_variable = load_http_proxy_from_env(self.logger)
+            if env_variable is not None:
+                self.proxy = env_variable
+
+    # -------------------------
+    # Users
+    # -------------------------
+
+    def search_users(
+        self,
+        *,
+        # Pagination required as of August 30, 2019.
+        count: int,
+        start_index: int,
+        filter: Optional[str] = None,
+    ) -> SearchUsersResponse:
+        return SearchUsersResponse(
+            self.api_call(
+                http_verb="GET",
+                path="Users",
+                query_params={
+                    "filter": filter,
+                    "count": count,
+                    "startIndex": start_index,
+                },
+            )
+        )
+
+    def read_user(self, id: str) -> ReadUserResponse:
+        return ReadUserResponse(self.api_call(http_verb="GET", path=f"Users/{quote(id)}"))
+
+    def create_user(self, user: Union[Dict[str, Any], User]) -> UserCreateResponse:
+        return UserCreateResponse(
+            self.api_call(
+                http_verb="POST",
+                path="Users",
+                body_params=user.to_dict() if isinstance(user, User) else _to_dict_without_not_given(user),
+            )
+        )
+
+    def patch_user(self, id: str, partial_user: Union[Dict[str, Any], User]) -> UserPatchResponse:
+        return UserPatchResponse(
+            self.api_call(
+                http_verb="PATCH",
+                path=f"Users/{quote(id)}",
+                body_params=(
+                    partial_user.to_dict() if isinstance(partial_user, User) else _to_dict_without_not_given(partial_user)
+                ),
+            )
+        )
+
+    def update_user(self, user: Union[Dict[str, Any], User]) -> UserUpdateResponse:
+        user_id = user.id if isinstance(user, User) else user["id"]
+        return UserUpdateResponse(
+            self.api_call(
+                http_verb="PUT",
+                path=f"Users/{quote(user_id)}",
+                body_params=user.to_dict() if isinstance(user, User) else _to_dict_without_not_given(user),
+            )
+        )
+
+    def delete_user(self, id: str) -> UserDeleteResponse:
+        return UserDeleteResponse(
+            self.api_call(
+                http_verb="DELETE",
+                path=f"Users/{quote(id)}",
+            )
+        )
+
+    # -------------------------
+    # Groups
+    # -------------------------
+
+    def search_groups(
+        self,
+        *,
+        # Pagination required as of August 30, 2019.
+        count: int,
+        start_index: int,
+        filter: Optional[str] = None,
+    ) -> SearchGroupsResponse:
+        return SearchGroupsResponse(
+            self.api_call(
+                http_verb="GET",
+                path="Groups",
+                query_params={
+                    "filter": filter,
+                    "count": count,
+                    "startIndex": start_index,
+                },
+            )
+        )
+
+    def read_group(self, id: str) -> ReadGroupResponse:
+        return ReadGroupResponse(self.api_call(http_verb="GET", path=f"Groups/{quote(id)}"))
+
+    def create_group(self, group: Union[Dict[str, Any], Group]) -> GroupCreateResponse:
+        return GroupCreateResponse(
+            self.api_call(
+                http_verb="POST",
+                path="Groups",
+                body_params=group.to_dict() if isinstance(group, Group) else _to_dict_without_not_given(group),
+            )
+        )
+
+    def patch_group(self, id: str, partial_group: Union[Dict[str, Any], Group]) -> GroupPatchResponse:
+        return GroupPatchResponse(
+            self.api_call(
+                http_verb="PATCH",
+                path=f"Groups/{quote(id)}",
+                body_params=(
+                    partial_group.to_dict()
+                    if isinstance(partial_group, Group)
+                    else _to_dict_without_not_given(partial_group)
+                ),
+            )
+        )
+
+    def update_group(self, group: Union[Dict[str, Any], Group]) -> GroupUpdateResponse:
+        group_id = group.id if isinstance(group, Group) else group["id"]
+        return GroupUpdateResponse(
+            self.api_call(
+                http_verb="PUT",
+                path=f"Groups/{quote(group_id)}",
+                body_params=group.to_dict() if isinstance(group, Group) else _to_dict_without_not_given(group),
+            )
+        )
+
+    def delete_group(self, id: str) -> GroupDeleteResponse:
+        return GroupDeleteResponse(
+            self.api_call(
+                http_verb="DELETE",
+                path=f"Groups/{quote(id)}",
+            )
+        )
+
+    # -------------------------
+
+    def api_call(
+        self,
+        *,
+        http_verb: str,
+        path: str,
+        query_params: Optional[Dict[str, Any]] = None,
+        body_params: Optional[Dict[str, Any]] = None,
+        headers: Optional[Dict[str, str]] = None,
+    ) -> SCIMResponse:
+        """Performs a Slack API request and returns the result."""
+        url = f"{self.base_url}{path}"
+        query = _build_query(query_params)
+        if len(query) > 0:
+            url += f"?{query}"
+
+        return self._perform_http_request(
+            http_verb=http_verb,
+            url=url,
+            body=body_params,
+            headers=_build_request_headers(
+                token=self.token,
+                default_headers=self.default_headers,
+                additional_headers=headers,
+            ),
+        )
+
+    def _perform_http_request(
+        self,
+        *,
+        http_verb: str = "GET",
+        url: str,
+        body: Optional[Dict[str, Any]] = None,
+        headers: Dict[str, str],
+    ) -> SCIMResponse:
+        if body is not None:
+            if body.get("schemas") is None:
+                body["schemas"] = ["urn:scim:schemas:core:1.0"]
+            body = json.dumps(body)
+        headers["Content-Type"] = "application/json;charset=utf-8"
+
+        if self.logger.level <= logging.DEBUG:
+            headers_for_logging = {k: "(redacted)" if k.lower() == "authorization" else v for k, v in headers.items()}
+            self.logger.debug(f"Sending a request - {http_verb} url: {url}, body: {body}, headers: {headers_for_logging}")
+
+        # NOTE: Intentionally ignore the `http_verb` here
+        # Slack APIs accepts any API method requests with POST methods
+        req = Request(
+            method=http_verb,
+            url=url,
+            data=body.encode("utf-8") if body is not None else None,
+            headers=headers,
+        )
+        resp = None
+        last_error = None
+
+        retry_state = RetryState()
+        counter_for_safety = 0
+        while counter_for_safety < 100:
+            counter_for_safety += 1
+            # If this is a retry, the next try started here. We can reset the flag.
+            retry_state.next_attempt_requested = False
+
+            try:
+                resp = self._perform_http_request_internal(url, req)
+                # The resp is a 200 OK response
+                return resp
+
+            except HTTPError as e:
+                # read the response body here
+                charset = e.headers.get_content_charset() or "utf-8"
+                response_body: str = e.read().decode(charset)
+                # As adding new values to HTTPError#headers can be ignored, building a new dict object here
+                response_headers = dict(e.headers.items())
+                resp = SCIMResponse(
+                    url=url,
+                    status_code=e.code,
+                    raw_body=response_body,
+                    headers=response_headers,
+                )
+                if e.code == 429:
+                    # for backward-compatibility with WebClient (v.2.5.0 or older)
+                    if "retry-after" not in resp.headers and "Retry-After" in resp.headers:
+                        resp.headers["retry-after"] = resp.headers["Retry-After"]
+                    if "Retry-After" not in resp.headers and "retry-after" in resp.headers:
+                        resp.headers["Retry-After"] = resp.headers["retry-after"]
+                _debug_log_response(self.logger, resp)
+
+                # Try to find a retry handler for this error
+                retry_request = RetryHttpRequest.from_urllib_http_request(req)
+                retry_response = RetryHttpResponse(
+                    status_code=e.code,
+                    headers={k: [v] for k, v in e.headers.items()},
+                    data=response_body.encode("utf-8") if response_body is not None else None,
+                )
+                for handler in self.retry_handlers:
+                    if handler.can_retry(
+                        state=retry_state,
+                        request=retry_request,
+                        response=retry_response,
+                        error=e,
+                    ):
+                        if self.logger.level <= logging.DEBUG:
+                            self.logger.info(
+                                f"A retry handler found: {type(handler).__name__} for {req.method} {req.full_url} - {e}"
+                            )
+                        handler.prepare_for_next_attempt(
+                            state=retry_state,
+                            request=retry_request,
+                            response=retry_response,
+                            error=e,
+                        )
+                        break
+
+                if retry_state.next_attempt_requested is False:
+                    return resp
+
+            except Exception as err:
+                last_error = err
+                self.logger.error(f"Failed to send a request to Slack API server: {err}")
+
+                # Try to find a retry handler for this error
+                retry_request = RetryHttpRequest.from_urllib_http_request(req)
+                for handler in self.retry_handlers:
+                    if handler.can_retry(
+                        state=retry_state,
+                        request=retry_request,
+                        response=None,
+                        error=err,
+                    ):
+                        if self.logger.level <= logging.DEBUG:
+                            self.logger.info(
+                                f"A retry handler found: {type(handler).__name__} for {req.method} {req.full_url} - {err}"
+                            )
+                        handler.prepare_for_next_attempt(
+                            state=retry_state,
+                            request=retry_request,
+                            response=None,
+                            error=err,
+                        )
+                        self.logger.info(f"Going to retry the same request: {req.method} {req.full_url}")
+                        break
+
+                if retry_state.next_attempt_requested is False:
+                    raise err
+
+        if resp is not None:
+            return resp
+        raise last_error
+
+    def _perform_http_request_internal(self, url: str, req: Request) -> SCIMResponse:
+        opener: Optional[OpenerDirector] = None
+        # for security (BAN-B310)
+        if url.lower().startswith("http"):
+            if self.proxy is not None:
+                if isinstance(self.proxy, str):
+                    opener = urllib.request.build_opener(
+                        ProxyHandler({"http": self.proxy, "https": self.proxy}),
+                        HTTPSHandler(context=self.ssl),
+                    )
+                else:
+                    raise SlackRequestError(f"Invalid proxy detected: {self.proxy} must be a str value")
+        else:
+            raise SlackRequestError(f"Invalid URL detected: {url}")
+
+        # NOTE: BAN-B310 is already checked above
+        http_resp: Optional[HTTPResponse] = None
+        if opener:
+            http_resp = opener.open(req, timeout=self.timeout)
+        else:
+            http_resp = urlopen(req, context=self.ssl, timeout=self.timeout)
+        charset: str = http_resp.headers.get_content_charset() or "utf-8"
+        response_body: str = http_resp.read().decode(charset)
+        resp = SCIMResponse(
+            url=url,
+            status_code=http_resp.status,
+            raw_body=response_body,
+            headers=http_resp.headers,
+        )
+        _debug_log_response(self.logger, resp)
+        return resp
+
+

API client for SCIM API +See https://docs.slack.dev/admins/scim-api/ for more details

+

Args

+
+
token
+
An admin user's token, which starts with xoxp-
+
timeout
+
Request timeout (in seconds)
+
ssl
+
ssl.SSLContext to use for requests
+
proxy
+
Proxy URL (e.g., localhost:9000, http://localhost:9000)
+
base_url
+
The base URL for API calls
+
default_headers
+
Request headers to add to all requests
+
user_agent_prefix
+
Prefix for User-Agent header value
+
user_agent_suffix
+
Suffix for User-Agent header value
+
logger
+
Custom logger
+
retry_handlers
+
Retry handlers
+
+

Class variables

+
+
var BASE_URL
+
+

The type of the None singleton.

+
+
var base_url : str
+
+

The type of the None singleton.

+
+
var default_headers : Dict[str, str]
+
+

The type of the None singleton.

+
+
var logger : logging.Logger
+
+

The type of the None singleton.

+
+
var proxy : str | None
+
+

The type of the None singleton.

+
+
var retry_handlers : List[RetryHandler]
+
+

The type of the None singleton.

+
+
var ssl : ssl.SSLContext | None
+
+

The type of the None singleton.

+
+
var timeout : int
+
+

The type of the None singleton.

+
+
var token : str
+
+

The type of the None singleton.

+
+
+

Methods

+
+
+def api_call(self,
*,
http_verb: str,
path: str,
query_params: Dict[str, Any] | None = None,
body_params: Dict[str, Any] | None = None,
headers: Dict[str, str] | None = None) ‑> SCIMResponse
+
+
+
+ +Expand source code + +
def api_call(
+    self,
+    *,
+    http_verb: str,
+    path: str,
+    query_params: Optional[Dict[str, Any]] = None,
+    body_params: Optional[Dict[str, Any]] = None,
+    headers: Optional[Dict[str, str]] = None,
+) -> SCIMResponse:
+    """Performs a Slack API request and returns the result."""
+    url = f"{self.base_url}{path}"
+    query = _build_query(query_params)
+    if len(query) > 0:
+        url += f"?{query}"
+
+    return self._perform_http_request(
+        http_verb=http_verb,
+        url=url,
+        body=body_params,
+        headers=_build_request_headers(
+            token=self.token,
+            default_headers=self.default_headers,
+            additional_headers=headers,
+        ),
+    )
+
+

Performs a Slack API request and returns the result.

+
+
+def create_group(self,
group: Dict[str, Any] | Group) ‑> GroupCreateResponse
+
+
+
+ +Expand source code + +
def create_group(self, group: Union[Dict[str, Any], Group]) -> GroupCreateResponse:
+    return GroupCreateResponse(
+        self.api_call(
+            http_verb="POST",
+            path="Groups",
+            body_params=group.to_dict() if isinstance(group, Group) else _to_dict_without_not_given(group),
+        )
+    )
+
+
+
+
+def create_user(self,
user: Dict[str, Any] | User) ‑> UserCreateResponse
+
+
+
+ +Expand source code + +
def create_user(self, user: Union[Dict[str, Any], User]) -> UserCreateResponse:
+    return UserCreateResponse(
+        self.api_call(
+            http_verb="POST",
+            path="Users",
+            body_params=user.to_dict() if isinstance(user, User) else _to_dict_without_not_given(user),
+        )
+    )
+
+
+
+
+def delete_group(self, id: str) ‑> GroupDeleteResponse +
+
+
+ +Expand source code + +
def delete_group(self, id: str) -> GroupDeleteResponse:
+    return GroupDeleteResponse(
+        self.api_call(
+            http_verb="DELETE",
+            path=f"Groups/{quote(id)}",
+        )
+    )
+
+
+
+
+def delete_user(self, id: str) ‑> UserDeleteResponse +
+
+
+ +Expand source code + +
def delete_user(self, id: str) -> UserDeleteResponse:
+    return UserDeleteResponse(
+        self.api_call(
+            http_verb="DELETE",
+            path=f"Users/{quote(id)}",
+        )
+    )
+
+
+
+
+def patch_group(self,
id: str,
partial_group: Dict[str, Any] | Group) ‑> GroupPatchResponse
+
+
+
+ +Expand source code + +
def patch_group(self, id: str, partial_group: Union[Dict[str, Any], Group]) -> GroupPatchResponse:
+    return GroupPatchResponse(
+        self.api_call(
+            http_verb="PATCH",
+            path=f"Groups/{quote(id)}",
+            body_params=(
+                partial_group.to_dict()
+                if isinstance(partial_group, Group)
+                else _to_dict_without_not_given(partial_group)
+            ),
+        )
+    )
+
+
+
+
+def patch_user(self,
id: str,
partial_user: Dict[str, Any] | User) ‑> UserPatchResponse
+
+
+
+ +Expand source code + +
def patch_user(self, id: str, partial_user: Union[Dict[str, Any], User]) -> UserPatchResponse:
+    return UserPatchResponse(
+        self.api_call(
+            http_verb="PATCH",
+            path=f"Users/{quote(id)}",
+            body_params=(
+                partial_user.to_dict() if isinstance(partial_user, User) else _to_dict_without_not_given(partial_user)
+            ),
+        )
+    )
+
+
+
+
+def read_group(self, id: str) ‑> ReadGroupResponse +
+
+
+ +Expand source code + +
def read_group(self, id: str) -> ReadGroupResponse:
+    return ReadGroupResponse(self.api_call(http_verb="GET", path=f"Groups/{quote(id)}"))
+
+
+
+
+def read_user(self, id: str) ‑> ReadUserResponse +
+
+
+ +Expand source code + +
def read_user(self, id: str) -> ReadUserResponse:
+    return ReadUserResponse(self.api_call(http_verb="GET", path=f"Users/{quote(id)}"))
+
+
+
+
+def search_groups(self, *, count: int, start_index: int, filter: str | None = None) ‑> SearchGroupsResponse +
+
+
+ +Expand source code + +
def search_groups(
+    self,
+    *,
+    # Pagination required as of August 30, 2019.
+    count: int,
+    start_index: int,
+    filter: Optional[str] = None,
+) -> SearchGroupsResponse:
+    return SearchGroupsResponse(
+        self.api_call(
+            http_verb="GET",
+            path="Groups",
+            query_params={
+                "filter": filter,
+                "count": count,
+                "startIndex": start_index,
+            },
+        )
+    )
+
+
+
+
+def search_users(self, *, count: int, start_index: int, filter: str | None = None) ‑> SearchUsersResponse +
+
+
+ +Expand source code + +
def search_users(
+    self,
+    *,
+    # Pagination required as of August 30, 2019.
+    count: int,
+    start_index: int,
+    filter: Optional[str] = None,
+) -> SearchUsersResponse:
+    return SearchUsersResponse(
+        self.api_call(
+            http_verb="GET",
+            path="Users",
+            query_params={
+                "filter": filter,
+                "count": count,
+                "startIndex": start_index,
+            },
+        )
+    )
+
+
+
+
+def update_group(self,
group: Dict[str, Any] | Group) ‑> GroupUpdateResponse
+
+
+
+ +Expand source code + +
def update_group(self, group: Union[Dict[str, Any], Group]) -> GroupUpdateResponse:
+    group_id = group.id if isinstance(group, Group) else group["id"]
+    return GroupUpdateResponse(
+        self.api_call(
+            http_verb="PUT",
+            path=f"Groups/{quote(group_id)}",
+            body_params=group.to_dict() if isinstance(group, Group) else _to_dict_without_not_given(group),
+        )
+    )
+
+
+
+
+def update_user(self,
user: Dict[str, Any] | User) ‑> UserUpdateResponse
+
+
+
+ +Expand source code + +
def update_user(self, user: Union[Dict[str, Any], User]) -> UserUpdateResponse:
+    user_id = user.id if isinstance(user, User) else user["id"]
+    return UserUpdateResponse(
+        self.api_call(
+            http_verb="PUT",
+            path=f"Users/{quote(user_id)}",
+            body_params=user.to_dict() if isinstance(user, User) else _to_dict_without_not_given(user),
+        )
+    )
+
+
+
+
+
+
+class SCIMResponse +(*, url: str, status_code: int, raw_body: str | None, headers: dict) +
+
+
+ +Expand source code + +
class SCIMResponse:
+    url: str
+    status_code: int
+    headers: Dict[str, Any]
+    raw_body: Optional[str]
+    body: Optional[Dict[str, Any]]
+    snake_cased_body: Optional[Dict[str, Any]]
+
+    errors: Optional[Errors]
+
+    @property
+    def snake_cased_body(self) -> Optional[Dict[str, Any]]:
+        if self._snake_cased_body is None:
+            self._snake_cased_body = _to_snake_cased(self.body)
+        return self._snake_cased_body
+
+    @property
+    def errors(self) -> Optional[Errors]:
+        errors = self.snake_cased_body.get("errors")
+        if errors is None:
+            return None
+        return Errors(**errors)
+
+    def __init__(
+        self,
+        *,
+        url: str,
+        status_code: int,
+        raw_body: Optional[str],
+        headers: dict,
+    ):
+        self.url = url
+        self.status_code = status_code
+        self.headers = headers
+        self.raw_body = raw_body
+        self.body = json.loads(raw_body) if raw_body is not None and raw_body.startswith("{") else None
+        self._snake_cased_body = None  # build this when it's accessed for the first time
+
+    def __repr__(self):
+        dict_value = {}
+        for key, value in vars(self).items():
+            dict_value[key] = value.to_dict() if hasattr(value, "to_dict") else value
+
+        if dict_value:
+            return f"<slack_sdk.scim.v1.{self.__class__.__name__}: {dict_value}>"
+        else:
+            return self.__str__()
+
+
+

Subclasses

+ +

Class variables

+
+
var body : Dict[str, Any] | None
+
+

The type of the None singleton.

+
+
var headers : Dict[str, Any]
+
+

The type of the None singleton.

+
+
var raw_body : str | None
+
+

The type of the None singleton.

+
+
var status_code : int
+
+

The type of the None singleton.

+
+
var url : str
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
prop errorsErrors | None
+
+
+ +Expand source code + +
@property
+def errors(self) -> Optional[Errors]:
+    errors = self.snake_cased_body.get("errors")
+    if errors is None:
+        return None
+    return Errors(**errors)
+
+
+
+
prop snake_cased_body : Dict[str, Any] | None
+
+
+ +Expand source code + +
@property
+def snake_cased_body(self) -> Optional[Dict[str, Any]]:
+    if self._snake_cased_body is None:
+        self._snake_cased_body = _to_snake_cased(self.body)
+    return self._snake_cased_body
+
+
+
+
+
+
+class SearchGroupsResponse +(underlying: SCIMResponse) +
+
+
+ +Expand source code + +
class SearchGroupsResponse(SCIMResponse):
+    groups: List[Group]
+
+    @property
+    def groups(self) -> List[Group]:
+        return [Group(**r) for r in self.snake_cased_body.get("resources")]
+
+    def __init__(self, underlying: SCIMResponse):
+        self.underlying = underlying
+        self.url = underlying.url
+        self.status_code = underlying.status_code
+        self.headers = underlying.headers
+        self.raw_body = underlying.raw_body
+        self.body = underlying.body
+        self._snake_cased_body = None
+
+
+

Ancestors

+ +

Instance variables

+
+
prop groups : List[Group]
+
+
+ +Expand source code + +
@property
+def groups(self) -> List[Group]:
+    return [Group(**r) for r in self.snake_cased_body.get("resources")]
+
+
+
+
+

Inherited members

+ +
+
+class SearchUsersResponse +(underlying: SCIMResponse) +
+
+
+ +Expand source code + +
class SearchUsersResponse(SCIMResponse):
+    users: List[User]
+
+    @property
+    def users(self) -> List[User]:
+        return [User(**r) for r in self.snake_cased_body.get("resources")]
+
+    def __init__(self, underlying: SCIMResponse):
+        self.underlying = underlying
+        self.url = underlying.url
+        self.status_code = underlying.status_code
+        self.headers = underlying.headers
+        self.raw_body = underlying.raw_body
+        self.body = underlying.body
+        self._snake_cased_body = None
+
+
+

Ancestors

+ +

Instance variables

+
+
prop users : List[User]
+
+
+ +Expand source code + +
@property
+def users(self) -> List[User]:
+    return [User(**r) for r in self.snake_cased_body.get("resources")]
+
+
+
+
+

Inherited members

+ +
+
+class User +(*,
active: bool | DefaultArg | None = <slack_sdk.scim.v1.default_arg.DefaultArg object>,
addresses: List[UserAddress | Dict[str, Any]] | DefaultArg | None = <slack_sdk.scim.v1.default_arg.DefaultArg object>,
display_name: str | DefaultArg | None = <slack_sdk.scim.v1.default_arg.DefaultArg object>,
emails: List[TypeAndValue | Dict[str, Any]] | DefaultArg | None = <slack_sdk.scim.v1.default_arg.DefaultArg object>,
external_id: str | DefaultArg | None = <slack_sdk.scim.v1.default_arg.DefaultArg object>,
groups: List[UserGroup | Dict[str, Any]] | DefaultArg | None = <slack_sdk.scim.v1.default_arg.DefaultArg object>,
id: str | DefaultArg | None = <slack_sdk.scim.v1.default_arg.DefaultArg object>,
meta: UserMeta | Dict[str, Any] | DefaultArg | None = <slack_sdk.scim.v1.default_arg.DefaultArg object>,
name: UserName | Dict[str, Any] | DefaultArg | None = <slack_sdk.scim.v1.default_arg.DefaultArg object>,
nick_name: str | DefaultArg | None = <slack_sdk.scim.v1.default_arg.DefaultArg object>,
phone_numbers: List[TypeAndValue | Dict[str, Any]] | DefaultArg | None = <slack_sdk.scim.v1.default_arg.DefaultArg object>,
photos: List[UserPhoto | Dict[str, Any]] | DefaultArg | None = <slack_sdk.scim.v1.default_arg.DefaultArg object>,
profile_url: str | DefaultArg | None = <slack_sdk.scim.v1.default_arg.DefaultArg object>,
roles: List[TypeAndValue | Dict[str, Any]] | DefaultArg | None = <slack_sdk.scim.v1.default_arg.DefaultArg object>,
schemas: List[str] | DefaultArg | None = <slack_sdk.scim.v1.default_arg.DefaultArg object>,
timezone: str | DefaultArg | None = <slack_sdk.scim.v1.default_arg.DefaultArg object>,
title: str | DefaultArg | None = <slack_sdk.scim.v1.default_arg.DefaultArg object>,
user_name: str | DefaultArg | None = <slack_sdk.scim.v1.default_arg.DefaultArg object>,
**kwargs)
+
+
+
+ +Expand source code + +
class User:
+    active: Union[Optional[bool], DefaultArg]
+    addresses: Union[Optional[List[UserAddress]], DefaultArg]
+    display_name: Union[Optional[str], DefaultArg]
+    emails: Union[Optional[List[TypeAndValue]], DefaultArg]
+    external_id: Union[Optional[str], DefaultArg]
+    groups: Union[Optional[List[UserGroup]], DefaultArg]
+    id: Union[Optional[str], DefaultArg]
+    meta: Union[Optional[UserMeta], DefaultArg]
+    name: Union[Optional[UserName], DefaultArg]
+    nick_name: Union[Optional[str], DefaultArg]
+    phone_numbers: Union[Optional[List[TypeAndValue]], DefaultArg]
+    photos: Union[Optional[List[UserPhoto]], DefaultArg]
+    profile_url: Union[Optional[str], DefaultArg]
+    roles: Union[Optional[List[TypeAndValue]], DefaultArg]
+    schemas: Union[Optional[List[str]], DefaultArg]
+    timezone: Union[Optional[str], DefaultArg]
+    title: Union[Optional[str], DefaultArg]
+    user_name: Union[Optional[str], DefaultArg]
+    unknown_fields: Dict[str, Any]
+
+    def __init__(
+        self,
+        *,
+        active: Union[Optional[bool], DefaultArg] = NotGiven,
+        addresses: Union[Optional[List[Union[UserAddress, Dict[str, Any]]]], DefaultArg] = NotGiven,
+        display_name: Union[Optional[str], DefaultArg] = NotGiven,
+        emails: Union[Optional[List[Union[TypeAndValue, Dict[str, Any]]]], DefaultArg] = NotGiven,
+        external_id: Union[Optional[str], DefaultArg] = NotGiven,
+        groups: Union[Optional[List[Union[UserGroup, Dict[str, Any]]]], DefaultArg] = NotGiven,
+        id: Union[Optional[str], DefaultArg] = NotGiven,
+        meta: Union[Optional[Union[UserMeta, Dict[str, Any]]], DefaultArg] = NotGiven,
+        name: Union[Optional[Union[UserName, Dict[str, Any]]], DefaultArg] = NotGiven,
+        nick_name: Union[Optional[str], DefaultArg] = NotGiven,
+        phone_numbers: Union[Optional[List[Union[TypeAndValue, Dict[str, Any]]]], DefaultArg] = NotGiven,
+        photos: Union[Optional[List[Union[UserPhoto, Dict[str, Any]]]], DefaultArg] = NotGiven,
+        profile_url: Union[Optional[str], DefaultArg] = NotGiven,
+        roles: Union[Optional[List[Union[TypeAndValue, Dict[str, Any]]]], DefaultArg] = NotGiven,
+        schemas: Union[Optional[List[str]], DefaultArg] = NotGiven,
+        timezone: Union[Optional[str], DefaultArg] = NotGiven,
+        title: Union[Optional[str], DefaultArg] = NotGiven,
+        user_name: Union[Optional[str], DefaultArg] = NotGiven,
+        **kwargs,
+    ) -> None:
+        self.active = active
+        self.addresses = (
+            [a if isinstance(a, UserAddress) else UserAddress(**a) for a in addresses]  # type: ignore
+            if _is_iterable(addresses)
+            else addresses
+        )
+        self.display_name = display_name
+        self.emails = (
+            [a if isinstance(a, TypeAndValue) else TypeAndValue(**a) for a in emails]  # type: ignore
+            if _is_iterable(emails)
+            else emails
+        )
+        self.external_id = external_id
+        self.groups = (
+            [a if isinstance(a, UserGroup) else UserGroup(**a) for a in groups]  # type: ignore
+            if _is_iterable(groups)
+            else groups
+        )
+        self.id = id
+        self.meta = UserMeta(**meta) if meta is not None and isinstance(meta, dict) else meta
+        self.name = UserName(**name) if name is not None and isinstance(name, dict) else name
+        self.nick_name = nick_name
+        self.phone_numbers = (
+            [a if isinstance(a, TypeAndValue) else TypeAndValue(**a) for a in phone_numbers]  # type: ignore
+            if _is_iterable(phone_numbers)
+            else phone_numbers
+        )
+        self.photos = (
+            [a if isinstance(a, UserPhoto) else UserPhoto(**a) for a in photos]  # type: ignore
+            if _is_iterable(photos)
+            else photos
+        )
+        self.profile_url = profile_url
+        self.roles = (
+            [a if isinstance(a, TypeAndValue) else TypeAndValue(**a) for a in roles]  # type: ignore
+            if _is_iterable(roles)
+            else roles
+        )
+        self.schemas = schemas
+        self.timezone = timezone
+        self.title = title
+        self.user_name = user_name
+
+        self.unknown_fields = kwargs
+
+    def to_dict(self):
+        return _to_dict_without_not_given(self)
+
+    def __repr__(self):
+        return f"<slack_sdk.scim.{self.__class__.__name__}: {self.to_dict()}>"
+
+
+

Class variables

+
+
var active : bool | DefaultArg | None
+
+

The type of the None singleton.

+
+
var addresses : List[UserAddress] | DefaultArg | None
+
+

The type of the None singleton.

+
+
var display_name : str | DefaultArg | None
+
+

The type of the None singleton.

+
+
var emails : List[TypeAndValue] | DefaultArg | None
+
+

The type of the None singleton.

+
+
var external_id : str | DefaultArg | None
+
+

The type of the None singleton.

+
+
var groups : List[UserGroup] | DefaultArg | None
+
+

The type of the None singleton.

+
+
var id : str | DefaultArg | None
+
+

The type of the None singleton.

+
+
var metaUserMeta | DefaultArg | None
+
+

The type of the None singleton.

+
+
var nameUserName | DefaultArg | None
+
+

The type of the None singleton.

+
+
var nick_name : str | DefaultArg | None
+
+

The type of the None singleton.

+
+
var phone_numbers : List[TypeAndValue] | DefaultArg | None
+
+

The type of the None singleton.

+
+
var photos : List[UserPhoto] | DefaultArg | None
+
+

The type of the None singleton.

+
+
var profile_url : str | DefaultArg | None
+
+

The type of the None singleton.

+
+
var roles : List[TypeAndValue] | DefaultArg | None
+
+

The type of the None singleton.

+
+
var schemas : List[str] | DefaultArg | None
+
+

The type of the None singleton.

+
+
var timezone : str | DefaultArg | None
+
+

The type of the None singleton.

+
+
var title : str | DefaultArg | None
+
+

The type of the None singleton.

+
+
var unknown_fields : Dict[str, Any]
+
+

The type of the None singleton.

+
+
var user_name : str | DefaultArg | None
+
+

The type of the None singleton.

+
+
+

Methods

+
+
+def to_dict(self) +
+
+
+ +Expand source code + +
def to_dict(self):
+    return _to_dict_without_not_given(self)
+
+
+
+
+
+
+
+
+ +
+ + + diff --git a/docs/reference/scim/v1/async_client.html b/docs/reference/scim/v1/async_client.html new file mode 100644 index 000000000..6a971bb80 --- /dev/null +++ b/docs/reference/scim/v1/async_client.html @@ -0,0 +1,838 @@ + + + + + + +slack_sdk.scim.v1.async_client API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.scim.v1.async_client

+
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class AsyncSCIMClient +(token: str,
timeout: int = 30,
ssl: ssl.SSLContext | None = None,
proxy: str | None = None,
base_url: str = 'https://api.slack.com/scim/v1/',
session: aiohttp.client.ClientSession | None = None,
trust_env_in_session: bool = False,
auth: aiohttp.helpers.BasicAuth | None = None,
default_headers: Dict[str, str] | None = None,
user_agent_prefix: str | None = None,
user_agent_suffix: str | None = None,
logger: logging.Logger | None = None,
retry_handlers: List[slack_sdk.http_retry.async_handler.AsyncRetryHandler] | None = None)
+
+
+
+ +Expand source code + +
class AsyncSCIMClient:
+    BASE_URL = "https://api.slack.com/scim/v1/"
+
+    token: str
+    timeout: int
+    ssl: Optional[SSLContext]
+    proxy: Optional[str]
+    base_url: str
+    session: Optional[ClientSession]
+    trust_env_in_session: bool
+    auth: Optional[BasicAuth]
+    default_headers: Dict[str, str]
+    logger: logging.Logger
+    retry_handlers: List[AsyncRetryHandler]
+
+    def __init__(
+        self,
+        token: str,
+        timeout: int = 30,
+        ssl: Optional[SSLContext] = None,
+        proxy: Optional[str] = None,
+        base_url: str = BASE_URL,
+        session: Optional[ClientSession] = None,
+        trust_env_in_session: bool = False,
+        auth: Optional[BasicAuth] = None,
+        default_headers: Optional[Dict[str, str]] = None,
+        user_agent_prefix: Optional[str] = None,
+        user_agent_suffix: Optional[str] = None,
+        logger: Optional[logging.Logger] = None,
+        retry_handlers: Optional[List[AsyncRetryHandler]] = None,
+    ):
+        """API client for SCIM API
+        See https://docs.slack.dev/admins/scim-api/ for more details
+
+        Args:
+            token: An admin user's token, which starts with `xoxp-`
+            timeout: Request timeout (in seconds)
+            ssl: `ssl.SSLContext` to use for requests
+            proxy: Proxy URL (e.g., `localhost:9000`, `http://localhost:9000`)
+            base_url: The base URL for API calls
+            session: `aiohttp.ClientSession` instance
+            trust_env_in_session: True/False for `aiohttp.ClientSession`
+            auth: Basic auth info for `aiohttp.ClientSession`
+            default_headers: Request headers to add to all requests
+            user_agent_prefix: Prefix for User-Agent header value
+            user_agent_suffix: Suffix for User-Agent header value
+            logger: Custom logger
+            retry_handlers: Retry handlers
+        """
+        self.token = token
+        self.timeout = timeout
+        self.ssl = ssl
+        self.proxy = proxy
+        self.base_url = base_url
+        self.session = session
+        self.trust_env_in_session = trust_env_in_session
+        self.auth = auth
+        self.default_headers = default_headers if default_headers else {}
+        self.default_headers["User-Agent"] = get_user_agent(user_agent_prefix, user_agent_suffix)
+        self.logger = logger if logger is not None else logging.getLogger(__name__)
+        self.retry_handlers = retry_handlers if retry_handlers is not None else async_default_handlers()
+
+        if self.proxy is None or len(self.proxy.strip()) == 0:
+            env_variable = load_http_proxy_from_env(self.logger)
+            if env_variable is not None:
+                self.proxy = env_variable
+
+    # -------------------------
+    # Users
+    # -------------------------
+
+    async def search_users(
+        self,
+        *,
+        # Pagination required as of August 30, 2019.
+        count: int,
+        start_index: int,
+        filter: Optional[str] = None,
+    ) -> SearchUsersResponse:
+        return SearchUsersResponse(
+            await self.api_call(
+                http_verb="GET",
+                path="Users",
+                query_params={
+                    "filter": filter,
+                    "count": count,
+                    "startIndex": start_index,
+                },
+            )
+        )
+
+    async def read_user(self, id: str) -> ReadUserResponse:
+        return ReadUserResponse(await self.api_call(http_verb="GET", path=f"Users/{quote(id)}"))
+
+    async def create_user(self, user: Union[Dict[str, Any], User]) -> UserCreateResponse:
+        return UserCreateResponse(
+            await self.api_call(
+                http_verb="POST",
+                path="Users",
+                body_params=user.to_dict() if isinstance(user, User) else _to_dict_without_not_given(user),
+            )
+        )
+
+    async def patch_user(self, id: str, partial_user: Union[Dict[str, Any], User]) -> UserPatchResponse:
+        return UserPatchResponse(
+            await self.api_call(
+                http_verb="PATCH",
+                path=f"Users/{quote(id)}",
+                body_params=(
+                    partial_user.to_dict() if isinstance(partial_user, User) else _to_dict_without_not_given(partial_user)
+                ),
+            )
+        )
+
+    async def update_user(self, user: Union[Dict[str, Any], User]) -> UserUpdateResponse:
+        user_id = user.id if isinstance(user, User) else user["id"]
+        return UserUpdateResponse(
+            await self.api_call(
+                http_verb="PUT",
+                path=f"Users/{quote(user_id)}",
+                body_params=user.to_dict() if isinstance(user, User) else _to_dict_without_not_given(user),
+            )
+        )
+
+    async def delete_user(self, id: str) -> UserDeleteResponse:
+        return UserDeleteResponse(
+            await self.api_call(
+                http_verb="DELETE",
+                path=f"Users/{quote(id)}",
+            )
+        )
+
+    # -------------------------
+    # Groups
+    # -------------------------
+
+    async def search_groups(
+        self,
+        *,
+        # Pagination required as of August 30, 2019.
+        count: int,
+        start_index: int,
+        filter: Optional[str] = None,
+    ) -> SearchGroupsResponse:
+        return SearchGroupsResponse(
+            await self.api_call(
+                http_verb="GET",
+                path="Groups",
+                query_params={
+                    "filter": filter,
+                    "count": count,
+                    "startIndex": start_index,
+                },
+            )
+        )
+
+    async def read_group(self, id: str) -> ReadGroupResponse:
+        return ReadGroupResponse(await self.api_call(http_verb="GET", path=f"Groups/{quote(id)}"))
+
+    async def create_group(self, group: Union[Dict[str, Any], Group]) -> GroupCreateResponse:
+        return GroupCreateResponse(
+            await self.api_call(
+                http_verb="POST",
+                path="Groups",
+                body_params=group.to_dict() if isinstance(group, Group) else _to_dict_without_not_given(group),
+            )
+        )
+
+    async def patch_group(self, id: str, partial_group: Union[Dict[str, Any], Group]) -> GroupPatchResponse:
+        return GroupPatchResponse(
+            await self.api_call(
+                http_verb="PATCH",
+                path=f"Groups/{quote(id)}",
+                body_params=(
+                    partial_group.to_dict()
+                    if isinstance(partial_group, Group)
+                    else _to_dict_without_not_given(partial_group)
+                ),
+            )
+        )
+
+    async def update_group(self, group: Union[Dict[str, Any], Group]) -> GroupUpdateResponse:
+        group_id = group.id if isinstance(group, Group) else group["id"]
+        return GroupUpdateResponse(
+            await self.api_call(
+                http_verb="PUT",
+                path=f"Groups/{quote(group_id)}",
+                body_params=group.to_dict() if isinstance(group, Group) else _to_dict_without_not_given(group),
+            )
+        )
+
+    async def delete_group(self, id: str) -> GroupDeleteResponse:
+        return GroupDeleteResponse(
+            await self.api_call(
+                http_verb="DELETE",
+                path=f"Groups/{quote(id)}",
+            )
+        )
+
+    # -------------------------
+
+    async def api_call(
+        self,
+        *,
+        http_verb: str,
+        path: str,
+        query_params: Optional[Dict[str, Any]] = None,
+        body_params: Optional[Dict[str, Any]] = None,
+        headers: Optional[Dict[str, str]] = None,
+    ) -> SCIMResponse:
+        url = f"{self.base_url}{path}"
+        query = _build_query(query_params)
+        if len(query) > 0:
+            url += f"?{query}"
+        return await self._perform_http_request(
+            http_verb=http_verb,
+            url=url,
+            body_params=body_params,
+            headers=_build_request_headers(
+                token=self.token,
+                default_headers=self.default_headers,
+                additional_headers=headers,
+            ),
+        )
+
+    async def _perform_http_request(
+        self,
+        *,
+        http_verb: str,
+        url: str,
+        body_params: Optional[Dict[str, Any]],
+        headers: Dict[str, str],
+    ) -> SCIMResponse:
+        if body_params is not None:
+            if body_params.get("schemas") is None:
+                body_params["schemas"] = ["urn:scim:schemas:core:1.0"]
+            body_params = json.dumps(body_params)
+        headers["Content-Type"] = "application/json;charset=utf-8"
+
+        session: Optional[ClientSession] = None
+        use_running_session = self.session and not self.session.closed
+        if use_running_session:
+            session = self.session
+        else:
+            session = aiohttp.ClientSession(
+                timeout=aiohttp.ClientTimeout(total=self.timeout),
+                auth=self.auth,
+                trust_env=self.trust_env_in_session,
+            )
+
+        last_error: Optional[Exception] = None
+        resp: Optional[SCIMResponse] = None
+        try:
+            request_kwargs = {
+                "headers": headers,
+                "data": body_params,
+                "ssl": self.ssl,
+                "proxy": self.proxy,
+            }
+            retry_request = RetryHttpRequest(
+                method=http_verb,
+                url=url,
+                headers=headers,
+                body_params=body_params,
+            )
+
+            retry_state = RetryState()
+            counter_for_safety = 0
+            while counter_for_safety < 100:
+                counter_for_safety += 1
+                # If this is a retry, the next try started here. We can reset the flag.
+                retry_state.next_attempt_requested = False
+                retry_response: Optional[RetryHttpResponse] = None
+                response_body = ""
+
+                if self.logger.level <= logging.DEBUG:
+                    headers_for_logging = {
+                        k: "(redacted)" if k.lower() == "authorization" else v for k, v in headers.items()
+                    }
+                    self.logger.debug(
+                        f"Sending a request - url: {url}, params: {body_params}, headers: {headers_for_logging}"
+                    )
+
+                try:
+                    async with session.request(http_verb, url, **request_kwargs) as res:
+                        try:
+                            response_body = await res.text()
+                            retry_response = RetryHttpResponse(
+                                status_code=res.status,
+                                headers=res.headers,
+                                data=response_body.encode("utf-8") if response_body is not None else None,
+                            )
+                        except aiohttp.ContentTypeError:
+                            self.logger.debug(f"No response data returned from the following API call: {url}.")
+                            retry_response = RetryHttpResponse(
+                                status_code=res.status,
+                                headers=res.headers,
+                            )
+
+                        if res.status == 429:
+                            for handler in self.retry_handlers:
+                                if await handler.can_retry_async(
+                                    state=retry_state,
+                                    request=retry_request,
+                                    response=retry_response,
+                                ):
+                                    if self.logger.level <= logging.DEBUG:
+                                        self.logger.info(
+                                            f"A retry handler found: {type(handler).__name__} "
+                                            f"for {http_verb} {url} - rate_limited"
+                                        )
+                                    await handler.prepare_for_next_attempt_async(
+                                        state=retry_state,
+                                        request=retry_request,
+                                        response=retry_response,
+                                    )
+                                    break
+
+                        if retry_state.next_attempt_requested is False:
+                            resp = SCIMResponse(
+                                url=url,
+                                status_code=res.status,
+                                raw_body=response_body,
+                                headers=res.headers,
+                            )
+                            _debug_log_response(self.logger, resp)
+                            return resp
+
+                except Exception as e:
+                    last_error = e
+                    for handler in self.retry_handlers:
+                        if await handler.can_retry_async(
+                            state=retry_state,
+                            request=retry_request,
+                            response=retry_response,
+                            error=e,
+                        ):
+                            if self.logger.level <= logging.DEBUG:
+                                self.logger.info(
+                                    f"A retry handler found: {type(handler).__name__} " f"for {http_verb} {url} - {e}"
+                                )
+                            await handler.prepare_for_next_attempt_async(
+                                state=retry_state,
+                                request=retry_request,
+                                response=retry_response,
+                                error=e,
+                            )
+                            break
+
+                    if retry_state.next_attempt_requested is False:
+                        raise last_error
+
+            if resp is not None:
+                return resp
+            raise last_error
+
+        finally:
+            if not use_running_session:
+                await session.close()
+
+        return resp
+
+

API client for SCIM API +See https://docs.slack.dev/admins/scim-api/ for more details

+

Args

+
+
token
+
An admin user's token, which starts with xoxp-
+
timeout
+
Request timeout (in seconds)
+
ssl
+
ssl.SSLContext to use for requests
+
proxy
+
Proxy URL (e.g., localhost:9000, http://localhost:9000)
+
base_url
+
The base URL for API calls
+
session
+
aiohttp.ClientSession instance
+
trust_env_in_session
+
True/False for aiohttp.ClientSession
+
auth
+
Basic auth info for aiohttp.ClientSession
+
default_headers
+
Request headers to add to all requests
+
user_agent_prefix
+
Prefix for User-Agent header value
+
user_agent_suffix
+
Suffix for User-Agent header value
+
logger
+
Custom logger
+
retry_handlers
+
Retry handlers
+
+

Class variables

+
+
var BASE_URL
+
+

The type of the None singleton.

+
+
var auth : aiohttp.helpers.BasicAuth | None
+
+

The type of the None singleton.

+
+
var base_url : str
+
+

The type of the None singleton.

+
+
var default_headers : Dict[str, str]
+
+

The type of the None singleton.

+
+
var logger : logging.Logger
+
+

The type of the None singleton.

+
+
var proxy : str | None
+
+

The type of the None singleton.

+
+
var retry_handlers : List[slack_sdk.http_retry.async_handler.AsyncRetryHandler]
+
+

The type of the None singleton.

+
+
var session : aiohttp.client.ClientSession | None
+
+

The type of the None singleton.

+
+
var ssl : ssl.SSLContext | None
+
+

The type of the None singleton.

+
+
var timeout : int
+
+

The type of the None singleton.

+
+
var token : str
+
+

The type of the None singleton.

+
+
var trust_env_in_session : bool
+
+

The type of the None singleton.

+
+
+

Methods

+
+
+async def api_call(self,
*,
http_verb: str,
path: str,
query_params: Dict[str, Any] | None = None,
body_params: Dict[str, Any] | None = None,
headers: Dict[str, str] | None = None) ‑> SCIMResponse
+
+
+
+ +Expand source code + +
async def api_call(
+    self,
+    *,
+    http_verb: str,
+    path: str,
+    query_params: Optional[Dict[str, Any]] = None,
+    body_params: Optional[Dict[str, Any]] = None,
+    headers: Optional[Dict[str, str]] = None,
+) -> SCIMResponse:
+    url = f"{self.base_url}{path}"
+    query = _build_query(query_params)
+    if len(query) > 0:
+        url += f"?{query}"
+    return await self._perform_http_request(
+        http_verb=http_verb,
+        url=url,
+        body_params=body_params,
+        headers=_build_request_headers(
+            token=self.token,
+            default_headers=self.default_headers,
+            additional_headers=headers,
+        ),
+    )
+
+
+
+
+async def create_group(self,
group: Dict[str, Any] | Group) ‑> GroupCreateResponse
+
+
+
+ +Expand source code + +
async def create_group(self, group: Union[Dict[str, Any], Group]) -> GroupCreateResponse:
+    return GroupCreateResponse(
+        await self.api_call(
+            http_verb="POST",
+            path="Groups",
+            body_params=group.to_dict() if isinstance(group, Group) else _to_dict_without_not_given(group),
+        )
+    )
+
+
+
+
+async def create_user(self,
user: Dict[str, Any] | User) ‑> UserCreateResponse
+
+
+
+ +Expand source code + +
async def create_user(self, user: Union[Dict[str, Any], User]) -> UserCreateResponse:
+    return UserCreateResponse(
+        await self.api_call(
+            http_verb="POST",
+            path="Users",
+            body_params=user.to_dict() if isinstance(user, User) else _to_dict_without_not_given(user),
+        )
+    )
+
+
+
+
+async def delete_group(self, id: str) ‑> GroupDeleteResponse +
+
+
+ +Expand source code + +
async def delete_group(self, id: str) -> GroupDeleteResponse:
+    return GroupDeleteResponse(
+        await self.api_call(
+            http_verb="DELETE",
+            path=f"Groups/{quote(id)}",
+        )
+    )
+
+
+
+
+async def delete_user(self, id: str) ‑> UserDeleteResponse +
+
+
+ +Expand source code + +
async def delete_user(self, id: str) -> UserDeleteResponse:
+    return UserDeleteResponse(
+        await self.api_call(
+            http_verb="DELETE",
+            path=f"Users/{quote(id)}",
+        )
+    )
+
+
+
+
+async def patch_group(self,
id: str,
partial_group: Dict[str, Any] | Group) ‑> GroupPatchResponse
+
+
+
+ +Expand source code + +
async def patch_group(self, id: str, partial_group: Union[Dict[str, Any], Group]) -> GroupPatchResponse:
+    return GroupPatchResponse(
+        await self.api_call(
+            http_verb="PATCH",
+            path=f"Groups/{quote(id)}",
+            body_params=(
+                partial_group.to_dict()
+                if isinstance(partial_group, Group)
+                else _to_dict_without_not_given(partial_group)
+            ),
+        )
+    )
+
+
+
+
+async def patch_user(self,
id: str,
partial_user: Dict[str, Any] | User) ‑> UserPatchResponse
+
+
+
+ +Expand source code + +
async def patch_user(self, id: str, partial_user: Union[Dict[str, Any], User]) -> UserPatchResponse:
+    return UserPatchResponse(
+        await self.api_call(
+            http_verb="PATCH",
+            path=f"Users/{quote(id)}",
+            body_params=(
+                partial_user.to_dict() if isinstance(partial_user, User) else _to_dict_without_not_given(partial_user)
+            ),
+        )
+    )
+
+
+
+
+async def read_group(self, id: str) ‑> ReadGroupResponse +
+
+
+ +Expand source code + +
async def read_group(self, id: str) -> ReadGroupResponse:
+    return ReadGroupResponse(await self.api_call(http_verb="GET", path=f"Groups/{quote(id)}"))
+
+
+
+
+async def read_user(self, id: str) ‑> ReadUserResponse +
+
+
+ +Expand source code + +
async def read_user(self, id: str) -> ReadUserResponse:
+    return ReadUserResponse(await self.api_call(http_verb="GET", path=f"Users/{quote(id)}"))
+
+
+
+
+async def search_groups(self, *, count: int, start_index: int, filter: str | None = None) ‑> SearchGroupsResponse +
+
+
+ +Expand source code + +
async def search_groups(
+    self,
+    *,
+    # Pagination required as of August 30, 2019.
+    count: int,
+    start_index: int,
+    filter: Optional[str] = None,
+) -> SearchGroupsResponse:
+    return SearchGroupsResponse(
+        await self.api_call(
+            http_verb="GET",
+            path="Groups",
+            query_params={
+                "filter": filter,
+                "count": count,
+                "startIndex": start_index,
+            },
+        )
+    )
+
+
+
+
+async def search_users(self, *, count: int, start_index: int, filter: str | None = None) ‑> SearchUsersResponse +
+
+
+ +Expand source code + +
async def search_users(
+    self,
+    *,
+    # Pagination required as of August 30, 2019.
+    count: int,
+    start_index: int,
+    filter: Optional[str] = None,
+) -> SearchUsersResponse:
+    return SearchUsersResponse(
+        await self.api_call(
+            http_verb="GET",
+            path="Users",
+            query_params={
+                "filter": filter,
+                "count": count,
+                "startIndex": start_index,
+            },
+        )
+    )
+
+
+
+
+async def update_group(self,
group: Dict[str, Any] | Group) ‑> GroupUpdateResponse
+
+
+
+ +Expand source code + +
async def update_group(self, group: Union[Dict[str, Any], Group]) -> GroupUpdateResponse:
+    group_id = group.id if isinstance(group, Group) else group["id"]
+    return GroupUpdateResponse(
+        await self.api_call(
+            http_verb="PUT",
+            path=f"Groups/{quote(group_id)}",
+            body_params=group.to_dict() if isinstance(group, Group) else _to_dict_without_not_given(group),
+        )
+    )
+
+
+
+
+async def update_user(self,
user: Dict[str, Any] | User) ‑> UserUpdateResponse
+
+
+
+ +Expand source code + +
async def update_user(self, user: Union[Dict[str, Any], User]) -> UserUpdateResponse:
+    user_id = user.id if isinstance(user, User) else user["id"]
+    return UserUpdateResponse(
+        await self.api_call(
+            http_verb="PUT",
+            path=f"Users/{quote(user_id)}",
+            body_params=user.to_dict() if isinstance(user, User) else _to_dict_without_not_given(user),
+        )
+    )
+
+
+
+
+
+
+
+
+ +
+ + + diff --git a/docs/reference/scim/v1/client.html b/docs/reference/scim/v1/client.html new file mode 100644 index 000000000..4317f2394 --- /dev/null +++ b/docs/reference/scim/v1/client.html @@ -0,0 +1,832 @@ + + + + + + +slack_sdk.scim.v1.client API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.scim.v1.client

+
+
+

SCIM API is a set of APIs for provisioning and managing user accounts and groups. +SCIM is used by Single Sign-On (SSO) services and identity providers to manage people across a variety of tools, +including Slack.

+

Refer to https://docs.slack.dev/tools/python-slack-sdk/scim/ for details.

+
+
+
+
+
+
+
+
+

Classes

+
+
+class SCIMClient +(token: str,
timeout: int = 30,
ssl: ssl.SSLContext | None = None,
proxy: str | None = None,
base_url: str = 'https://api.slack.com/scim/v1/',
default_headers: Dict[str, str] | None = None,
user_agent_prefix: str | None = None,
user_agent_suffix: str | None = None,
logger: logging.Logger | None = None,
retry_handlers: List[RetryHandler] | None = None)
+
+
+
+ +Expand source code + +
class SCIMClient:
+    BASE_URL = "https://api.slack.com/scim/v1/"
+
+    token: str
+    timeout: int
+    ssl: Optional[SSLContext]
+    proxy: Optional[str]
+    base_url: str
+    default_headers: Dict[str, str]
+    logger: logging.Logger
+    retry_handlers: List[RetryHandler]
+
+    def __init__(
+        self,
+        token: str,
+        timeout: int = 30,
+        ssl: Optional[SSLContext] = None,
+        proxy: Optional[str] = None,
+        base_url: str = BASE_URL,
+        default_headers: Optional[Dict[str, str]] = None,
+        user_agent_prefix: Optional[str] = None,
+        user_agent_suffix: Optional[str] = None,
+        logger: Optional[logging.Logger] = None,
+        retry_handlers: Optional[List[RetryHandler]] = None,
+    ):
+        """API client for SCIM API
+        See https://docs.slack.dev/admins/scim-api/ for more details
+
+        Args:
+            token: An admin user's token, which starts with `xoxp-`
+            timeout: Request timeout (in seconds)
+            ssl: `ssl.SSLContext` to use for requests
+            proxy: Proxy URL (e.g., `localhost:9000`, `http://localhost:9000`)
+            base_url: The base URL for API calls
+            default_headers: Request headers to add to all requests
+            user_agent_prefix: Prefix for User-Agent header value
+            user_agent_suffix: Suffix for User-Agent header value
+            logger: Custom logger
+            retry_handlers: Retry handlers
+        """
+        self.token = token
+        self.timeout = timeout
+        self.ssl = ssl
+        self.proxy = proxy
+        self.base_url = base_url
+        self.default_headers = default_headers if default_headers else {}
+        self.default_headers["User-Agent"] = get_user_agent(user_agent_prefix, user_agent_suffix)
+        self.logger = logger if logger is not None else logging.getLogger(__name__)
+        self.retry_handlers = retry_handlers if retry_handlers is not None else default_retry_handlers()
+
+        if self.proxy is None or len(self.proxy.strip()) == 0:
+            env_variable = load_http_proxy_from_env(self.logger)
+            if env_variable is not None:
+                self.proxy = env_variable
+
+    # -------------------------
+    # Users
+    # -------------------------
+
+    def search_users(
+        self,
+        *,
+        # Pagination required as of August 30, 2019.
+        count: int,
+        start_index: int,
+        filter: Optional[str] = None,
+    ) -> SearchUsersResponse:
+        return SearchUsersResponse(
+            self.api_call(
+                http_verb="GET",
+                path="Users",
+                query_params={
+                    "filter": filter,
+                    "count": count,
+                    "startIndex": start_index,
+                },
+            )
+        )
+
+    def read_user(self, id: str) -> ReadUserResponse:
+        return ReadUserResponse(self.api_call(http_verb="GET", path=f"Users/{quote(id)}"))
+
+    def create_user(self, user: Union[Dict[str, Any], User]) -> UserCreateResponse:
+        return UserCreateResponse(
+            self.api_call(
+                http_verb="POST",
+                path="Users",
+                body_params=user.to_dict() if isinstance(user, User) else _to_dict_without_not_given(user),
+            )
+        )
+
+    def patch_user(self, id: str, partial_user: Union[Dict[str, Any], User]) -> UserPatchResponse:
+        return UserPatchResponse(
+            self.api_call(
+                http_verb="PATCH",
+                path=f"Users/{quote(id)}",
+                body_params=(
+                    partial_user.to_dict() if isinstance(partial_user, User) else _to_dict_without_not_given(partial_user)
+                ),
+            )
+        )
+
+    def update_user(self, user: Union[Dict[str, Any], User]) -> UserUpdateResponse:
+        user_id = user.id if isinstance(user, User) else user["id"]
+        return UserUpdateResponse(
+            self.api_call(
+                http_verb="PUT",
+                path=f"Users/{quote(user_id)}",
+                body_params=user.to_dict() if isinstance(user, User) else _to_dict_without_not_given(user),
+            )
+        )
+
+    def delete_user(self, id: str) -> UserDeleteResponse:
+        return UserDeleteResponse(
+            self.api_call(
+                http_verb="DELETE",
+                path=f"Users/{quote(id)}",
+            )
+        )
+
+    # -------------------------
+    # Groups
+    # -------------------------
+
+    def search_groups(
+        self,
+        *,
+        # Pagination required as of August 30, 2019.
+        count: int,
+        start_index: int,
+        filter: Optional[str] = None,
+    ) -> SearchGroupsResponse:
+        return SearchGroupsResponse(
+            self.api_call(
+                http_verb="GET",
+                path="Groups",
+                query_params={
+                    "filter": filter,
+                    "count": count,
+                    "startIndex": start_index,
+                },
+            )
+        )
+
+    def read_group(self, id: str) -> ReadGroupResponse:
+        return ReadGroupResponse(self.api_call(http_verb="GET", path=f"Groups/{quote(id)}"))
+
+    def create_group(self, group: Union[Dict[str, Any], Group]) -> GroupCreateResponse:
+        return GroupCreateResponse(
+            self.api_call(
+                http_verb="POST",
+                path="Groups",
+                body_params=group.to_dict() if isinstance(group, Group) else _to_dict_without_not_given(group),
+            )
+        )
+
+    def patch_group(self, id: str, partial_group: Union[Dict[str, Any], Group]) -> GroupPatchResponse:
+        return GroupPatchResponse(
+            self.api_call(
+                http_verb="PATCH",
+                path=f"Groups/{quote(id)}",
+                body_params=(
+                    partial_group.to_dict()
+                    if isinstance(partial_group, Group)
+                    else _to_dict_without_not_given(partial_group)
+                ),
+            )
+        )
+
+    def update_group(self, group: Union[Dict[str, Any], Group]) -> GroupUpdateResponse:
+        group_id = group.id if isinstance(group, Group) else group["id"]
+        return GroupUpdateResponse(
+            self.api_call(
+                http_verb="PUT",
+                path=f"Groups/{quote(group_id)}",
+                body_params=group.to_dict() if isinstance(group, Group) else _to_dict_without_not_given(group),
+            )
+        )
+
+    def delete_group(self, id: str) -> GroupDeleteResponse:
+        return GroupDeleteResponse(
+            self.api_call(
+                http_verb="DELETE",
+                path=f"Groups/{quote(id)}",
+            )
+        )
+
+    # -------------------------
+
+    def api_call(
+        self,
+        *,
+        http_verb: str,
+        path: str,
+        query_params: Optional[Dict[str, Any]] = None,
+        body_params: Optional[Dict[str, Any]] = None,
+        headers: Optional[Dict[str, str]] = None,
+    ) -> SCIMResponse:
+        """Performs a Slack API request and returns the result."""
+        url = f"{self.base_url}{path}"
+        query = _build_query(query_params)
+        if len(query) > 0:
+            url += f"?{query}"
+
+        return self._perform_http_request(
+            http_verb=http_verb,
+            url=url,
+            body=body_params,
+            headers=_build_request_headers(
+                token=self.token,
+                default_headers=self.default_headers,
+                additional_headers=headers,
+            ),
+        )
+
+    def _perform_http_request(
+        self,
+        *,
+        http_verb: str = "GET",
+        url: str,
+        body: Optional[Dict[str, Any]] = None,
+        headers: Dict[str, str],
+    ) -> SCIMResponse:
+        if body is not None:
+            if body.get("schemas") is None:
+                body["schemas"] = ["urn:scim:schemas:core:1.0"]
+            body = json.dumps(body)
+        headers["Content-Type"] = "application/json;charset=utf-8"
+
+        if self.logger.level <= logging.DEBUG:
+            headers_for_logging = {k: "(redacted)" if k.lower() == "authorization" else v for k, v in headers.items()}
+            self.logger.debug(f"Sending a request - {http_verb} url: {url}, body: {body}, headers: {headers_for_logging}")
+
+        # NOTE: Intentionally ignore the `http_verb` here
+        # Slack APIs accepts any API method requests with POST methods
+        req = Request(
+            method=http_verb,
+            url=url,
+            data=body.encode("utf-8") if body is not None else None,
+            headers=headers,
+        )
+        resp = None
+        last_error = None
+
+        retry_state = RetryState()
+        counter_for_safety = 0
+        while counter_for_safety < 100:
+            counter_for_safety += 1
+            # If this is a retry, the next try started here. We can reset the flag.
+            retry_state.next_attempt_requested = False
+
+            try:
+                resp = self._perform_http_request_internal(url, req)
+                # The resp is a 200 OK response
+                return resp
+
+            except HTTPError as e:
+                # read the response body here
+                charset = e.headers.get_content_charset() or "utf-8"
+                response_body: str = e.read().decode(charset)
+                # As adding new values to HTTPError#headers can be ignored, building a new dict object here
+                response_headers = dict(e.headers.items())
+                resp = SCIMResponse(
+                    url=url,
+                    status_code=e.code,
+                    raw_body=response_body,
+                    headers=response_headers,
+                )
+                if e.code == 429:
+                    # for backward-compatibility with WebClient (v.2.5.0 or older)
+                    if "retry-after" not in resp.headers and "Retry-After" in resp.headers:
+                        resp.headers["retry-after"] = resp.headers["Retry-After"]
+                    if "Retry-After" not in resp.headers and "retry-after" in resp.headers:
+                        resp.headers["Retry-After"] = resp.headers["retry-after"]
+                _debug_log_response(self.logger, resp)
+
+                # Try to find a retry handler for this error
+                retry_request = RetryHttpRequest.from_urllib_http_request(req)
+                retry_response = RetryHttpResponse(
+                    status_code=e.code,
+                    headers={k: [v] for k, v in e.headers.items()},
+                    data=response_body.encode("utf-8") if response_body is not None else None,
+                )
+                for handler in self.retry_handlers:
+                    if handler.can_retry(
+                        state=retry_state,
+                        request=retry_request,
+                        response=retry_response,
+                        error=e,
+                    ):
+                        if self.logger.level <= logging.DEBUG:
+                            self.logger.info(
+                                f"A retry handler found: {type(handler).__name__} for {req.method} {req.full_url} - {e}"
+                            )
+                        handler.prepare_for_next_attempt(
+                            state=retry_state,
+                            request=retry_request,
+                            response=retry_response,
+                            error=e,
+                        )
+                        break
+
+                if retry_state.next_attempt_requested is False:
+                    return resp
+
+            except Exception as err:
+                last_error = err
+                self.logger.error(f"Failed to send a request to Slack API server: {err}")
+
+                # Try to find a retry handler for this error
+                retry_request = RetryHttpRequest.from_urllib_http_request(req)
+                for handler in self.retry_handlers:
+                    if handler.can_retry(
+                        state=retry_state,
+                        request=retry_request,
+                        response=None,
+                        error=err,
+                    ):
+                        if self.logger.level <= logging.DEBUG:
+                            self.logger.info(
+                                f"A retry handler found: {type(handler).__name__} for {req.method} {req.full_url} - {err}"
+                            )
+                        handler.prepare_for_next_attempt(
+                            state=retry_state,
+                            request=retry_request,
+                            response=None,
+                            error=err,
+                        )
+                        self.logger.info(f"Going to retry the same request: {req.method} {req.full_url}")
+                        break
+
+                if retry_state.next_attempt_requested is False:
+                    raise err
+
+        if resp is not None:
+            return resp
+        raise last_error
+
+    def _perform_http_request_internal(self, url: str, req: Request) -> SCIMResponse:
+        opener: Optional[OpenerDirector] = None
+        # for security (BAN-B310)
+        if url.lower().startswith("http"):
+            if self.proxy is not None:
+                if isinstance(self.proxy, str):
+                    opener = urllib.request.build_opener(
+                        ProxyHandler({"http": self.proxy, "https": self.proxy}),
+                        HTTPSHandler(context=self.ssl),
+                    )
+                else:
+                    raise SlackRequestError(f"Invalid proxy detected: {self.proxy} must be a str value")
+        else:
+            raise SlackRequestError(f"Invalid URL detected: {url}")
+
+        # NOTE: BAN-B310 is already checked above
+        http_resp: Optional[HTTPResponse] = None
+        if opener:
+            http_resp = opener.open(req, timeout=self.timeout)
+        else:
+            http_resp = urlopen(req, context=self.ssl, timeout=self.timeout)
+        charset: str = http_resp.headers.get_content_charset() or "utf-8"
+        response_body: str = http_resp.read().decode(charset)
+        resp = SCIMResponse(
+            url=url,
+            status_code=http_resp.status,
+            raw_body=response_body,
+            headers=http_resp.headers,
+        )
+        _debug_log_response(self.logger, resp)
+        return resp
+
+

API client for SCIM API +See https://docs.slack.dev/admins/scim-api/ for more details

+

Args

+
+
token
+
An admin user's token, which starts with xoxp-
+
timeout
+
Request timeout (in seconds)
+
ssl
+
ssl.SSLContext to use for requests
+
proxy
+
Proxy URL (e.g., localhost:9000, http://localhost:9000)
+
base_url
+
The base URL for API calls
+
default_headers
+
Request headers to add to all requests
+
user_agent_prefix
+
Prefix for User-Agent header value
+
user_agent_suffix
+
Suffix for User-Agent header value
+
logger
+
Custom logger
+
retry_handlers
+
Retry handlers
+
+

Class variables

+
+
var BASE_URL
+
+

The type of the None singleton.

+
+
var base_url : str
+
+

The type of the None singleton.

+
+
var default_headers : Dict[str, str]
+
+

The type of the None singleton.

+
+
var logger : logging.Logger
+
+

The type of the None singleton.

+
+
var proxy : str | None
+
+

The type of the None singleton.

+
+
var retry_handlers : List[RetryHandler]
+
+

The type of the None singleton.

+
+
var ssl : ssl.SSLContext | None
+
+

The type of the None singleton.

+
+
var timeout : int
+
+

The type of the None singleton.

+
+
var token : str
+
+

The type of the None singleton.

+
+
+

Methods

+
+
+def api_call(self,
*,
http_verb: str,
path: str,
query_params: Dict[str, Any] | None = None,
body_params: Dict[str, Any] | None = None,
headers: Dict[str, str] | None = None) ‑> SCIMResponse
+
+
+
+ +Expand source code + +
def api_call(
+    self,
+    *,
+    http_verb: str,
+    path: str,
+    query_params: Optional[Dict[str, Any]] = None,
+    body_params: Optional[Dict[str, Any]] = None,
+    headers: Optional[Dict[str, str]] = None,
+) -> SCIMResponse:
+    """Performs a Slack API request and returns the result."""
+    url = f"{self.base_url}{path}"
+    query = _build_query(query_params)
+    if len(query) > 0:
+        url += f"?{query}"
+
+    return self._perform_http_request(
+        http_verb=http_verb,
+        url=url,
+        body=body_params,
+        headers=_build_request_headers(
+            token=self.token,
+            default_headers=self.default_headers,
+            additional_headers=headers,
+        ),
+    )
+
+

Performs a Slack API request and returns the result.

+
+
+def create_group(self,
group: Dict[str, Any] | Group) ‑> GroupCreateResponse
+
+
+
+ +Expand source code + +
def create_group(self, group: Union[Dict[str, Any], Group]) -> GroupCreateResponse:
+    return GroupCreateResponse(
+        self.api_call(
+            http_verb="POST",
+            path="Groups",
+            body_params=group.to_dict() if isinstance(group, Group) else _to_dict_without_not_given(group),
+        )
+    )
+
+
+
+
+def create_user(self,
user: Dict[str, Any] | User) ‑> UserCreateResponse
+
+
+
+ +Expand source code + +
def create_user(self, user: Union[Dict[str, Any], User]) -> UserCreateResponse:
+    return UserCreateResponse(
+        self.api_call(
+            http_verb="POST",
+            path="Users",
+            body_params=user.to_dict() if isinstance(user, User) else _to_dict_without_not_given(user),
+        )
+    )
+
+
+
+
+def delete_group(self, id: str) ‑> GroupDeleteResponse +
+
+
+ +Expand source code + +
def delete_group(self, id: str) -> GroupDeleteResponse:
+    return GroupDeleteResponse(
+        self.api_call(
+            http_verb="DELETE",
+            path=f"Groups/{quote(id)}",
+        )
+    )
+
+
+
+
+def delete_user(self, id: str) ‑> UserDeleteResponse +
+
+
+ +Expand source code + +
def delete_user(self, id: str) -> UserDeleteResponse:
+    return UserDeleteResponse(
+        self.api_call(
+            http_verb="DELETE",
+            path=f"Users/{quote(id)}",
+        )
+    )
+
+
+
+
+def patch_group(self,
id: str,
partial_group: Dict[str, Any] | Group) ‑> GroupPatchResponse
+
+
+
+ +Expand source code + +
def patch_group(self, id: str, partial_group: Union[Dict[str, Any], Group]) -> GroupPatchResponse:
+    return GroupPatchResponse(
+        self.api_call(
+            http_verb="PATCH",
+            path=f"Groups/{quote(id)}",
+            body_params=(
+                partial_group.to_dict()
+                if isinstance(partial_group, Group)
+                else _to_dict_without_not_given(partial_group)
+            ),
+        )
+    )
+
+
+
+
+def patch_user(self,
id: str,
partial_user: Dict[str, Any] | User) ‑> UserPatchResponse
+
+
+
+ +Expand source code + +
def patch_user(self, id: str, partial_user: Union[Dict[str, Any], User]) -> UserPatchResponse:
+    return UserPatchResponse(
+        self.api_call(
+            http_verb="PATCH",
+            path=f"Users/{quote(id)}",
+            body_params=(
+                partial_user.to_dict() if isinstance(partial_user, User) else _to_dict_without_not_given(partial_user)
+            ),
+        )
+    )
+
+
+
+
+def read_group(self, id: str) ‑> ReadGroupResponse +
+
+
+ +Expand source code + +
def read_group(self, id: str) -> ReadGroupResponse:
+    return ReadGroupResponse(self.api_call(http_verb="GET", path=f"Groups/{quote(id)}"))
+
+
+
+
+def read_user(self, id: str) ‑> ReadUserResponse +
+
+
+ +Expand source code + +
def read_user(self, id: str) -> ReadUserResponse:
+    return ReadUserResponse(self.api_call(http_verb="GET", path=f"Users/{quote(id)}"))
+
+
+
+
+def search_groups(self, *, count: int, start_index: int, filter: str | None = None) ‑> SearchGroupsResponse +
+
+
+ +Expand source code + +
def search_groups(
+    self,
+    *,
+    # Pagination required as of August 30, 2019.
+    count: int,
+    start_index: int,
+    filter: Optional[str] = None,
+) -> SearchGroupsResponse:
+    return SearchGroupsResponse(
+        self.api_call(
+            http_verb="GET",
+            path="Groups",
+            query_params={
+                "filter": filter,
+                "count": count,
+                "startIndex": start_index,
+            },
+        )
+    )
+
+
+
+
+def search_users(self, *, count: int, start_index: int, filter: str | None = None) ‑> SearchUsersResponse +
+
+
+ +Expand source code + +
def search_users(
+    self,
+    *,
+    # Pagination required as of August 30, 2019.
+    count: int,
+    start_index: int,
+    filter: Optional[str] = None,
+) -> SearchUsersResponse:
+    return SearchUsersResponse(
+        self.api_call(
+            http_verb="GET",
+            path="Users",
+            query_params={
+                "filter": filter,
+                "count": count,
+                "startIndex": start_index,
+            },
+        )
+    )
+
+
+
+
+def update_group(self,
group: Dict[str, Any] | Group) ‑> GroupUpdateResponse
+
+
+
+ +Expand source code + +
def update_group(self, group: Union[Dict[str, Any], Group]) -> GroupUpdateResponse:
+    group_id = group.id if isinstance(group, Group) else group["id"]
+    return GroupUpdateResponse(
+        self.api_call(
+            http_verb="PUT",
+            path=f"Groups/{quote(group_id)}",
+            body_params=group.to_dict() if isinstance(group, Group) else _to_dict_without_not_given(group),
+        )
+    )
+
+
+
+
+def update_user(self,
user: Dict[str, Any] | User) ‑> UserUpdateResponse
+
+
+
+ +Expand source code + +
def update_user(self, user: Union[Dict[str, Any], User]) -> UserUpdateResponse:
+    user_id = user.id if isinstance(user, User) else user["id"]
+    return UserUpdateResponse(
+        self.api_call(
+            http_verb="PUT",
+            path=f"Users/{quote(user_id)}",
+            body_params=user.to_dict() if isinstance(user, User) else _to_dict_without_not_given(user),
+        )
+    )
+
+
+
+
+
+
+
+
+ +
+ + + diff --git a/docs/reference/scim/v1/default_arg.html b/docs/reference/scim/v1/default_arg.html new file mode 100644 index 000000000..071fa3ca8 --- /dev/null +++ b/docs/reference/scim/v1/default_arg.html @@ -0,0 +1,89 @@ + + + + + + +slack_sdk.scim.v1.default_arg API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.scim.v1.default_arg

+
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class DefaultArg +
+
+
+ +Expand source code + +
class DefaultArg:
+    pass
+
+
+
+
+
+
+ +
+ + + diff --git a/docs/reference/scim/v1/group.html b/docs/reference/scim/v1/group.html new file mode 100644 index 000000000..615d75d06 --- /dev/null +++ b/docs/reference/scim/v1/group.html @@ -0,0 +1,312 @@ + + + + + + +slack_sdk.scim.v1.group API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.scim.v1.group

+
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class Group +(*,
display_name: str | DefaultArg | None = <slack_sdk.scim.v1.default_arg.DefaultArg object>,
id: str | DefaultArg | None = <slack_sdk.scim.v1.default_arg.DefaultArg object>,
members: List[GroupMember] | DefaultArg | None = <slack_sdk.scim.v1.default_arg.DefaultArg object>,
meta: GroupMeta | DefaultArg | None = <slack_sdk.scim.v1.default_arg.DefaultArg object>,
schemas: List[str] | DefaultArg | None = <slack_sdk.scim.v1.default_arg.DefaultArg object>,
**kwargs)
+
+
+
+ +Expand source code + +
class Group:
+    display_name: Union[Optional[str], DefaultArg]
+    id: Union[Optional[str], DefaultArg]
+    members: Union[Optional[List[GroupMember]], DefaultArg]
+    meta: Union[Optional[GroupMeta], DefaultArg]
+    schemas: Union[Optional[List[str]], DefaultArg]
+    unknown_fields: Dict[str, Any]
+
+    def __init__(
+        self,
+        *,
+        display_name: Union[Optional[str], DefaultArg] = NotGiven,
+        id: Union[Optional[str], DefaultArg] = NotGiven,
+        members: Union[Optional[List[GroupMember]], DefaultArg] = NotGiven,
+        meta: Union[Optional[GroupMeta], DefaultArg] = NotGiven,
+        schemas: Union[Optional[List[str]], DefaultArg] = NotGiven,
+        **kwargs,
+    ) -> None:
+        self.display_name = display_name
+        self.id = id
+        self.members = (
+            [a if isinstance(a, GroupMember) else GroupMember(**a) for a in members] if _is_iterable(members) else members
+        )
+        self.meta = GroupMeta(**meta) if meta is not None and isinstance(meta, dict) else meta
+        self.schemas = schemas
+        self.unknown_fields = kwargs
+
+    def to_dict(self):
+        return _to_dict_without_not_given(self)
+
+    def __repr__(self):
+        return f"<slack_sdk.scim.{self.__class__.__name__}: {self.to_dict()}>"
+
+
+

Class variables

+
+
var display_name : str | DefaultArg | None
+
+

The type of the None singleton.

+
+
var id : str | DefaultArg | None
+
+

The type of the None singleton.

+
+
var members : List[GroupMember] | DefaultArg | None
+
+

The type of the None singleton.

+
+
var metaGroupMeta | DefaultArg | None
+
+

The type of the None singleton.

+
+
var schemas : List[str] | DefaultArg | None
+
+

The type of the None singleton.

+
+
var unknown_fields : Dict[str, Any]
+
+

The type of the None singleton.

+
+
+

Methods

+
+
+def to_dict(self) +
+
+
+ +Expand source code + +
def to_dict(self):
+    return _to_dict_without_not_given(self)
+
+
+
+
+
+
+class GroupMember +(*,
display: str | DefaultArg | None = <slack_sdk.scim.v1.default_arg.DefaultArg object>,
value: str | DefaultArg | None = <slack_sdk.scim.v1.default_arg.DefaultArg object>,
**kwargs)
+
+
+
+ +Expand source code + +
class GroupMember:
+    display: Union[Optional[str], DefaultArg]
+    value: Union[Optional[str], DefaultArg]
+    unknown_fields: Dict[str, Any]
+
+    def __init__(
+        self,
+        *,
+        display: Union[Optional[str], DefaultArg] = NotGiven,
+        value: Union[Optional[str], DefaultArg] = NotGiven,
+        **kwargs,
+    ) -> None:
+        self.display = display
+        self.value = value
+        self.unknown_fields = kwargs
+
+    def to_dict(self):
+        return _to_dict_without_not_given(self)
+
+
+

Class variables

+
+
var display : str | DefaultArg | None
+
+

The type of the None singleton.

+
+
var unknown_fields : Dict[str, Any]
+
+

The type of the None singleton.

+
+
var value : str | DefaultArg | None
+
+

The type of the None singleton.

+
+
+

Methods

+
+
+def to_dict(self) +
+
+
+ +Expand source code + +
def to_dict(self):
+    return _to_dict_without_not_given(self)
+
+
+
+
+
+
+class GroupMeta +(*,
created: str | DefaultArg | None = <slack_sdk.scim.v1.default_arg.DefaultArg object>,
location: str | DefaultArg | None = <slack_sdk.scim.v1.default_arg.DefaultArg object>,
**kwargs)
+
+
+
+ +Expand source code + +
class GroupMeta:
+    created: Union[Optional[str], DefaultArg]
+    location: Union[Optional[str], DefaultArg]
+    unknown_fields: Dict[str, Any]
+
+    def __init__(
+        self,
+        *,
+        created: Union[Optional[str], DefaultArg] = NotGiven,
+        location: Union[Optional[str], DefaultArg] = NotGiven,
+        **kwargs,
+    ) -> None:
+        self.created = created
+        self.location = location
+        self.unknown_fields = kwargs
+
+    def to_dict(self):
+        return _to_dict_without_not_given(self)
+
+
+

Class variables

+
+
var created : str | DefaultArg | None
+
+

The type of the None singleton.

+
+
var location : str | DefaultArg | None
+
+

The type of the None singleton.

+
+
var unknown_fields : Dict[str, Any]
+
+

The type of the None singleton.

+
+
+

Methods

+
+
+def to_dict(self) +
+
+
+ +Expand source code + +
def to_dict(self):
+    return _to_dict_without_not_given(self)
+
+
+
+
+
+
+
+
+ +
+ + + diff --git a/docs/reference/scim/v1/index.html b/docs/reference/scim/v1/index.html new file mode 100644 index 000000000..2cbda3da0 --- /dev/null +++ b/docs/reference/scim/v1/index.html @@ -0,0 +1,119 @@ + + + + + + +slack_sdk.scim.v1 API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.scim.v1

+
+
+

SCIM API is a set of APIs for provisioning and managing user accounts and groups. +SCIM is used by Single Sign-On (SSO) services and identity providers to manage people across a variety of tools, +including Slack.

+

Refer to https://docs.slack.dev/tools/python-slack-sdk/scim for details.

+
+
+

Sub-modules

+
+
slack_sdk.scim.v1.async_client
+
+
+
+
slack_sdk.scim.v1.client
+
+

SCIM API is a set of APIs for provisioning and managing user accounts and groups. +SCIM is used by Single Sign-On (SSO) services and identity providers …

+
+
slack_sdk.scim.v1.default_arg
+
+
+
+
slack_sdk.scim.v1.group
+
+
+
+
slack_sdk.scim.v1.internal_utils
+
+
+
+
slack_sdk.scim.v1.response
+
+
+
+
slack_sdk.scim.v1.types
+
+
+
+
slack_sdk.scim.v1.user
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+ + + diff --git a/docs/reference/scim/v1/internal_utils.html b/docs/reference/scim/v1/internal_utils.html new file mode 100644 index 000000000..c8aecfd94 --- /dev/null +++ b/docs/reference/scim/v1/internal_utils.html @@ -0,0 +1,66 @@ + + + + + + +slack_sdk.scim.v1.internal_utils API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.scim.v1.internal_utils

+
+
+
+
+
+
+
+
+
+
+
+
+ +
+ + + diff --git a/docs/reference/scim/v1/response.html b/docs/reference/scim/v1/response.html new file mode 100644 index 000000000..83b638f69 --- /dev/null +++ b/docs/reference/scim/v1/response.html @@ -0,0 +1,967 @@ + + + + + + +slack_sdk.scim.v1.response API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.scim.v1.response

+
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class Errors +(code: int, description: str) +
+
+
+ +Expand source code + +
class Errors:
+    code: int
+    description: str
+
+    def __init__(self, code: int, description: str) -> None:
+        self.code = code
+        self.description = description
+
+    def to_dict(self) -> dict:
+        return {"code": self.code, "description": self.description}
+
+
+

Class variables

+
+
var code : int
+
+

The type of the None singleton.

+
+
var description : str
+
+

The type of the None singleton.

+
+
+

Methods

+
+
+def to_dict(self) ‑> dict +
+
+
+ +Expand source code + +
def to_dict(self) -> dict:
+    return {"code": self.code, "description": self.description}
+
+
+
+
+
+
+class GroupCreateResponse +(underlying: SCIMResponse) +
+
+
+ +Expand source code + +
class GroupCreateResponse(SCIMResponse):
+    group: Group
+
+    @property
+    def group(self) -> Group:
+        return Group(**self.snake_cased_body)
+
+    def __init__(self, underlying: SCIMResponse):
+        self.underlying = underlying
+        self.url = underlying.url
+        self.status_code = underlying.status_code
+        self.headers = underlying.headers
+        self.raw_body = underlying.raw_body
+        self.body = underlying.body
+        self._snake_cased_body = None
+
+
+

Ancestors

+ +

Instance variables

+
+
prop groupGroup
+
+
+ +Expand source code + +
@property
+def group(self) -> Group:
+    return Group(**self.snake_cased_body)
+
+
+
+
+

Inherited members

+ +
+
+class GroupDeleteResponse +(underlying: SCIMResponse) +
+
+
+ +Expand source code + +
class GroupDeleteResponse(SCIMResponse):
+    def __init__(self, underlying: SCIMResponse):
+        self.underlying = underlying
+        self.url = underlying.url
+        self.status_code = underlying.status_code
+        self.headers = underlying.headers
+        self.raw_body = underlying.raw_body
+        self.body = underlying.body
+        self._snake_cased_body = None
+
+
+

Ancestors

+ +

Inherited members

+ +
+
+class GroupPatchResponse +(underlying: SCIMResponse) +
+
+
+ +Expand source code + +
class GroupPatchResponse(SCIMResponse):
+    def __init__(self, underlying: SCIMResponse):
+        self.underlying = underlying
+        self.url = underlying.url
+        self.status_code = underlying.status_code
+        self.headers = underlying.headers
+        self.raw_body = underlying.raw_body
+        self.body = underlying.body
+        self._snake_cased_body = None
+
+
+

Ancestors

+ +

Inherited members

+ +
+
+class GroupUpdateResponse +(underlying: SCIMResponse) +
+
+
+ +Expand source code + +
class GroupUpdateResponse(SCIMResponse):
+    group: Group
+
+    @property
+    def group(self) -> Group:
+        return Group(**self.snake_cased_body)
+
+    def __init__(self, underlying: SCIMResponse):
+        self.underlying = underlying
+        self.url = underlying.url
+        self.status_code = underlying.status_code
+        self.headers = underlying.headers
+        self.raw_body = underlying.raw_body
+        self.body = underlying.body
+        self._snake_cased_body = None
+
+
+

Ancestors

+ +

Instance variables

+
+
prop groupGroup
+
+
+ +Expand source code + +
@property
+def group(self) -> Group:
+    return Group(**self.snake_cased_body)
+
+
+
+
+

Inherited members

+ +
+
+class ReadGroupResponse +(underlying: SCIMResponse) +
+
+
+ +Expand source code + +
class ReadGroupResponse(SCIMResponse):
+    group: Group
+
+    @property
+    def group(self) -> Group:
+        return Group(**self.snake_cased_body)
+
+    def __init__(self, underlying: SCIMResponse):
+        self.underlying = underlying
+        self.url = underlying.url
+        self.status_code = underlying.status_code
+        self.headers = underlying.headers
+        self.raw_body = underlying.raw_body
+        self.body = underlying.body
+        self._snake_cased_body = None
+
+
+

Ancestors

+ +

Instance variables

+
+
prop groupGroup
+
+
+ +Expand source code + +
@property
+def group(self) -> Group:
+    return Group(**self.snake_cased_body)
+
+
+
+
+

Inherited members

+ +
+
+class ReadUserResponse +(underlying: SCIMResponse) +
+
+
+ +Expand source code + +
class ReadUserResponse(SCIMResponse):
+    user: User
+
+    @property
+    def user(self) -> User:
+        return User(**self.snake_cased_body)
+
+    def __init__(self, underlying: SCIMResponse):
+        self.underlying = underlying
+        self.url = underlying.url
+        self.status_code = underlying.status_code
+        self.headers = underlying.headers
+        self.raw_body = underlying.raw_body
+        self.body = underlying.body
+        self._snake_cased_body = None
+
+
+

Ancestors

+ +

Instance variables

+
+
prop userUser
+
+
+ +Expand source code + +
@property
+def user(self) -> User:
+    return User(**self.snake_cased_body)
+
+
+
+
+

Inherited members

+ +
+
+class SCIMResponse +(*, url: str, status_code: int, raw_body: str | None, headers: dict) +
+
+
+ +Expand source code + +
class SCIMResponse:
+    url: str
+    status_code: int
+    headers: Dict[str, Any]
+    raw_body: Optional[str]
+    body: Optional[Dict[str, Any]]
+    snake_cased_body: Optional[Dict[str, Any]]
+
+    errors: Optional[Errors]
+
+    @property
+    def snake_cased_body(self) -> Optional[Dict[str, Any]]:
+        if self._snake_cased_body is None:
+            self._snake_cased_body = _to_snake_cased(self.body)
+        return self._snake_cased_body
+
+    @property
+    def errors(self) -> Optional[Errors]:
+        errors = self.snake_cased_body.get("errors")
+        if errors is None:
+            return None
+        return Errors(**errors)
+
+    def __init__(
+        self,
+        *,
+        url: str,
+        status_code: int,
+        raw_body: Optional[str],
+        headers: dict,
+    ):
+        self.url = url
+        self.status_code = status_code
+        self.headers = headers
+        self.raw_body = raw_body
+        self.body = json.loads(raw_body) if raw_body is not None and raw_body.startswith("{") else None
+        self._snake_cased_body = None  # build this when it's accessed for the first time
+
+    def __repr__(self):
+        dict_value = {}
+        for key, value in vars(self).items():
+            dict_value[key] = value.to_dict() if hasattr(value, "to_dict") else value
+
+        if dict_value:
+            return f"<slack_sdk.scim.v1.{self.__class__.__name__}: {dict_value}>"
+        else:
+            return self.__str__()
+
+
+

Subclasses

+ +

Class variables

+
+
var body : Dict[str, Any] | None
+
+

The type of the None singleton.

+
+
var headers : Dict[str, Any]
+
+

The type of the None singleton.

+
+
var raw_body : str | None
+
+

The type of the None singleton.

+
+
var status_code : int
+
+

The type of the None singleton.

+
+
var url : str
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
prop errorsErrors | None
+
+
+ +Expand source code + +
@property
+def errors(self) -> Optional[Errors]:
+    errors = self.snake_cased_body.get("errors")
+    if errors is None:
+        return None
+    return Errors(**errors)
+
+
+
+
prop snake_cased_body : Dict[str, Any] | None
+
+
+ +Expand source code + +
@property
+def snake_cased_body(self) -> Optional[Dict[str, Any]]:
+    if self._snake_cased_body is None:
+        self._snake_cased_body = _to_snake_cased(self.body)
+    return self._snake_cased_body
+
+
+
+
+
+
+class SearchGroupsResponse +(underlying: SCIMResponse) +
+
+
+ +Expand source code + +
class SearchGroupsResponse(SCIMResponse):
+    groups: List[Group]
+
+    @property
+    def groups(self) -> List[Group]:
+        return [Group(**r) for r in self.snake_cased_body.get("resources")]
+
+    def __init__(self, underlying: SCIMResponse):
+        self.underlying = underlying
+        self.url = underlying.url
+        self.status_code = underlying.status_code
+        self.headers = underlying.headers
+        self.raw_body = underlying.raw_body
+        self.body = underlying.body
+        self._snake_cased_body = None
+
+
+

Ancestors

+ +

Instance variables

+
+
prop groups : List[Group]
+
+
+ +Expand source code + +
@property
+def groups(self) -> List[Group]:
+    return [Group(**r) for r in self.snake_cased_body.get("resources")]
+
+
+
+
+

Inherited members

+ +
+
+class SearchUsersResponse +(underlying: SCIMResponse) +
+
+
+ +Expand source code + +
class SearchUsersResponse(SCIMResponse):
+    users: List[User]
+
+    @property
+    def users(self) -> List[User]:
+        return [User(**r) for r in self.snake_cased_body.get("resources")]
+
+    def __init__(self, underlying: SCIMResponse):
+        self.underlying = underlying
+        self.url = underlying.url
+        self.status_code = underlying.status_code
+        self.headers = underlying.headers
+        self.raw_body = underlying.raw_body
+        self.body = underlying.body
+        self._snake_cased_body = None
+
+
+

Ancestors

+ +

Instance variables

+
+
prop users : List[User]
+
+
+ +Expand source code + +
@property
+def users(self) -> List[User]:
+    return [User(**r) for r in self.snake_cased_body.get("resources")]
+
+
+
+
+

Inherited members

+ +
+
+class UserCreateResponse +(underlying: SCIMResponse) +
+
+
+ +Expand source code + +
class UserCreateResponse(SCIMResponse):
+    user: User
+
+    @property
+    def user(self) -> User:
+        return User(**self.snake_cased_body)
+
+    def __init__(self, underlying: SCIMResponse):
+        self.underlying = underlying
+        self.url = underlying.url
+        self.status_code = underlying.status_code
+        self.headers = underlying.headers
+        self.raw_body = underlying.raw_body
+        self.body = underlying.body
+        self._snake_cased_body = None
+
+
+

Ancestors

+ +

Instance variables

+
+
prop userUser
+
+
+ +Expand source code + +
@property
+def user(self) -> User:
+    return User(**self.snake_cased_body)
+
+
+
+
+

Inherited members

+ +
+
+class UserDeleteResponse +(underlying: SCIMResponse) +
+
+
+ +Expand source code + +
class UserDeleteResponse(SCIMResponse):
+    def __init__(self, underlying: SCIMResponse):
+        self.underlying = underlying
+        self.url = underlying.url
+        self.status_code = underlying.status_code
+        self.headers = underlying.headers
+        self.raw_body = underlying.raw_body
+        self.body = underlying.body
+        self._snake_cased_body = None
+
+
+

Ancestors

+ +

Inherited members

+ +
+
+class UserPatchResponse +(underlying: SCIMResponse) +
+
+
+ +Expand source code + +
class UserPatchResponse(SCIMResponse):
+    user: User
+
+    @property
+    def user(self) -> User:
+        return User(**self.snake_cased_body)
+
+    def __init__(self, underlying: SCIMResponse):
+        self.underlying = underlying
+        self.url = underlying.url
+        self.status_code = underlying.status_code
+        self.headers = underlying.headers
+        self.raw_body = underlying.raw_body
+        self.body = underlying.body
+        self._snake_cased_body = None
+
+
+

Ancestors

+ +

Instance variables

+
+
prop userUser
+
+
+ +Expand source code + +
@property
+def user(self) -> User:
+    return User(**self.snake_cased_body)
+
+
+
+
+

Inherited members

+ +
+
+class UserUpdateResponse +(underlying: SCIMResponse) +
+
+
+ +Expand source code + +
class UserUpdateResponse(SCIMResponse):
+    user: User
+
+    @property
+    def user(self) -> User:
+        return User(**self.snake_cased_body)
+
+    def __init__(self, underlying: SCIMResponse):
+        self.underlying = underlying
+        self.url = underlying.url
+        self.status_code = underlying.status_code
+        self.headers = underlying.headers
+        self.raw_body = underlying.raw_body
+        self.body = underlying.body
+        self._snake_cased_body = None
+
+
+

Ancestors

+ +

Instance variables

+
+
prop userUser
+
+
+ +Expand source code + +
@property
+def user(self) -> User:
+    return User(**self.snake_cased_body)
+
+
+
+
+

Inherited members

+ +
+
+
+
+ +
+ + + diff --git a/docs/reference/scim/v1/types.html b/docs/reference/scim/v1/types.html new file mode 100644 index 000000000..2d8b58aeb --- /dev/null +++ b/docs/reference/scim/v1/types.html @@ -0,0 +1,157 @@ + + + + + + +slack_sdk.scim.v1.types API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.scim.v1.types

+
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class TypeAndValue +(*,
primary: bool | DefaultArg | None = <slack_sdk.scim.v1.default_arg.DefaultArg object>,
type: str | DefaultArg | None = <slack_sdk.scim.v1.default_arg.DefaultArg object>,
value: str | DefaultArg | None = <slack_sdk.scim.v1.default_arg.DefaultArg object>,
**kwargs)
+
+
+
+ +Expand source code + +
class TypeAndValue:
+    primary: Union[Optional[bool], DefaultArg]
+    type: Union[Optional[str], DefaultArg]
+    value: Union[Optional[str], DefaultArg]
+    unknown_fields: Dict[str, Any]
+
+    def __init__(
+        self,
+        *,
+        primary: Union[Optional[bool], DefaultArg] = NotGiven,
+        type: Union[Optional[str], DefaultArg] = NotGiven,
+        value: Union[Optional[str], DefaultArg] = NotGiven,
+        **kwargs,
+    ) -> None:
+        self.primary = primary
+        self.type = type
+        self.value = value
+        self.unknown_fields = kwargs
+
+    def to_dict(self) -> dict:
+        return _to_dict_without_not_given(self)
+
+
+

Subclasses

+ +

Class variables

+
+
var primary : bool | DefaultArg | None
+
+

The type of the None singleton.

+
+
var type : str | DefaultArg | None
+
+

The type of the None singleton.

+
+
var unknown_fields : Dict[str, Any]
+
+

The type of the None singleton.

+
+
var value : str | DefaultArg | None
+
+

The type of the None singleton.

+
+
+

Methods

+
+
+def to_dict(self) ‑> dict +
+
+
+ +Expand source code + +
def to_dict(self) -> dict:
+    return _to_dict_without_not_given(self)
+
+
+
+
+
+
+
+
+ +
+ + + diff --git a/docs/reference/scim/v1/user.html b/docs/reference/scim/v1/user.html new file mode 100644 index 000000000..49e328355 --- /dev/null +++ b/docs/reference/scim/v1/user.html @@ -0,0 +1,774 @@ + + + + + + +slack_sdk.scim.v1.user API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.scim.v1.user

+
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class User +(*,
active: bool | DefaultArg | None = <slack_sdk.scim.v1.default_arg.DefaultArg object>,
addresses: List[UserAddress | Dict[str, Any]] | DefaultArg | None = <slack_sdk.scim.v1.default_arg.DefaultArg object>,
display_name: str | DefaultArg | None = <slack_sdk.scim.v1.default_arg.DefaultArg object>,
emails: List[TypeAndValue | Dict[str, Any]] | DefaultArg | None = <slack_sdk.scim.v1.default_arg.DefaultArg object>,
external_id: str | DefaultArg | None = <slack_sdk.scim.v1.default_arg.DefaultArg object>,
groups: List[UserGroup | Dict[str, Any]] | DefaultArg | None = <slack_sdk.scim.v1.default_arg.DefaultArg object>,
id: str | DefaultArg | None = <slack_sdk.scim.v1.default_arg.DefaultArg object>,
meta: UserMeta | Dict[str, Any] | DefaultArg | None = <slack_sdk.scim.v1.default_arg.DefaultArg object>,
name: UserName | Dict[str, Any] | DefaultArg | None = <slack_sdk.scim.v1.default_arg.DefaultArg object>,
nick_name: str | DefaultArg | None = <slack_sdk.scim.v1.default_arg.DefaultArg object>,
phone_numbers: List[TypeAndValue | Dict[str, Any]] | DefaultArg | None = <slack_sdk.scim.v1.default_arg.DefaultArg object>,
photos: List[UserPhoto | Dict[str, Any]] | DefaultArg | None = <slack_sdk.scim.v1.default_arg.DefaultArg object>,
profile_url: str | DefaultArg | None = <slack_sdk.scim.v1.default_arg.DefaultArg object>,
roles: List[TypeAndValue | Dict[str, Any]] | DefaultArg | None = <slack_sdk.scim.v1.default_arg.DefaultArg object>,
schemas: List[str] | DefaultArg | None = <slack_sdk.scim.v1.default_arg.DefaultArg object>,
timezone: str | DefaultArg | None = <slack_sdk.scim.v1.default_arg.DefaultArg object>,
title: str | DefaultArg | None = <slack_sdk.scim.v1.default_arg.DefaultArg object>,
user_name: str | DefaultArg | None = <slack_sdk.scim.v1.default_arg.DefaultArg object>,
**kwargs)
+
+
+
+ +Expand source code + +
class User:
+    active: Union[Optional[bool], DefaultArg]
+    addresses: Union[Optional[List[UserAddress]], DefaultArg]
+    display_name: Union[Optional[str], DefaultArg]
+    emails: Union[Optional[List[TypeAndValue]], DefaultArg]
+    external_id: Union[Optional[str], DefaultArg]
+    groups: Union[Optional[List[UserGroup]], DefaultArg]
+    id: Union[Optional[str], DefaultArg]
+    meta: Union[Optional[UserMeta], DefaultArg]
+    name: Union[Optional[UserName], DefaultArg]
+    nick_name: Union[Optional[str], DefaultArg]
+    phone_numbers: Union[Optional[List[TypeAndValue]], DefaultArg]
+    photos: Union[Optional[List[UserPhoto]], DefaultArg]
+    profile_url: Union[Optional[str], DefaultArg]
+    roles: Union[Optional[List[TypeAndValue]], DefaultArg]
+    schemas: Union[Optional[List[str]], DefaultArg]
+    timezone: Union[Optional[str], DefaultArg]
+    title: Union[Optional[str], DefaultArg]
+    user_name: Union[Optional[str], DefaultArg]
+    unknown_fields: Dict[str, Any]
+
+    def __init__(
+        self,
+        *,
+        active: Union[Optional[bool], DefaultArg] = NotGiven,
+        addresses: Union[Optional[List[Union[UserAddress, Dict[str, Any]]]], DefaultArg] = NotGiven,
+        display_name: Union[Optional[str], DefaultArg] = NotGiven,
+        emails: Union[Optional[List[Union[TypeAndValue, Dict[str, Any]]]], DefaultArg] = NotGiven,
+        external_id: Union[Optional[str], DefaultArg] = NotGiven,
+        groups: Union[Optional[List[Union[UserGroup, Dict[str, Any]]]], DefaultArg] = NotGiven,
+        id: Union[Optional[str], DefaultArg] = NotGiven,
+        meta: Union[Optional[Union[UserMeta, Dict[str, Any]]], DefaultArg] = NotGiven,
+        name: Union[Optional[Union[UserName, Dict[str, Any]]], DefaultArg] = NotGiven,
+        nick_name: Union[Optional[str], DefaultArg] = NotGiven,
+        phone_numbers: Union[Optional[List[Union[TypeAndValue, Dict[str, Any]]]], DefaultArg] = NotGiven,
+        photos: Union[Optional[List[Union[UserPhoto, Dict[str, Any]]]], DefaultArg] = NotGiven,
+        profile_url: Union[Optional[str], DefaultArg] = NotGiven,
+        roles: Union[Optional[List[Union[TypeAndValue, Dict[str, Any]]]], DefaultArg] = NotGiven,
+        schemas: Union[Optional[List[str]], DefaultArg] = NotGiven,
+        timezone: Union[Optional[str], DefaultArg] = NotGiven,
+        title: Union[Optional[str], DefaultArg] = NotGiven,
+        user_name: Union[Optional[str], DefaultArg] = NotGiven,
+        **kwargs,
+    ) -> None:
+        self.active = active
+        self.addresses = (
+            [a if isinstance(a, UserAddress) else UserAddress(**a) for a in addresses]  # type: ignore
+            if _is_iterable(addresses)
+            else addresses
+        )
+        self.display_name = display_name
+        self.emails = (
+            [a if isinstance(a, TypeAndValue) else TypeAndValue(**a) for a in emails]  # type: ignore
+            if _is_iterable(emails)
+            else emails
+        )
+        self.external_id = external_id
+        self.groups = (
+            [a if isinstance(a, UserGroup) else UserGroup(**a) for a in groups]  # type: ignore
+            if _is_iterable(groups)
+            else groups
+        )
+        self.id = id
+        self.meta = UserMeta(**meta) if meta is not None and isinstance(meta, dict) else meta
+        self.name = UserName(**name) if name is not None and isinstance(name, dict) else name
+        self.nick_name = nick_name
+        self.phone_numbers = (
+            [a if isinstance(a, TypeAndValue) else TypeAndValue(**a) for a in phone_numbers]  # type: ignore
+            if _is_iterable(phone_numbers)
+            else phone_numbers
+        )
+        self.photos = (
+            [a if isinstance(a, UserPhoto) else UserPhoto(**a) for a in photos]  # type: ignore
+            if _is_iterable(photos)
+            else photos
+        )
+        self.profile_url = profile_url
+        self.roles = (
+            [a if isinstance(a, TypeAndValue) else TypeAndValue(**a) for a in roles]  # type: ignore
+            if _is_iterable(roles)
+            else roles
+        )
+        self.schemas = schemas
+        self.timezone = timezone
+        self.title = title
+        self.user_name = user_name
+
+        self.unknown_fields = kwargs
+
+    def to_dict(self):
+        return _to_dict_without_not_given(self)
+
+    def __repr__(self):
+        return f"<slack_sdk.scim.{self.__class__.__name__}: {self.to_dict()}>"
+
+
+

Class variables

+
+
var active : bool | DefaultArg | None
+
+

The type of the None singleton.

+
+
var addresses : List[UserAddress] | DefaultArg | None
+
+

The type of the None singleton.

+
+
var display_name : str | DefaultArg | None
+
+

The type of the None singleton.

+
+
var emails : List[TypeAndValue] | DefaultArg | None
+
+

The type of the None singleton.

+
+
var external_id : str | DefaultArg | None
+
+

The type of the None singleton.

+
+
var groups : List[UserGroup] | DefaultArg | None
+
+

The type of the None singleton.

+
+
var id : str | DefaultArg | None
+
+

The type of the None singleton.

+
+
var metaUserMeta | DefaultArg | None
+
+

The type of the None singleton.

+
+
var nameUserName | DefaultArg | None
+
+

The type of the None singleton.

+
+
var nick_name : str | DefaultArg | None
+
+

The type of the None singleton.

+
+
var phone_numbers : List[TypeAndValue] | DefaultArg | None
+
+

The type of the None singleton.

+
+
var photos : List[UserPhoto] | DefaultArg | None
+
+

The type of the None singleton.

+
+
var profile_url : str | DefaultArg | None
+
+

The type of the None singleton.

+
+
var roles : List[TypeAndValue] | DefaultArg | None
+
+

The type of the None singleton.

+
+
var schemas : List[str] | DefaultArg | None
+
+

The type of the None singleton.

+
+
var timezone : str | DefaultArg | None
+
+

The type of the None singleton.

+
+
var title : str | DefaultArg | None
+
+

The type of the None singleton.

+
+
var unknown_fields : Dict[str, Any]
+
+

The type of the None singleton.

+
+
var user_name : str | DefaultArg | None
+
+

The type of the None singleton.

+
+
+

Methods

+
+
+def to_dict(self) +
+
+
+ +Expand source code + +
def to_dict(self):
+    return _to_dict_without_not_given(self)
+
+
+
+
+
+
+class UserAddress +(*,
country: str | DefaultArg | None = <slack_sdk.scim.v1.default_arg.DefaultArg object>,
locality: str | DefaultArg | None = <slack_sdk.scim.v1.default_arg.DefaultArg object>,
postal_code: str | DefaultArg | None = <slack_sdk.scim.v1.default_arg.DefaultArg object>,
primary: bool | DefaultArg | None = <slack_sdk.scim.v1.default_arg.DefaultArg object>,
region: str | DefaultArg | None = <slack_sdk.scim.v1.default_arg.DefaultArg object>,
street_address: str | DefaultArg | None = <slack_sdk.scim.v1.default_arg.DefaultArg object>,
**kwargs)
+
+
+
+ +Expand source code + +
class UserAddress:
+    country: Union[Optional[str], DefaultArg]
+    locality: Union[Optional[str], DefaultArg]
+    postal_code: Union[Optional[str], DefaultArg]
+    primary: Union[Optional[bool], DefaultArg]
+    region: Union[Optional[str], DefaultArg]
+    street_address: Union[Optional[str], DefaultArg]
+    unknown_fields: Dict[str, Any]
+
+    def __init__(
+        self,
+        *,
+        country: Union[Optional[str], DefaultArg] = NotGiven,
+        locality: Union[Optional[str], DefaultArg] = NotGiven,
+        postal_code: Union[Optional[str], DefaultArg] = NotGiven,
+        primary: Union[Optional[bool], DefaultArg] = NotGiven,
+        region: Union[Optional[str], DefaultArg] = NotGiven,
+        street_address: Union[Optional[str], DefaultArg] = NotGiven,
+        **kwargs,
+    ) -> None:
+        self.country = country
+        self.locality = locality
+        self.postal_code = postal_code
+        self.primary = primary
+        self.region = region
+        self.street_address = street_address
+        self.unknown_fields = kwargs
+
+    def to_dict(self) -> dict:
+        return _to_dict_without_not_given(self)
+
+
+

Class variables

+
+
var country : str | DefaultArg | None
+
+

The type of the None singleton.

+
+
var locality : str | DefaultArg | None
+
+

The type of the None singleton.

+
+
var postal_code : str | DefaultArg | None
+
+

The type of the None singleton.

+
+
var primary : bool | DefaultArg | None
+
+

The type of the None singleton.

+
+
var region : str | DefaultArg | None
+
+

The type of the None singleton.

+
+
var street_address : str | DefaultArg | None
+
+

The type of the None singleton.

+
+
var unknown_fields : Dict[str, Any]
+
+

The type of the None singleton.

+
+
+

Methods

+
+
+def to_dict(self) ‑> dict +
+
+
+ +Expand source code + +
def to_dict(self) -> dict:
+    return _to_dict_without_not_given(self)
+
+
+
+
+
+
+class UserEmail +(*,
primary: bool | DefaultArg | None = <slack_sdk.scim.v1.default_arg.DefaultArg object>,
type: str | DefaultArg | None = <slack_sdk.scim.v1.default_arg.DefaultArg object>,
value: str | DefaultArg | None = <slack_sdk.scim.v1.default_arg.DefaultArg object>,
**kwargs)
+
+
+
+ +Expand source code + +
class UserEmail(TypeAndValue):
+    pass
+
+
+

Ancestors

+ +

Inherited members

+ +
+
+class UserGroup +(*,
display: str | DefaultArg | None = <slack_sdk.scim.v1.default_arg.DefaultArg object>,
value: str | DefaultArg | None = <slack_sdk.scim.v1.default_arg.DefaultArg object>,
**kwargs)
+
+
+
+ +Expand source code + +
class UserGroup:
+    display: Union[Optional[str], DefaultArg]
+    value: Union[Optional[str], DefaultArg]
+    unknown_fields: Dict[str, Any]
+
+    def __init__(
+        self,
+        *,
+        display: Union[Optional[str], DefaultArg] = NotGiven,
+        value: Union[Optional[str], DefaultArg] = NotGiven,
+        **kwargs,
+    ) -> None:
+        self.display = display
+        self.value = value
+        self.unknown_fields = kwargs
+
+    def to_dict(self) -> dict:
+        return _to_dict_without_not_given(self)
+
+
+

Class variables

+
+
var display : str | DefaultArg | None
+
+

The type of the None singleton.

+
+
var unknown_fields : Dict[str, Any]
+
+

The type of the None singleton.

+
+
var value : str | DefaultArg | None
+
+

The type of the None singleton.

+
+
+

Methods

+
+
+def to_dict(self) ‑> dict +
+
+
+ +Expand source code + +
def to_dict(self) -> dict:
+    return _to_dict_without_not_given(self)
+
+
+
+
+
+
+class UserMeta +(created: str | DefaultArg | None = <slack_sdk.scim.v1.default_arg.DefaultArg object>,
location: str | DefaultArg | None = <slack_sdk.scim.v1.default_arg.DefaultArg object>,
**kwargs)
+
+
+
+ +Expand source code + +
class UserMeta:
+    created: Union[Optional[str], DefaultArg]
+    location: Union[Optional[str], DefaultArg]
+    unknown_fields: Dict[str, Any]
+
+    def __init__(
+        self,
+        created: Union[Optional[str], DefaultArg] = NotGiven,
+        location: Union[Optional[str], DefaultArg] = NotGiven,
+        **kwargs,
+    ) -> None:
+        self.created = created
+        self.location = location
+        self.unknown_fields = kwargs
+
+    def to_dict(self) -> dict:
+        return _to_dict_without_not_given(self)
+
+
+

Class variables

+
+
var created : str | DefaultArg | None
+
+

The type of the None singleton.

+
+
var location : str | DefaultArg | None
+
+

The type of the None singleton.

+
+
var unknown_fields : Dict[str, Any]
+
+

The type of the None singleton.

+
+
+

Methods

+
+
+def to_dict(self) ‑> dict +
+
+
+ +Expand source code + +
def to_dict(self) -> dict:
+    return _to_dict_without_not_given(self)
+
+
+
+
+
+
+class UserName +(family_name: str | DefaultArg | None = <slack_sdk.scim.v1.default_arg.DefaultArg object>,
given_name: str | DefaultArg | None = <slack_sdk.scim.v1.default_arg.DefaultArg object>,
**kwargs)
+
+
+
+ +Expand source code + +
class UserName:
+    family_name: Union[Optional[str], DefaultArg]
+    given_name: Union[Optional[str], DefaultArg]
+    unknown_fields: Dict[str, Any]
+
+    def __init__(
+        self,
+        family_name: Union[Optional[str], DefaultArg] = NotGiven,
+        given_name: Union[Optional[str], DefaultArg] = NotGiven,
+        **kwargs,
+    ) -> None:
+        self.family_name = family_name
+        self.given_name = given_name
+        self.unknown_fields = kwargs
+
+    def to_dict(self) -> dict:
+        return _to_dict_without_not_given(self)
+
+
+

Class variables

+
+
var family_name : str | DefaultArg | None
+
+

The type of the None singleton.

+
+
var given_name : str | DefaultArg | None
+
+

The type of the None singleton.

+
+
var unknown_fields : Dict[str, Any]
+
+

The type of the None singleton.

+
+
+

Methods

+
+
+def to_dict(self) ‑> dict +
+
+
+ +Expand source code + +
def to_dict(self) -> dict:
+    return _to_dict_without_not_given(self)
+
+
+
+
+
+
+class UserPhoneNumber +(*,
primary: bool | DefaultArg | None = <slack_sdk.scim.v1.default_arg.DefaultArg object>,
type: str | DefaultArg | None = <slack_sdk.scim.v1.default_arg.DefaultArg object>,
value: str | DefaultArg | None = <slack_sdk.scim.v1.default_arg.DefaultArg object>,
**kwargs)
+
+
+
+ +Expand source code + +
class UserPhoneNumber(TypeAndValue):
+    pass
+
+
+

Ancestors

+ +

Inherited members

+ +
+
+class UserPhoto +(type: str | DefaultArg | None = <slack_sdk.scim.v1.default_arg.DefaultArg object>,
value: str | DefaultArg | None = <slack_sdk.scim.v1.default_arg.DefaultArg object>,
**kwargs)
+
+
+
+ +Expand source code + +
class UserPhoto:
+    type: Union[Optional[str], DefaultArg]
+    value: Union[Optional[str], DefaultArg]
+    unknown_fields: Dict[str, Any]
+
+    def __init__(
+        self,
+        type: Union[Optional[str], DefaultArg] = NotGiven,
+        value: Union[Optional[str], DefaultArg] = NotGiven,
+        **kwargs,
+    ) -> None:
+        self.type = type
+        self.value = value
+        self.unknown_fields = kwargs
+
+    def to_dict(self) -> dict:
+        return _to_dict_without_not_given(self)
+
+
+

Class variables

+
+
var type : str | DefaultArg | None
+
+

The type of the None singleton.

+
+
var unknown_fields : Dict[str, Any]
+
+

The type of the None singleton.

+
+
var value : str | DefaultArg | None
+
+

The type of the None singleton.

+
+
+

Methods

+
+
+def to_dict(self) ‑> dict +
+
+
+ +Expand source code + +
def to_dict(self) -> dict:
+    return _to_dict_without_not_given(self)
+
+
+
+
+
+
+class UserRole +(*,
primary: bool | DefaultArg | None = <slack_sdk.scim.v1.default_arg.DefaultArg object>,
type: str | DefaultArg | None = <slack_sdk.scim.v1.default_arg.DefaultArg object>,
value: str | DefaultArg | None = <slack_sdk.scim.v1.default_arg.DefaultArg object>,
**kwargs)
+
+
+
+ +Expand source code + +
class UserRole(TypeAndValue):
+    pass
+
+
+

Ancestors

+ +

Inherited members

+ +
+
+
+
+ +
+ + + diff --git a/docs/reference/signature/index.html b/docs/reference/signature/index.html new file mode 100644 index 000000000..ecc1f0831 --- /dev/null +++ b/docs/reference/signature/index.html @@ -0,0 +1,274 @@ + + + + + + +slack_sdk.signature API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.signature

+
+
+

Slack request signature verifier

+
+
+
+
+
+
+
+
+

Classes

+
+
+class Clock +
+
+
+ +Expand source code + +
class Clock:
+    def now(self) -> float:
+        return time()
+
+
+

Methods

+
+
+def now(self) ‑> float +
+
+
+ +Expand source code + +
def now(self) -> float:
+    return time()
+
+
+
+
+
+
+class SignatureVerifier +(signing_secret: str,
clock: Clock = <slack_sdk.signature.Clock object>)
+
+
+
+ +Expand source code + +
class SignatureVerifier:
+    def __init__(self, signing_secret: str, clock: Clock = Clock()):
+        """Slack request signature verifier
+
+        Slack signs its requests using a secret that's unique to your app.
+        With the help of signing secrets, your app can more confidently verify
+        whether requests from us are authentic.
+        https://docs.slack.dev/authentication/verifying-requests-from-slack/
+        """
+        self.signing_secret = signing_secret
+        self.clock = clock
+
+    def is_valid_request(
+        self,
+        body: Union[str, bytes],
+        headers: Dict[str, str],
+    ) -> bool:
+        """Verifies if the given signature is valid"""
+        if headers is None:
+            return False
+        normalized_headers = {k.lower(): v for k, v in headers.items()}
+        return self.is_valid(
+            body=body,
+            timestamp=normalized_headers.get("x-slack-request-timestamp", None),  # type: ignore[arg-type]
+            signature=normalized_headers.get("x-slack-signature", None),  # type: ignore[arg-type]
+        )
+
+    def is_valid(
+        self,
+        body: Union[str, bytes],
+        timestamp: str,
+        signature: str,
+    ) -> bool:
+        """Verifies if the given signature is valid"""
+        if timestamp is None or signature is None:
+            return False
+
+        if abs(self.clock.now() - int(timestamp)) > 60 * 5:
+            return False
+
+        calculated_signature = self.generate_signature(timestamp=timestamp, body=body)
+        if calculated_signature is None:
+            return False
+        return hmac.compare_digest(calculated_signature, signature)
+
+    def generate_signature(self, *, timestamp: str, body: Union[str, bytes]) -> Optional[str]:
+        """Generates a signature"""
+        if timestamp is None:
+            return None
+        if body is None:
+            body = ""
+        if isinstance(body, bytes):
+            body = body.decode("utf-8")
+
+        format_req = str.encode(f"v0:{timestamp}:{body}")
+        encoded_secret = str.encode(self.signing_secret)
+        request_hash = hmac.new(encoded_secret, format_req, hashlib.sha256).hexdigest()
+        calculated_signature = f"v0={request_hash}"
+        return calculated_signature
+
+

Slack request signature verifier

+

Slack signs its requests using a secret that's unique to your app. +With the help of signing secrets, your app can more confidently verify +whether requests from us are authentic. +https://docs.slack.dev/authentication/verifying-requests-from-slack/

+

Methods

+
+
+def generate_signature(self, *, timestamp: str, body: str | bytes) ‑> str | None +
+
+
+ +Expand source code + +
def generate_signature(self, *, timestamp: str, body: Union[str, bytes]) -> Optional[str]:
+    """Generates a signature"""
+    if timestamp is None:
+        return None
+    if body is None:
+        body = ""
+    if isinstance(body, bytes):
+        body = body.decode("utf-8")
+
+    format_req = str.encode(f"v0:{timestamp}:{body}")
+    encoded_secret = str.encode(self.signing_secret)
+    request_hash = hmac.new(encoded_secret, format_req, hashlib.sha256).hexdigest()
+    calculated_signature = f"v0={request_hash}"
+    return calculated_signature
+
+

Generates a signature

+
+
+def is_valid(self, body: str | bytes, timestamp: str, signature: str) ‑> bool +
+
+
+ +Expand source code + +
def is_valid(
+    self,
+    body: Union[str, bytes],
+    timestamp: str,
+    signature: str,
+) -> bool:
+    """Verifies if the given signature is valid"""
+    if timestamp is None or signature is None:
+        return False
+
+    if abs(self.clock.now() - int(timestamp)) > 60 * 5:
+        return False
+
+    calculated_signature = self.generate_signature(timestamp=timestamp, body=body)
+    if calculated_signature is None:
+        return False
+    return hmac.compare_digest(calculated_signature, signature)
+
+

Verifies if the given signature is valid

+
+
+def is_valid_request(self, body: str | bytes, headers: Dict[str, str]) ‑> bool +
+
+
+ +Expand source code + +
def is_valid_request(
+    self,
+    body: Union[str, bytes],
+    headers: Dict[str, str],
+) -> bool:
+    """Verifies if the given signature is valid"""
+    if headers is None:
+        return False
+    normalized_headers = {k.lower(): v for k, v in headers.items()}
+    return self.is_valid(
+        body=body,
+        timestamp=normalized_headers.get("x-slack-request-timestamp", None),  # type: ignore[arg-type]
+        signature=normalized_headers.get("x-slack-signature", None),  # type: ignore[arg-type]
+    )
+
+

Verifies if the given signature is valid

+
+
+
+
+
+
+ +
+ + + diff --git a/docs/reference/socket_mode/aiohttp/index.html b/docs/reference/socket_mode/aiohttp/index.html new file mode 100644 index 000000000..92c6225c1 --- /dev/null +++ b/docs/reference/socket_mode/aiohttp/index.html @@ -0,0 +1,1071 @@ + + + + + + +slack_sdk.socket_mode.aiohttp API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.socket_mode.aiohttp

+
+
+

aiohttp based Socket Mode client

+ +
+
+
+
+
+
+
+
+

Classes

+
+
+class SocketModeClient +(app_token: str,
logger: logging.Logger | None = None,
web_client: AsyncWebClient | None = None,
proxy: str | None = None,
auto_reconnect_enabled: bool = True,
ping_interval: float = 5,
trace_enabled: bool = False,
on_message_listeners: List[Callable[[aiohttp._websocket.models.WSMessage], Awaitable[None]]] | None = None,
on_error_listeners: List[Callable[[aiohttp._websocket.models.WSMessage], Awaitable[None]]] | None = None,
on_close_listeners: List[Callable[[aiohttp._websocket.models.WSMessage], Awaitable[None]]] | None = None,
loop: asyncio.events.AbstractEventLoop | None = None)
+
+
+
+ +Expand source code + +
class SocketModeClient(AsyncBaseSocketModeClient):
+    logger: Logger
+    web_client: AsyncWebClient
+    app_token: str
+    wss_uri: Optional[str]  # type: ignore[assignment]
+    auto_reconnect_enabled: bool
+    message_queue: Queue
+    message_listeners: List[
+        Union[
+            AsyncWebSocketMessageListener,
+            Callable[["AsyncBaseSocketModeClient", dict, Optional[str]], Awaitable[None]],
+        ]
+    ]
+    socket_mode_request_listeners: List[
+        Union[
+            AsyncSocketModeRequestListener,
+            Callable[["AsyncBaseSocketModeClient", SocketModeRequest], Awaitable[None]],
+        ]
+    ]
+
+    message_receiver: Optional[Future]
+    message_processor: Future
+
+    proxy: Optional[str]
+    ping_interval: float
+    trace_enabled: bool
+
+    last_ping_pong_time: Optional[float]
+    current_session: Optional[ClientWebSocketResponse]
+    current_session_monitor: Optional[Future]
+
+    default_auto_reconnect_enabled: bool
+    closed: bool
+    stale: bool
+    connect_operation_lock: Lock
+
+    on_message_listeners: List[Callable[[WSMessage], Awaitable[None]]]
+    on_error_listeners: List[Callable[[WSMessage], Awaitable[None]]]
+    on_close_listeners: List[Callable[[WSMessage], Awaitable[None]]]
+
+    def __init__(
+        self,
+        app_token: str,
+        logger: Optional[Logger] = None,
+        web_client: Optional[AsyncWebClient] = None,
+        proxy: Optional[str] = None,
+        auto_reconnect_enabled: bool = True,
+        ping_interval: float = 5,
+        trace_enabled: bool = False,
+        on_message_listeners: Optional[List[Callable[[WSMessage], Awaitable[None]]]] = None,
+        on_error_listeners: Optional[List[Callable[[WSMessage], Awaitable[None]]]] = None,
+        on_close_listeners: Optional[List[Callable[[WSMessage], Awaitable[None]]]] = None,
+        loop: Optional[AbstractEventLoop] = None,
+    ):
+        """Socket Mode client
+
+        Args:
+            app_token: App-level token
+            logger: Custom logger
+            web_client: Web API client
+            auto_reconnect_enabled: True if automatic reconnection is enabled (default: True)
+            ping_interval: interval for ping-pong with Slack servers (seconds)
+            trace_enabled: True if more verbose logs to see what's happening under the hood
+            proxy: the HTTP proxy URL
+            on_message_listeners: listener functions for on_message
+            on_error_listeners: listener functions for on_error
+            on_close_listeners: listener functions for on_close
+            loop: an existing asyncio event loop
+        """
+        self.app_token = app_token
+        self.logger = logger or logging.getLogger(__name__)
+        self.web_client = web_client or AsyncWebClient()
+        self.closed = False
+        self.stale = False
+        self.connect_operation_lock = Lock()
+        self.proxy = proxy
+        if self.proxy is None or len(self.proxy.strip()) == 0:
+            env_variable = load_http_proxy_from_env(self.logger)
+            if env_variable is not None:
+                self.proxy = env_variable
+
+        self.default_auto_reconnect_enabled = auto_reconnect_enabled
+        self.auto_reconnect_enabled = self.default_auto_reconnect_enabled
+        self.ping_interval = ping_interval
+        self.trace_enabled = trace_enabled
+        self.last_ping_pong_time = None
+
+        self.wss_uri = None
+        self.message_queue = Queue()
+        self.message_listeners = []
+        self.socket_mode_request_listeners = []
+        self.current_session = None
+        self.current_session_monitor = None
+
+        # https://docs.aiohttp.org/en/stable/client_reference.html
+        # Unless you are connecting to a large, unknown number of different servers
+        # over the lifetime of your application,
+        # it is suggested you use a single session for the lifetime of your application
+        # to benefit from connection pooling.
+        self.aiohttp_client_session = aiohttp.ClientSession(loop=loop)
+
+        self.on_message_listeners = on_message_listeners or []
+        self.on_error_listeners = on_error_listeners or []
+        self.on_close_listeners = on_close_listeners or []
+
+        self.message_receiver = None
+        self.message_processor = asyncio.ensure_future(self.process_messages())
+
+    async def monitor_current_session(self) -> None:
+        # In the asyncio runtime, accessing a shared object (self.current_session here) from
+        # multiple tasks can cause race conditions and errors.
+        # To avoid such, we access only the session that is active when this loop starts.
+        session: ClientWebSocketResponse = self.current_session  # type: ignore[assignment]
+        session_id: str = self.build_session_id(session)
+
+        if self.logger.level <= logging.DEBUG:
+            self.logger.debug(f"A new monitor_current_session() execution loop for {session_id} started")
+        try:
+            logging_interval = 100
+            counter_for_logging = 0
+
+            while not self.closed:
+                if session != self.current_session:
+                    if self.logger.level <= logging.DEBUG:
+                        self.logger.debug(f"The monitor_current_session task for {session_id} is now cancelled")
+                    break
+                try:
+                    if self.trace_enabled and self.logger.level <= logging.DEBUG:
+                        # The logging here is for detailed investigation on potential issues in this client.
+                        # If you don't see this log for a while, it means that
+                        # this receive_messages execution is no longer working for some reason.
+                        counter_for_logging += 1
+                        if counter_for_logging >= logging_interval:
+                            counter_for_logging = 0
+                            log_message = (
+                                "#monitor_current_session method has been verifying if this session is active "
+                                f"(session: {session_id}, logging interval: {logging_interval})"
+                            )
+                            self.logger.debug(log_message)
+
+                    await asyncio.sleep(self.ping_interval)
+
+                    if session is not None and session.closed is False:
+                        t = time.time()
+                        if self.last_ping_pong_time is None:
+                            self.last_ping_pong_time = float(t)
+                        try:
+                            await session.ping(f"sdk-ping-pong:{t}".encode("utf-8"))
+                        except Exception as e:
+                            # The ping() method can fail for some reason.
+                            # To establish a new connection even in this scenario,
+                            # we ignore the exception here.
+                            self.logger.warning(f"Failed to send a ping message ({session_id}): {e}")
+
+                    if self.auto_reconnect_enabled:
+                        should_reconnect = False
+                        if session is None or session.closed:
+                            self.logger.info(f"The session ({session_id}) seems to be already closed. Reconnecting...")
+                            should_reconnect = True
+
+                        if await self.is_ping_pong_failing():
+                            disconnected_seconds = int(time.time() - self.last_ping_pong_time)  # type: ignore[operator]
+                            self.logger.info(
+                                f"The session ({session_id}) seems to be stale. Reconnecting..."
+                                f" reason: disconnected for {disconnected_seconds}+ seconds)"
+                            )
+                            self.stale = True
+                            self.last_ping_pong_time = None
+                            should_reconnect = True
+
+                        if should_reconnect is True or not await self.is_connected():
+                            await self.connect_to_new_endpoint()
+
+                except Exception as e:
+                    self.logger.error(
+                        f"Failed to check the current session ({session_id}) or reconnect to the server "
+                        f"(error: {type(e).__name__}, message: {e})"
+                    )
+        except asyncio.CancelledError:
+            if self.logger.level <= logging.DEBUG:
+                self.logger.debug(f"The monitor_current_session task for {session_id} is now cancelled")
+            raise
+
+    async def receive_messages(self) -> None:
+        # In the asyncio runtime, accessing a shared object (self.current_session here) from
+        # multiple tasks can cause race conditions and errors.
+        # To avoid such, we access only the session that is active when this loop starts.
+        session = self.current_session
+        session_id = self.build_session_id(session)  # type: ignore[arg-type]
+        if self.logger.level <= logging.DEBUG:
+            self.logger.debug(f"A new receive_messages() execution loop with {session_id} started")
+        try:
+            consecutive_error_count = 0
+            logging_interval = 100
+            counter_for_logging = 0
+
+            while not self.closed:
+                if session != self.current_session:
+                    if self.logger.level <= logging.DEBUG:
+                        self.logger.debug(f"The running receive_messages task for {session_id} is now cancelled")
+                    break
+                try:
+                    message: WSMessage = await session.receive()  # type: ignore[union-attr]
+                    # just in case, checking if the value is not None
+                    if message is not None:
+                        if self.logger.level <= logging.DEBUG:
+                            # The following logging prints every single received message
+                            # except empty message data ones.
+                            m_type = WSMsgType(message.type)
+                            message_type = m_type.name if m_type is not None else message.type
+                            message_data = message.data
+                            if isinstance(message_data, bytes):
+                                message_data = message_data.decode("utf-8")
+                            if message_data is not None and isinstance(message_data, (str, bytes)) and len(message_data) > 0:
+                                # To skip the empty message that Slack server-side often sends
+                                self.logger.debug(
+                                    f"Received message "
+                                    f"(type: {message_type}, "
+                                    f"data: {message_data}, "
+                                    f"extra: {message.extra}, "
+                                    f"session: {session_id})"
+                                )
+
+                            if self.trace_enabled:
+                                # The logging here is for detailed trouble shooting of potential issues in this client.
+                                # If you don't see this log for a while, it can mean that
+                                # this receive_messages execution is no longer working for some reason.
+                                counter_for_logging += 1
+                                if counter_for_logging >= logging_interval:
+                                    counter_for_logging = 0
+                                    log_message = (
+                                        "#receive_messages method has been working without any issues "
+                                        f"(session: {session_id}, logging interval: {logging_interval})"
+                                    )
+                                    self.logger.debug(log_message)
+
+                        if message.type == WSMsgType.TEXT:
+                            message_data = message.data
+                            await self.enqueue_message(message_data)
+                            for listener in self.on_message_listeners:
+                                await listener(message)
+                        elif message.type == WSMsgType.CLOSE:
+                            if self.auto_reconnect_enabled:
+                                self.logger.info(f"Received CLOSE event from {session_id}. Reconnecting...")
+                                await self.connect_to_new_endpoint()
+                            for listener in self.on_close_listeners:
+                                await listener(message)
+                        elif message.type == WSMsgType.ERROR:
+                            for listener in self.on_error_listeners:
+                                await listener(message)
+                        elif message.type == WSMsgType.CLOSED:
+                            await asyncio.sleep(self.ping_interval)
+                            continue
+                        elif message.type == WSMsgType.PING:
+                            await session.pong(message.data)  # type: ignore[union-attr]
+                            continue
+                        elif message.type == WSMsgType.PONG:
+                            if message.data is not None:
+                                str_message_data = message.data.decode("utf-8")
+                                elements = str_message_data.split(":")
+                                if len(elements) == 2 and elements[0] == "sdk-ping-pong":
+                                    try:
+                                        self.last_ping_pong_time = float(elements[1])
+                                    except Exception as e:
+                                        self.logger.warning(
+                                            f"Failed to parse the last_ping_pong_time value from {str_message_data}"
+                                            f" - error : {e}, session: {session_id}"
+                                        )
+                            continue
+
+                    consecutive_error_count = 0
+
+                except Exception as e:
+                    consecutive_error_count += 1
+                    self.logger.error(f"Failed to receive or enqueue a message: {type(e).__name__}, {e} ({session_id})")
+                    if isinstance(e, ClientConnectionError):
+                        await asyncio.sleep(self.ping_interval)
+                    else:
+                        await asyncio.sleep(consecutive_error_count)
+        except asyncio.CancelledError:
+            if self.logger.level <= logging.DEBUG:
+                self.logger.debug(f"The running receive_messages task for {session_id} is now cancelled")
+            raise
+
+    async def is_ping_pong_failing(self) -> bool:
+        if self.last_ping_pong_time is None:
+            return False
+        disconnected_seconds = int(time.time() - self.last_ping_pong_time)
+        return disconnected_seconds >= (self.ping_interval * 4)
+
+    async def is_connected(self) -> bool:
+        connected: bool = (
+            not self.closed
+            and not self.stale
+            and self.current_session is not None
+            and not self.current_session.closed
+            and not await self.is_ping_pong_failing()
+        )
+        if self.logger.level <= logging.DEBUG and connected is False:
+            # Prints more detailed information about the inactive connection
+            is_ping_pong_failing = await self.is_ping_pong_failing()
+            session_id = await self.session_id()
+            self.logger.debug(
+                "Inactive connection detected ("
+                f"session_id: {session_id}, "
+                f"closed: {self.closed}, "
+                f"stale: {self.stale}, "
+                f"current_session.closed: {self.current_session and self.current_session.closed}, "
+                f"is_ping_pong_failing: {is_ping_pong_failing}"
+                ")"
+            )
+        return connected
+
+    async def session_id(self) -> str:
+        return self.build_session_id(self.current_session)  # type: ignore[arg-type]
+
+    async def connect(self):
+        # This loop is used to ensure when a new session is created,
+        # a new monitor and a new message receiver are also created.
+        # If a new session is created but we failed to create the new
+        # monitor or the new message, we should try it.
+        while True:
+            try:
+                old_session: Optional[ClientWebSocketResponse] = (
+                    None if self.current_session is None else self.current_session
+                )
+
+                # If the old session is broken (e.g. reset by peer), it might fail to close it.
+                # We don't want to retry when this kind of cases happen.
+                try:
+                    # We should close old session before create a new one. Because when disconnect
+                    # reason is `too_many_websockets`, we need to close the old one first to
+                    # to decrease the number of connections.
+                    self.auto_reconnect_enabled = False
+                    if old_session is not None:
+                        await old_session.close()
+                        old_session_id = self.build_session_id(old_session)
+                        self.logger.info(f"The old session ({old_session_id}) has been abandoned")
+                except Exception as e:
+                    self.logger.exception(f"Failed to close the old session : {e}")
+
+                if self.wss_uri is None:
+                    # If the underlying WSS URL does not exist,
+                    # acquiring a new active WSS URL from the server-side first
+                    self.wss_uri = await self.issue_new_wss_url()
+
+                self.current_session = await self.aiohttp_client_session.ws_connect(
+                    self.wss_uri,
+                    autoping=False,
+                    heartbeat=self.ping_interval,
+                    proxy=self.proxy,
+                    ssl=self.web_client.ssl,
+                )
+                session_id: str = await self.session_id()
+                self.auto_reconnect_enabled = self.default_auto_reconnect_enabled
+                self.stale = False
+                self.logger.info(f"A new session ({session_id}) has been established")
+
+                # The first ping from the new connection
+                if self.logger.level <= logging.DEBUG:
+                    self.logger.debug(f"Sending a ping message with the newly established connection ({session_id})...")
+                t = time.time()
+                await self.current_session.ping(f"sdk-ping-pong:{t}".encode("utf-8"))
+
+                if self.current_session_monitor is not None:
+                    self.current_session_monitor.cancel()
+                self.current_session_monitor = asyncio.ensure_future(self.monitor_current_session())
+                if self.logger.level <= logging.DEBUG:
+                    self.logger.debug(f"A new monitor_current_session() executor has been recreated for {session_id}")
+
+                if self.message_receiver is not None:
+                    self.message_receiver.cancel()
+                self.message_receiver = asyncio.ensure_future(self.receive_messages())
+                if self.logger.level <= logging.DEBUG:
+                    self.logger.debug(f"A new receive_messages() executor has been recreated for {session_id}")
+                break
+            except Exception as e:
+                self.logger.exception(f"Failed to connect (error: {e}); Retrying...")
+                await asyncio.sleep(self.ping_interval)
+
+    async def disconnect(self):
+        if self.current_session is not None:
+            await self.current_session.close()
+        session_id = await self.session_id()
+        self.logger.info(f"The current session ({session_id}) has been abandoned by disconnect() method call")
+
+    async def send_message(self, message: str):
+        session_id = await self.session_id()
+        if self.logger.level <= logging.DEBUG:
+            self.logger.debug(f"Sending a message: {message} from session: {session_id}")
+        try:
+            await self.current_session.send_str(message)  # type: ignore[union-attr]
+        except ConnectionError as e:
+            # We rarely get this exception while replacing the underlying WebSocket connections.
+            # We can do one more try here as the self.current_session should be ready now.
+            if self.logger.level <= logging.DEBUG:
+                self.logger.debug(
+                    f"Failed to send a message (error: {e}, message: {message}, session: {session_id})"
+                    " as the underlying connection was replaced. Retrying the same request only one time..."
+                )
+            # Although acquiring self.connect_operation_lock also for the first method call is the safest way,
+            # we avoid synchronizing a lot for better performance. That's why we are doing a retry here.
+            try:
+                await self.connect_operation_lock.acquire()
+                if await self.is_connected():
+                    await self.current_session.send_str(message)  # type: ignore[union-attr]
+                else:
+                    self.logger.warning(
+                        f"The current session ({session_id}) is no longer active. " "Failed to send a message"
+                    )
+                    raise e
+            finally:
+                if self.connect_operation_lock.locked() is True:
+                    self.connect_operation_lock.release()
+
+    async def close(self):
+        self.closed = True
+        self.auto_reconnect_enabled = False
+        await self.disconnect()
+        if self.message_processor is not None:
+            self.message_processor.cancel()
+        if self.current_session_monitor is not None:
+            self.current_session_monitor.cancel()
+        if self.message_receiver is not None:
+            self.message_receiver.cancel()
+        if self.aiohttp_client_session is not None:
+            await self.aiohttp_client_session.close()
+
+    @classmethod
+    def build_session_id(cls, session: ClientWebSocketResponse) -> str:
+        if session is None:
+            return ""
+        return "s_" + str(hash(session))
+
+

Socket Mode client

+

Args

+
+
app_token
+
App-level token
+
logger
+
Custom logger
+
web_client
+
Web API client
+
auto_reconnect_enabled
+
True if automatic reconnection is enabled (default: True)
+
ping_interval
+
interval for ping-pong with Slack servers (seconds)
+
trace_enabled
+
True if more verbose logs to see what's happening under the hood
+
proxy
+
the HTTP proxy URL
+
on_message_listeners
+
listener functions for on_message
+
on_error_listeners
+
listener functions for on_error
+
on_close_listeners
+
listener functions for on_close
+
loop
+
an existing asyncio event loop
+
+

Ancestors

+ +

Class variables

+
+
var current_session : aiohttp.client_ws.ClientWebSocketResponse | None
+
+

The type of the None singleton.

+
+
var current_session_monitor : _asyncio.Future | None
+
+

The type of the None singleton.

+
+
var default_auto_reconnect_enabled : bool
+
+

The type of the None singleton.

+
+
var last_ping_pong_time : float | None
+
+

The type of the None singleton.

+
+
var message_processor : _asyncio.Future
+
+

The type of the None singleton.

+
+
var message_receiver : _asyncio.Future | None
+
+

The type of the None singleton.

+
+
var on_close_listeners : List[Callable[[aiohttp._websocket.models.WSMessage], Awaitable[None]]]
+
+

The type of the None singleton.

+
+
var on_error_listeners : List[Callable[[aiohttp._websocket.models.WSMessage], Awaitable[None]]]
+
+

The type of the None singleton.

+
+
var on_message_listeners : List[Callable[[aiohttp._websocket.models.WSMessage], Awaitable[None]]]
+
+

The type of the None singleton.

+
+
var ping_interval : float
+
+

The type of the None singleton.

+
+
var proxy : str | None
+
+

The type of the None singleton.

+
+
var stale : bool
+
+

The type of the None singleton.

+
+
+

Static methods

+
+
+def build_session_id(session: aiohttp.client_ws.ClientWebSocketResponse) ‑> str +
+
+
+
+
+

Methods

+
+
+async def close(self) +
+
+
+ +Expand source code + +
async def close(self):
+    self.closed = True
+    self.auto_reconnect_enabled = False
+    await self.disconnect()
+    if self.message_processor is not None:
+        self.message_processor.cancel()
+    if self.current_session_monitor is not None:
+        self.current_session_monitor.cancel()
+    if self.message_receiver is not None:
+        self.message_receiver.cancel()
+    if self.aiohttp_client_session is not None:
+        await self.aiohttp_client_session.close()
+
+
+
+
+async def connect(self) +
+
+
+ +Expand source code + +
async def connect(self):
+    # This loop is used to ensure when a new session is created,
+    # a new monitor and a new message receiver are also created.
+    # If a new session is created but we failed to create the new
+    # monitor or the new message, we should try it.
+    while True:
+        try:
+            old_session: Optional[ClientWebSocketResponse] = (
+                None if self.current_session is None else self.current_session
+            )
+
+            # If the old session is broken (e.g. reset by peer), it might fail to close it.
+            # We don't want to retry when this kind of cases happen.
+            try:
+                # We should close old session before create a new one. Because when disconnect
+                # reason is `too_many_websockets`, we need to close the old one first to
+                # to decrease the number of connections.
+                self.auto_reconnect_enabled = False
+                if old_session is not None:
+                    await old_session.close()
+                    old_session_id = self.build_session_id(old_session)
+                    self.logger.info(f"The old session ({old_session_id}) has been abandoned")
+            except Exception as e:
+                self.logger.exception(f"Failed to close the old session : {e}")
+
+            if self.wss_uri is None:
+                # If the underlying WSS URL does not exist,
+                # acquiring a new active WSS URL from the server-side first
+                self.wss_uri = await self.issue_new_wss_url()
+
+            self.current_session = await self.aiohttp_client_session.ws_connect(
+                self.wss_uri,
+                autoping=False,
+                heartbeat=self.ping_interval,
+                proxy=self.proxy,
+                ssl=self.web_client.ssl,
+            )
+            session_id: str = await self.session_id()
+            self.auto_reconnect_enabled = self.default_auto_reconnect_enabled
+            self.stale = False
+            self.logger.info(f"A new session ({session_id}) has been established")
+
+            # The first ping from the new connection
+            if self.logger.level <= logging.DEBUG:
+                self.logger.debug(f"Sending a ping message with the newly established connection ({session_id})...")
+            t = time.time()
+            await self.current_session.ping(f"sdk-ping-pong:{t}".encode("utf-8"))
+
+            if self.current_session_monitor is not None:
+                self.current_session_monitor.cancel()
+            self.current_session_monitor = asyncio.ensure_future(self.monitor_current_session())
+            if self.logger.level <= logging.DEBUG:
+                self.logger.debug(f"A new monitor_current_session() executor has been recreated for {session_id}")
+
+            if self.message_receiver is not None:
+                self.message_receiver.cancel()
+            self.message_receiver = asyncio.ensure_future(self.receive_messages())
+            if self.logger.level <= logging.DEBUG:
+                self.logger.debug(f"A new receive_messages() executor has been recreated for {session_id}")
+            break
+        except Exception as e:
+            self.logger.exception(f"Failed to connect (error: {e}); Retrying...")
+            await asyncio.sleep(self.ping_interval)
+
+
+
+
+async def disconnect(self) +
+
+
+ +Expand source code + +
async def disconnect(self):
+    if self.current_session is not None:
+        await self.current_session.close()
+    session_id = await self.session_id()
+    self.logger.info(f"The current session ({session_id}) has been abandoned by disconnect() method call")
+
+
+
+
+async def is_connected(self) ‑> bool +
+
+
+ +Expand source code + +
async def is_connected(self) -> bool:
+    connected: bool = (
+        not self.closed
+        and not self.stale
+        and self.current_session is not None
+        and not self.current_session.closed
+        and not await self.is_ping_pong_failing()
+    )
+    if self.logger.level <= logging.DEBUG and connected is False:
+        # Prints more detailed information about the inactive connection
+        is_ping_pong_failing = await self.is_ping_pong_failing()
+        session_id = await self.session_id()
+        self.logger.debug(
+            "Inactive connection detected ("
+            f"session_id: {session_id}, "
+            f"closed: {self.closed}, "
+            f"stale: {self.stale}, "
+            f"current_session.closed: {self.current_session and self.current_session.closed}, "
+            f"is_ping_pong_failing: {is_ping_pong_failing}"
+            ")"
+        )
+    return connected
+
+
+
+
+async def is_ping_pong_failing(self) ‑> bool +
+
+
+ +Expand source code + +
async def is_ping_pong_failing(self) -> bool:
+    if self.last_ping_pong_time is None:
+        return False
+    disconnected_seconds = int(time.time() - self.last_ping_pong_time)
+    return disconnected_seconds >= (self.ping_interval * 4)
+
+
+
+
+async def monitor_current_session(self) ‑> None +
+
+
+ +Expand source code + +
async def monitor_current_session(self) -> None:
+    # In the asyncio runtime, accessing a shared object (self.current_session here) from
+    # multiple tasks can cause race conditions and errors.
+    # To avoid such, we access only the session that is active when this loop starts.
+    session: ClientWebSocketResponse = self.current_session  # type: ignore[assignment]
+    session_id: str = self.build_session_id(session)
+
+    if self.logger.level <= logging.DEBUG:
+        self.logger.debug(f"A new monitor_current_session() execution loop for {session_id} started")
+    try:
+        logging_interval = 100
+        counter_for_logging = 0
+
+        while not self.closed:
+            if session != self.current_session:
+                if self.logger.level <= logging.DEBUG:
+                    self.logger.debug(f"The monitor_current_session task for {session_id} is now cancelled")
+                break
+            try:
+                if self.trace_enabled and self.logger.level <= logging.DEBUG:
+                    # The logging here is for detailed investigation on potential issues in this client.
+                    # If you don't see this log for a while, it means that
+                    # this receive_messages execution is no longer working for some reason.
+                    counter_for_logging += 1
+                    if counter_for_logging >= logging_interval:
+                        counter_for_logging = 0
+                        log_message = (
+                            "#monitor_current_session method has been verifying if this session is active "
+                            f"(session: {session_id}, logging interval: {logging_interval})"
+                        )
+                        self.logger.debug(log_message)
+
+                await asyncio.sleep(self.ping_interval)
+
+                if session is not None and session.closed is False:
+                    t = time.time()
+                    if self.last_ping_pong_time is None:
+                        self.last_ping_pong_time = float(t)
+                    try:
+                        await session.ping(f"sdk-ping-pong:{t}".encode("utf-8"))
+                    except Exception as e:
+                        # The ping() method can fail for some reason.
+                        # To establish a new connection even in this scenario,
+                        # we ignore the exception here.
+                        self.logger.warning(f"Failed to send a ping message ({session_id}): {e}")
+
+                if self.auto_reconnect_enabled:
+                    should_reconnect = False
+                    if session is None or session.closed:
+                        self.logger.info(f"The session ({session_id}) seems to be already closed. Reconnecting...")
+                        should_reconnect = True
+
+                    if await self.is_ping_pong_failing():
+                        disconnected_seconds = int(time.time() - self.last_ping_pong_time)  # type: ignore[operator]
+                        self.logger.info(
+                            f"The session ({session_id}) seems to be stale. Reconnecting..."
+                            f" reason: disconnected for {disconnected_seconds}+ seconds)"
+                        )
+                        self.stale = True
+                        self.last_ping_pong_time = None
+                        should_reconnect = True
+
+                    if should_reconnect is True or not await self.is_connected():
+                        await self.connect_to_new_endpoint()
+
+            except Exception as e:
+                self.logger.error(
+                    f"Failed to check the current session ({session_id}) or reconnect to the server "
+                    f"(error: {type(e).__name__}, message: {e})"
+                )
+    except asyncio.CancelledError:
+        if self.logger.level <= logging.DEBUG:
+            self.logger.debug(f"The monitor_current_session task for {session_id} is now cancelled")
+        raise
+
+
+
+
+async def receive_messages(self) ‑> None +
+
+
+ +Expand source code + +
async def receive_messages(self) -> None:
+    # In the asyncio runtime, accessing a shared object (self.current_session here) from
+    # multiple tasks can cause race conditions and errors.
+    # To avoid such, we access only the session that is active when this loop starts.
+    session = self.current_session
+    session_id = self.build_session_id(session)  # type: ignore[arg-type]
+    if self.logger.level <= logging.DEBUG:
+        self.logger.debug(f"A new receive_messages() execution loop with {session_id} started")
+    try:
+        consecutive_error_count = 0
+        logging_interval = 100
+        counter_for_logging = 0
+
+        while not self.closed:
+            if session != self.current_session:
+                if self.logger.level <= logging.DEBUG:
+                    self.logger.debug(f"The running receive_messages task for {session_id} is now cancelled")
+                break
+            try:
+                message: WSMessage = await session.receive()  # type: ignore[union-attr]
+                # just in case, checking if the value is not None
+                if message is not None:
+                    if self.logger.level <= logging.DEBUG:
+                        # The following logging prints every single received message
+                        # except empty message data ones.
+                        m_type = WSMsgType(message.type)
+                        message_type = m_type.name if m_type is not None else message.type
+                        message_data = message.data
+                        if isinstance(message_data, bytes):
+                            message_data = message_data.decode("utf-8")
+                        if message_data is not None and isinstance(message_data, (str, bytes)) and len(message_data) > 0:
+                            # To skip the empty message that Slack server-side often sends
+                            self.logger.debug(
+                                f"Received message "
+                                f"(type: {message_type}, "
+                                f"data: {message_data}, "
+                                f"extra: {message.extra}, "
+                                f"session: {session_id})"
+                            )
+
+                        if self.trace_enabled:
+                            # The logging here is for detailed trouble shooting of potential issues in this client.
+                            # If you don't see this log for a while, it can mean that
+                            # this receive_messages execution is no longer working for some reason.
+                            counter_for_logging += 1
+                            if counter_for_logging >= logging_interval:
+                                counter_for_logging = 0
+                                log_message = (
+                                    "#receive_messages method has been working without any issues "
+                                    f"(session: {session_id}, logging interval: {logging_interval})"
+                                )
+                                self.logger.debug(log_message)
+
+                    if message.type == WSMsgType.TEXT:
+                        message_data = message.data
+                        await self.enqueue_message(message_data)
+                        for listener in self.on_message_listeners:
+                            await listener(message)
+                    elif message.type == WSMsgType.CLOSE:
+                        if self.auto_reconnect_enabled:
+                            self.logger.info(f"Received CLOSE event from {session_id}. Reconnecting...")
+                            await self.connect_to_new_endpoint()
+                        for listener in self.on_close_listeners:
+                            await listener(message)
+                    elif message.type == WSMsgType.ERROR:
+                        for listener in self.on_error_listeners:
+                            await listener(message)
+                    elif message.type == WSMsgType.CLOSED:
+                        await asyncio.sleep(self.ping_interval)
+                        continue
+                    elif message.type == WSMsgType.PING:
+                        await session.pong(message.data)  # type: ignore[union-attr]
+                        continue
+                    elif message.type == WSMsgType.PONG:
+                        if message.data is not None:
+                            str_message_data = message.data.decode("utf-8")
+                            elements = str_message_data.split(":")
+                            if len(elements) == 2 and elements[0] == "sdk-ping-pong":
+                                try:
+                                    self.last_ping_pong_time = float(elements[1])
+                                except Exception as e:
+                                    self.logger.warning(
+                                        f"Failed to parse the last_ping_pong_time value from {str_message_data}"
+                                        f" - error : {e}, session: {session_id}"
+                                    )
+                        continue
+
+                consecutive_error_count = 0
+
+            except Exception as e:
+                consecutive_error_count += 1
+                self.logger.error(f"Failed to receive or enqueue a message: {type(e).__name__}, {e} ({session_id})")
+                if isinstance(e, ClientConnectionError):
+                    await asyncio.sleep(self.ping_interval)
+                else:
+                    await asyncio.sleep(consecutive_error_count)
+    except asyncio.CancelledError:
+        if self.logger.level <= logging.DEBUG:
+            self.logger.debug(f"The running receive_messages task for {session_id} is now cancelled")
+        raise
+
+
+
+
+async def send_message(self, message: str) +
+
+
+ +Expand source code + +
async def send_message(self, message: str):
+    session_id = await self.session_id()
+    if self.logger.level <= logging.DEBUG:
+        self.logger.debug(f"Sending a message: {message} from session: {session_id}")
+    try:
+        await self.current_session.send_str(message)  # type: ignore[union-attr]
+    except ConnectionError as e:
+        # We rarely get this exception while replacing the underlying WebSocket connections.
+        # We can do one more try here as the self.current_session should be ready now.
+        if self.logger.level <= logging.DEBUG:
+            self.logger.debug(
+                f"Failed to send a message (error: {e}, message: {message}, session: {session_id})"
+                " as the underlying connection was replaced. Retrying the same request only one time..."
+            )
+        # Although acquiring self.connect_operation_lock also for the first method call is the safest way,
+        # we avoid synchronizing a lot for better performance. That's why we are doing a retry here.
+        try:
+            await self.connect_operation_lock.acquire()
+            if await self.is_connected():
+                await self.current_session.send_str(message)  # type: ignore[union-attr]
+            else:
+                self.logger.warning(
+                    f"The current session ({session_id}) is no longer active. " "Failed to send a message"
+                )
+                raise e
+        finally:
+            if self.connect_operation_lock.locked() is True:
+                self.connect_operation_lock.release()
+
+
+
+
+async def session_id(self) ‑> str +
+
+
+ +Expand source code + +
async def session_id(self) -> str:
+    return self.build_session_id(self.current_session)  # type: ignore[arg-type]
+
+
+
+
+

Inherited members

+ +
+
+
+
+ +
+ + + diff --git a/docs/reference/socket_mode/async_client.html b/docs/reference/socket_mode/async_client.html new file mode 100644 index 000000000..20d07951a --- /dev/null +++ b/docs/reference/socket_mode/async_client.html @@ -0,0 +1,575 @@ + + + + + + +slack_sdk.socket_mode.async_client API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.socket_mode.async_client

+
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class AsyncBaseSocketModeClient +
+
+
+ +Expand source code + +
class AsyncBaseSocketModeClient:
+    logger: Logger
+    web_client: AsyncWebClient
+    app_token: str
+    wss_uri: str
+    auto_reconnect_enabled: bool
+    trace_enabled: bool
+    closed: bool
+    connect_operation_lock: Lock
+
+    message_queue: Queue
+    message_listeners: List[
+        Union[
+            AsyncWebSocketMessageListener,
+            Callable[["AsyncBaseSocketModeClient", dict, Optional[str]], Awaitable[None]],
+        ]
+    ]
+    socket_mode_request_listeners: List[
+        Union[
+            AsyncSocketModeRequestListener,
+            Callable[["AsyncBaseSocketModeClient", SocketModeRequest], Awaitable[None]],
+        ]
+    ]
+
+    async def issue_new_wss_url(self) -> str:
+        try:
+            response = await self.web_client.apps_connections_open(app_token=self.app_token)
+            return response["url"]
+        except SlackApiError as e:
+            if e.response["error"] == "ratelimited":
+                # NOTE: ratelimited errors rarely occur with this endpoint
+                delay = int(e.response.headers.get("Retry-After", "30"))  # Tier1
+                self.logger.info(f"Rate limited. Retrying in {delay} seconds...")
+                await asyncio.sleep(delay)
+                # Retry to issue a new WSS URL
+                return await self.issue_new_wss_url()
+            else:
+                # other errors
+                self.logger.error(f"Failed to retrieve WSS URL: {e}")
+                raise e
+
+    async def is_connected(self) -> bool:
+        return False
+
+    async def session_id(self) -> str:
+        return ""
+
+    async def connect(self):
+        raise NotImplementedError()
+
+    async def disconnect(self):
+        raise NotImplementedError()
+
+    async def connect_to_new_endpoint(self, force: bool = False):
+        session_id = await self.session_id()
+        try:
+            await self.connect_operation_lock.acquire()
+            if self.trace_enabled:
+                self.logger.debug(f"For reconnection, the connect_operation_lock was acquired (session: {session_id})")
+            if force or not await self.is_connected():
+                self.wss_uri = await self.issue_new_wss_url()
+                await self.connect()
+        finally:
+            if self.connect_operation_lock.locked() is True:
+                self.connect_operation_lock.release()
+                if self.trace_enabled:
+                    self.logger.debug(f"The connect_operation_lock for reconnection was released (session: {session_id})")
+
+    async def close(self):
+        self.closed = True
+        await self.disconnect()
+
+    async def send_message(self, message: str):
+        raise NotImplementedError()
+
+    async def send_socket_mode_response(self, response: Union[Dict[str, Any], SocketModeResponse]):
+        if isinstance(response, SocketModeResponse):
+            await self.send_message(json.dumps(response.to_dict()))
+        else:
+            await self.send_message(json.dumps(response))
+
+    async def enqueue_message(self, message: str):
+        await self.message_queue.put(message)
+        if self.logger.level <= logging.DEBUG:
+            queue_size = self.message_queue.qsize()
+            session_id = await self.session_id()
+            self.logger.debug(f"A new message enqueued (current queue size: {queue_size}, session: {session_id})")
+
+    async def process_messages(self):
+        session_id = await self.session_id()
+        try:
+            while not self.closed:
+                try:
+                    await self.process_message()
+                except asyncio.CancelledError:
+                    # if self.closed is True, the connection is already closed
+                    # In this case, we can ignore the exception here
+                    if not self.closed:
+                        raise
+                except Exception as e:
+                    self.logger.exception(f"Failed to process a message: {e}, session: {session_id}")
+        except asyncio.CancelledError:
+            if self.trace_enabled:
+                self.logger.debug(f"The running process_messages task for {session_id} is now cancelled")
+            raise
+
+    async def process_message(self):
+        raw_message = await self.message_queue.get()
+        if raw_message is not None:
+            message: dict = {}
+            if raw_message.startswith("{"):
+                message = json.loads(raw_message)
+            _: Future[None] = asyncio.ensure_future(self.run_message_listeners(message, raw_message))
+
+    async def run_message_listeners(self, message: dict, raw_message: str) -> None:
+        session_id = await self.session_id()
+        type, envelope_id = message.get("type"), message.get("envelope_id")
+        if self.logger.level <= logging.DEBUG:
+            self.logger.debug(
+                f"Message processing started (type: {type}, envelope_id: {envelope_id}, session: {session_id})"
+            )
+        try:
+            if message.get("type") == "disconnect":
+                await self.connect_to_new_endpoint(force=True)
+                return
+
+            for listener in self.message_listeners:
+                try:
+                    await listener(self, message, raw_message)  # type: ignore[call-arg, arg-type, misc]
+                except Exception as e:
+                    self.logger.exception(f"Failed to run a message listener: {e}, session: {session_id}")
+
+            if len(self.socket_mode_request_listeners) > 0:
+                request = SocketModeRequest.from_dict(message)
+                if request is not None:
+                    for listener in self.socket_mode_request_listeners:  # type: ignore[assignment]
+                        try:
+                            await listener(self, request)  # type: ignore[call-arg, arg-type]
+                        except Exception as e:
+                            self.logger.exception(f"Failed to run a request listener: {e}, session: {session_id}")
+        except Exception as e:
+            self.logger.exception(f"Failed to run message listeners: {e}, session: {session_id}")
+        finally:
+            if self.logger.level <= logging.DEBUG:
+                self.logger.debug(
+                    f"Message processing completed ("
+                    f"type: {type}, "
+                    f"envelope_id: {envelope_id}, "
+                    f"session: {session_id})"
+                )
+
+
+

Subclasses

+ +

Class variables

+
+
var app_token : str
+
+

The type of the None singleton.

+
+
var auto_reconnect_enabled : bool
+
+

The type of the None singleton.

+
+
var closed : bool
+
+

The type of the None singleton.

+
+
var connect_operation_lock : asyncio.locks.Lock
+
+

The type of the None singleton.

+
+
var logger : logging.Logger
+
+

The type of the None singleton.

+
+
var message_listeners : List[AsyncWebSocketMessageListener | Callable[[AsyncBaseSocketModeClient, dict, str | None], Awaitable[None]]]
+
+

The type of the None singleton.

+
+
var message_queue : asyncio.queues.Queue
+
+

The type of the None singleton.

+
+
var socket_mode_request_listeners : List[AsyncSocketModeRequestListener | Callable[[AsyncBaseSocketModeClientSocketModeRequest], Awaitable[None]]]
+
+

The type of the None singleton.

+
+
var trace_enabled : bool
+
+

The type of the None singleton.

+
+
var web_clientAsyncWebClient
+
+

The type of the None singleton.

+
+
var wss_uri : str
+
+

The type of the None singleton.

+
+
+

Methods

+
+
+async def close(self) +
+
+
+ +Expand source code + +
async def close(self):
+    self.closed = True
+    await self.disconnect()
+
+
+
+
+async def connect(self) +
+
+
+ +Expand source code + +
async def connect(self):
+    raise NotImplementedError()
+
+
+
+
+async def connect_to_new_endpoint(self, force: bool = False) +
+
+
+ +Expand source code + +
async def connect_to_new_endpoint(self, force: bool = False):
+    session_id = await self.session_id()
+    try:
+        await self.connect_operation_lock.acquire()
+        if self.trace_enabled:
+            self.logger.debug(f"For reconnection, the connect_operation_lock was acquired (session: {session_id})")
+        if force or not await self.is_connected():
+            self.wss_uri = await self.issue_new_wss_url()
+            await self.connect()
+    finally:
+        if self.connect_operation_lock.locked() is True:
+            self.connect_operation_lock.release()
+            if self.trace_enabled:
+                self.logger.debug(f"The connect_operation_lock for reconnection was released (session: {session_id})")
+
+
+
+
+async def disconnect(self) +
+
+
+ +Expand source code + +
async def disconnect(self):
+    raise NotImplementedError()
+
+
+
+
+async def enqueue_message(self, message: str) +
+
+
+ +Expand source code + +
async def enqueue_message(self, message: str):
+    await self.message_queue.put(message)
+    if self.logger.level <= logging.DEBUG:
+        queue_size = self.message_queue.qsize()
+        session_id = await self.session_id()
+        self.logger.debug(f"A new message enqueued (current queue size: {queue_size}, session: {session_id})")
+
+
+
+
+async def is_connected(self) ‑> bool +
+
+
+ +Expand source code + +
async def is_connected(self) -> bool:
+    return False
+
+
+
+
+async def issue_new_wss_url(self) ‑> str +
+
+
+ +Expand source code + +
async def issue_new_wss_url(self) -> str:
+    try:
+        response = await self.web_client.apps_connections_open(app_token=self.app_token)
+        return response["url"]
+    except SlackApiError as e:
+        if e.response["error"] == "ratelimited":
+            # NOTE: ratelimited errors rarely occur with this endpoint
+            delay = int(e.response.headers.get("Retry-After", "30"))  # Tier1
+            self.logger.info(f"Rate limited. Retrying in {delay} seconds...")
+            await asyncio.sleep(delay)
+            # Retry to issue a new WSS URL
+            return await self.issue_new_wss_url()
+        else:
+            # other errors
+            self.logger.error(f"Failed to retrieve WSS URL: {e}")
+            raise e
+
+
+
+
+async def process_message(self) +
+
+
+ +Expand source code + +
async def process_message(self):
+    raw_message = await self.message_queue.get()
+    if raw_message is not None:
+        message: dict = {}
+        if raw_message.startswith("{"):
+            message = json.loads(raw_message)
+        _: Future[None] = asyncio.ensure_future(self.run_message_listeners(message, raw_message))
+
+
+
+
+async def process_messages(self) +
+
+
+ +Expand source code + +
async def process_messages(self):
+    session_id = await self.session_id()
+    try:
+        while not self.closed:
+            try:
+                await self.process_message()
+            except asyncio.CancelledError:
+                # if self.closed is True, the connection is already closed
+                # In this case, we can ignore the exception here
+                if not self.closed:
+                    raise
+            except Exception as e:
+                self.logger.exception(f"Failed to process a message: {e}, session: {session_id}")
+    except asyncio.CancelledError:
+        if self.trace_enabled:
+            self.logger.debug(f"The running process_messages task for {session_id} is now cancelled")
+        raise
+
+
+
+
+async def run_message_listeners(self, message: dict, raw_message: str) ‑> None +
+
+
+ +Expand source code + +
async def run_message_listeners(self, message: dict, raw_message: str) -> None:
+    session_id = await self.session_id()
+    type, envelope_id = message.get("type"), message.get("envelope_id")
+    if self.logger.level <= logging.DEBUG:
+        self.logger.debug(
+            f"Message processing started (type: {type}, envelope_id: {envelope_id}, session: {session_id})"
+        )
+    try:
+        if message.get("type") == "disconnect":
+            await self.connect_to_new_endpoint(force=True)
+            return
+
+        for listener in self.message_listeners:
+            try:
+                await listener(self, message, raw_message)  # type: ignore[call-arg, arg-type, misc]
+            except Exception as e:
+                self.logger.exception(f"Failed to run a message listener: {e}, session: {session_id}")
+
+        if len(self.socket_mode_request_listeners) > 0:
+            request = SocketModeRequest.from_dict(message)
+            if request is not None:
+                for listener in self.socket_mode_request_listeners:  # type: ignore[assignment]
+                    try:
+                        await listener(self, request)  # type: ignore[call-arg, arg-type]
+                    except Exception as e:
+                        self.logger.exception(f"Failed to run a request listener: {e}, session: {session_id}")
+    except Exception as e:
+        self.logger.exception(f"Failed to run message listeners: {e}, session: {session_id}")
+    finally:
+        if self.logger.level <= logging.DEBUG:
+            self.logger.debug(
+                f"Message processing completed ("
+                f"type: {type}, "
+                f"envelope_id: {envelope_id}, "
+                f"session: {session_id})"
+            )
+
+
+
+
+async def send_message(self, message: str) +
+
+
+ +Expand source code + +
async def send_message(self, message: str):
+    raise NotImplementedError()
+
+
+
+
+async def send_socket_mode_response(self,
response: Dict[str, Any] | SocketModeResponse)
+
+
+
+ +Expand source code + +
async def send_socket_mode_response(self, response: Union[Dict[str, Any], SocketModeResponse]):
+    if isinstance(response, SocketModeResponse):
+        await self.send_message(json.dumps(response.to_dict()))
+    else:
+        await self.send_message(json.dumps(response))
+
+
+
+
+async def session_id(self) ‑> str +
+
+
+ +Expand source code + +
async def session_id(self) -> str:
+    return ""
+
+
+
+
+
+
+
+
+ +
+ + + diff --git a/docs/reference/socket_mode/async_listeners.html b/docs/reference/socket_mode/async_listeners.html new file mode 100644 index 000000000..e78b3c29e --- /dev/null +++ b/docs/reference/socket_mode/async_listeners.html @@ -0,0 +1,158 @@ + + + + + + +slack_sdk.socket_mode.async_listeners API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.socket_mode.async_listeners

+
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class AsyncSocketModeRequestListener +
+
+
+ +Expand source code + +
class AsyncSocketModeRequestListener(Callable):  # type: ignore[misc]
+    async def __call__(
+        client: "AsyncBaseSocketModeClient",  # type: ignore[name-defined] # noqa: F821
+        request: SocketModeRequest,
+    ):  # noqa: F821
+        raise NotImplementedError()
+
+

Abstract base class for generic types.

+

On Python 3.12 and newer, generic classes implicitly inherit from +Generic when they declare a parameter list after the class's name::

+
class Mapping[KT, VT]:
+    def __getitem__(self, key: KT) -> VT:
+        ...
+    # Etc.
+
+

On older versions of Python, however, generic classes have to +explicitly inherit from Generic.

+

After a class has been declared to be generic, it can then be used as +follows::

+
def lookup_name[KT, VT](mapping: Mapping[KT, VT], key: KT, default: VT) -> VT:
+    try:
+        return mapping[key]
+    except KeyError:
+        return default
+
+

Ancestors

+
    +
  • collections.abc.Callable
  • +
  • typing.Generic
  • +
+
+
+class AsyncWebSocketMessageListener +
+
+
+ +Expand source code + +
class AsyncWebSocketMessageListener(Callable):  # type: ignore[misc]
+    async def __call__(
+        client: "AsyncBaseSocketModeClient",  # type: ignore[name-defined] # noqa: F821
+        message: dict,
+        raw_message: Optional[str] = None,
+    ):  # noqa: F821
+        raise NotImplementedError()
+
+

Abstract base class for generic types.

+

On Python 3.12 and newer, generic classes implicitly inherit from +Generic when they declare a parameter list after the class's name::

+
class Mapping[KT, VT]:
+    def __getitem__(self, key: KT) -> VT:
+        ...
+    # Etc.
+
+

On older versions of Python, however, generic classes have to +explicitly inherit from Generic.

+

After a class has been declared to be generic, it can then be used as +follows::

+
def lookup_name[KT, VT](mapping: Mapping[KT, VT], key: KT, default: VT) -> VT:
+    try:
+        return mapping[key]
+    except KeyError:
+        return default
+
+

Ancestors

+
    +
  • collections.abc.Callable
  • +
  • typing.Generic
  • +
+
+
+
+
+ +
+ + + diff --git a/docs/reference/socket_mode/builtin/client.html b/docs/reference/socket_mode/builtin/client.html new file mode 100644 index 000000000..8e55f2347 --- /dev/null +++ b/docs/reference/socket_mode/builtin/client.html @@ -0,0 +1,631 @@ + + + + + + +slack_sdk.socket_mode.builtin.client API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.socket_mode.builtin.client

+
+
+

The built-in Socket Mode client

+ +
+
+
+
+
+
+
+
+

Classes

+
+
+class SocketModeClient +(app_token: str,
logger: logging.Logger | None = None,
web_client: WebClient | None = None,
auto_reconnect_enabled: bool = True,
trace_enabled: bool = False,
all_message_trace_enabled: bool = False,
ping_pong_trace_enabled: bool = False,
ping_interval: float = 5,
receive_buffer_size: int = 1024,
concurrency: int = 10,
proxy: str | None = None,
proxy_headers: Dict[str, str] | None = None,
on_message_listeners: List[Callable[[str], None]] | None = None,
on_error_listeners: List[Callable[[Exception], None]] | None = None,
on_close_listeners: List[Callable[[int, str | None], None]] | None = None)
+
+
+
+ +Expand source code + +
class SocketModeClient(BaseSocketModeClient):
+    logger: Logger
+    web_client: WebClient
+    app_token: str
+    wss_uri: Optional[str]  # type: ignore[assignment]
+    message_queue: Queue
+    message_listeners: List[
+        Union[
+            WebSocketMessageListener,
+            Callable[["BaseSocketModeClient", dict, Optional[str]], None],
+        ]
+    ]
+    socket_mode_request_listeners: List[
+        Union[
+            SocketModeRequestListener,
+            Callable[["BaseSocketModeClient", SocketModeRequest], None],
+        ]
+    ]
+
+    current_session: Optional[Connection]
+    current_session_state: ConnectionState
+    current_session_runner: IntervalRunner
+
+    current_app_monitor: IntervalRunner
+    current_app_monitor_started: bool
+
+    message_processor: IntervalRunner
+    message_workers: ThreadPoolExecutor
+
+    auto_reconnect_enabled: bool
+    default_auto_reconnect_enabled: bool
+    trace_enabled: bool
+    receive_buffer_size: int  # bytes size
+
+    connect_operation_lock: Lock
+
+    on_message_listeners: List[Callable[[str], None]]
+    on_error_listeners: List[Callable[[Exception], None]]
+    on_close_listeners: List[Callable[[int, Optional[str]], None]]
+
+    def __init__(
+        self,
+        app_token: str,
+        logger: Optional[Logger] = None,
+        web_client: Optional[WebClient] = None,
+        auto_reconnect_enabled: bool = True,
+        trace_enabled: bool = False,
+        all_message_trace_enabled: bool = False,
+        ping_pong_trace_enabled: bool = False,
+        ping_interval: float = 5,
+        receive_buffer_size: int = 1024,
+        concurrency: int = 10,
+        proxy: Optional[str] = None,
+        proxy_headers: Optional[Dict[str, str]] = None,
+        on_message_listeners: Optional[List[Callable[[str], None]]] = None,
+        on_error_listeners: Optional[List[Callable[[Exception], None]]] = None,
+        on_close_listeners: Optional[List[Callable[[int, Optional[str]], None]]] = None,
+    ):
+        """Socket Mode client
+
+        Args:
+            app_token: App-level token
+            logger: Custom logger
+            web_client: Web API client
+            auto_reconnect_enabled: True if automatic reconnection is enabled (default: True)
+            trace_enabled: True if more detailed debug-logging is enabled (default: False)
+            all_message_trace_enabled: True if all message dump in debug logs is enabled (default: False)
+            ping_pong_trace_enabled: True if trace logging for all ping-pong communications is enabled (default: False)
+            ping_interval: interval for ping-pong with Slack servers (seconds)
+            receive_buffer_size: the chunk size of a single socket recv operation (default: 1024)
+            concurrency: the size of thread pool (default: 10)
+            proxy: the HTTP proxy URL
+            proxy_headers: additional HTTP header for proxy connection
+            on_message_listeners: listener functions for on_message
+            on_error_listeners: listener functions for on_error
+            on_close_listeners: listener functions for on_close
+        """
+        self.app_token = app_token
+        self.logger = logger or logging.getLogger(__name__)
+        self.web_client = web_client or WebClient()
+        self.default_auto_reconnect_enabled = auto_reconnect_enabled
+        self.auto_reconnect_enabled = self.default_auto_reconnect_enabled
+        self.trace_enabled = trace_enabled
+        self.all_message_trace_enabled = all_message_trace_enabled
+        self.ping_pong_trace_enabled = ping_pong_trace_enabled
+        self.ping_interval = ping_interval
+        self.receive_buffer_size = receive_buffer_size
+        if self.receive_buffer_size < 16:
+            raise SlackClientConfigurationError("Too small receive_buffer_size detected.")
+
+        self.wss_uri = None
+        self.message_queue = Queue()
+        self.message_listeners = []
+        self.socket_mode_request_listeners = []
+
+        self.current_session = None
+        self.current_session_state = ConnectionState()
+        self.current_session_runner = IntervalRunner(self._run_current_session, 0.1).start()
+
+        self.current_app_monitor_started = False
+        self.current_app_monitor = IntervalRunner(self._monitor_current_session, self.ping_interval)
+
+        self.closed = False
+        self.connect_operation_lock = Lock()
+
+        self.message_processor = IntervalRunner(self.process_messages, 0.001).start()
+        self.message_workers = ThreadPoolExecutor(max_workers=concurrency)
+
+        self.proxy = proxy
+        if self.proxy is None or len(self.proxy.strip()) == 0:
+            env_variable = load_http_proxy_from_env(self.logger)
+            if env_variable is not None:
+                self.proxy = env_variable
+        self.proxy_headers = proxy_headers
+
+        self.on_message_listeners = on_message_listeners or []
+        self.on_error_listeners = on_error_listeners or []
+        self.on_close_listeners = on_close_listeners or []
+
+    def session_id(self) -> Optional[str]:
+        if self.current_session is not None:
+            return self.current_session.session_id
+        return None
+
+    def is_connected(self) -> bool:
+        return self.current_session is not None and self.current_session.is_active()
+
+    def connect(self) -> None:
+        old_session: Optional[Connection] = self.current_session
+        old_current_session_state: ConnectionState = self.current_session_state
+
+        if self.wss_uri is None:
+            self.wss_uri = self.issue_new_wss_url()
+
+        current_session = Connection(
+            url=self.wss_uri,
+            logger=self.logger,
+            ping_interval=self.ping_interval,
+            trace_enabled=self.trace_enabled,
+            all_message_trace_enabled=self.all_message_trace_enabled,
+            ping_pong_trace_enabled=self.ping_pong_trace_enabled,
+            receive_buffer_size=self.receive_buffer_size,
+            proxy=self.proxy,
+            proxy_headers=self.proxy_headers,
+            on_message_listener=self._on_message,
+            on_error_listener=self._on_error,
+            on_close_listener=self._on_close,
+            ssl_context=self.web_client.ssl,
+        )
+        current_session.connect()
+
+        if old_current_session_state is not None:
+            old_current_session_state.terminated = True
+        if old_session is not None:
+            old_session.close()
+
+        self.current_session = current_session
+        self.current_session_state = ConnectionState()
+        self.auto_reconnect_enabled = self.default_auto_reconnect_enabled
+
+        if not self.current_app_monitor_started:
+            self.current_app_monitor_started = True
+            self.current_app_monitor.start()
+
+        self.logger.info(f"A new session has been established (session id: {self.session_id()})")
+
+    def disconnect(self) -> None:
+        if self.current_session is not None:
+            self.current_session.close()
+
+    def send_message(self, message: str) -> None:
+        if self.logger.level <= logging.DEBUG:
+            self.logger.debug(f"Sending a message (session id: {self.session_id()}, message: {message})")
+        try:
+            self.current_session.send(message)  # type: ignore[union-attr]
+        except SlackClientNotConnectedError as e:
+            # We rarely get this exception while replacing the underlying WebSocket connections.
+            # We can do one more try here as the self.current_session should be ready now.
+            if self.logger.level <= logging.DEBUG:
+                self.logger.debug(
+                    f"Failed to send a message (session id: {self.session_id()}, error: {e}, message: {message})"
+                    " as the underlying connection was replaced. Retrying the same request only one time..."
+                )
+            # Although acquiring self.connect_operation_lock also for the first method call is the safest way,
+            # we avoid synchronizing a lot for better performance. That's why we are doing a retry here.
+            with self.connect_operation_lock:
+                if self.is_connected():
+                    self.current_session.send(message)  # type: ignore[union-attr]
+                else:
+                    self.logger.warning(
+                        f"The current session (session id: {self.session_id()}) is no longer active. "
+                        "Failed to send a message"
+                    )
+                    raise e
+
+    def close(self):
+        self.closed = True
+        self.auto_reconnect_enabled = False
+        self.disconnect()
+        if self.current_app_monitor.is_alive():
+            self.current_app_monitor.shutdown()
+        if self.message_processor.is_alive():
+            self.message_processor.shutdown()
+        self.message_workers.shutdown()
+
+    def _on_message(self, message: str):
+        if self.logger.level <= logging.DEBUG:
+            self.logger.debug(f"on_message invoked: (message: {debug_redacted_message_string(message)})")
+        self.enqueue_message(message)
+        for listener in self.on_message_listeners:
+            listener(message)
+
+    def _on_error(self, error: Exception):
+        error_message = (
+            f"on_error invoked (session id: {self.session_id()}, " f"error: {type(error).__name__}, message: {error})"
+        )
+        if self.trace_enabled:
+            self.logger.exception(error_message)
+        else:
+            self.logger.error(error_message)
+
+        for listener in self.on_error_listeners:
+            listener(error)
+
+    def _on_close(self, code: int, reason: Optional[str] = None):
+        if self.logger.level <= logging.DEBUG:
+            self.logger.debug(f"on_close invoked (session id: {self.session_id()})")
+        if self.auto_reconnect_enabled:
+            self.logger.info("Received CLOSE event. Reconnecting... " f"(session id: {self.session_id()})")
+            self.connect_to_new_endpoint()
+        for listener in self.on_close_listeners:
+            listener(code, reason)
+
+    def _run_current_session(self):
+        if self.current_session is not None and self.current_session.is_active():
+            session_id = self.session_id()
+            try:
+                self.logger.info("Starting to receive messages from a new connection" f" (session id: {session_id})")
+                self.current_session_state.terminated = False
+                self.current_session.run_until_completion(self.current_session_state)
+                self.logger.info("Stopped receiving messages from a connection" f" (session id: {session_id})")
+            except Exception as e:
+                error_message = "Failed to start or stop the current session" f" (session id: {session_id}, error: {e})"
+                if self.trace_enabled:
+                    self.logger.exception(error_message)
+                else:
+                    self.logger.error(error_message)
+
+    def _monitor_current_session(self):
+        if self.current_app_monitor_started:
+            try:
+                self.current_session.check_state()
+
+                if self.auto_reconnect_enabled and (self.current_session is None or not self.current_session.is_active()):
+                    self.logger.info(
+                        "The session seems to be already closed. Reconnecting... " f"(session id: {self.session_id()})"
+                    )
+                    self.connect_to_new_endpoint()
+            except Exception as e:
+                self.logger.error(
+                    "Failed to check the current session or reconnect to the server "
+                    f"(session id: {self.session_id()}, error: {type(e).__name__}, message: {e})"
+                )
+
+

Socket Mode client

+

Args

+
+
app_token
+
App-level token
+
logger
+
Custom logger
+
web_client
+
Web API client
+
auto_reconnect_enabled
+
True if automatic reconnection is enabled (default: True)
+
trace_enabled
+
True if more detailed debug-logging is enabled (default: False)
+
all_message_trace_enabled
+
True if all message dump in debug logs is enabled (default: False)
+
ping_pong_trace_enabled
+
True if trace logging for all ping-pong communications is enabled (default: False)
+
ping_interval
+
interval for ping-pong with Slack servers (seconds)
+
receive_buffer_size
+
the chunk size of a single socket recv operation (default: 1024)
+
concurrency
+
the size of thread pool (default: 10)
+
proxy
+
the HTTP proxy URL
+
proxy_headers
+
additional HTTP header for proxy connection
+
on_message_listeners
+
listener functions for on_message
+
on_error_listeners
+
listener functions for on_error
+
on_close_listeners
+
listener functions for on_close
+
+

Ancestors

+ +

Class variables

+
+
var auto_reconnect_enabled : bool
+
+

The type of the None singleton.

+
+
var current_app_monitorIntervalRunner
+
+

The type of the None singleton.

+
+
var current_app_monitor_started : bool
+
+

The type of the None singleton.

+
+
var current_sessionConnection | None
+
+

The type of the None singleton.

+
+
var current_session_runnerIntervalRunner
+
+

The type of the None singleton.

+
+
var current_session_stateConnectionState
+
+

The type of the None singleton.

+
+
var default_auto_reconnect_enabled : bool
+
+

The type of the None singleton.

+
+
var on_close_listeners : List[Callable[[int, str | None], None]]
+
+

The type of the None singleton.

+
+
var on_error_listeners : List[Callable[[Exception], None]]
+
+

The type of the None singleton.

+
+
var on_message_listeners : List[Callable[[str], None]]
+
+

The type of the None singleton.

+
+
var receive_buffer_size : int
+
+

The type of the None singleton.

+
+
var trace_enabled : bool
+
+

The type of the None singleton.

+
+
+

Methods

+
+
+def close(self) +
+
+
+ +Expand source code + +
def close(self):
+    self.closed = True
+    self.auto_reconnect_enabled = False
+    self.disconnect()
+    if self.current_app_monitor.is_alive():
+        self.current_app_monitor.shutdown()
+    if self.message_processor.is_alive():
+        self.message_processor.shutdown()
+    self.message_workers.shutdown()
+
+
+
+
+def connect(self) ‑> None +
+
+
+ +Expand source code + +
def connect(self) -> None:
+    old_session: Optional[Connection] = self.current_session
+    old_current_session_state: ConnectionState = self.current_session_state
+
+    if self.wss_uri is None:
+        self.wss_uri = self.issue_new_wss_url()
+
+    current_session = Connection(
+        url=self.wss_uri,
+        logger=self.logger,
+        ping_interval=self.ping_interval,
+        trace_enabled=self.trace_enabled,
+        all_message_trace_enabled=self.all_message_trace_enabled,
+        ping_pong_trace_enabled=self.ping_pong_trace_enabled,
+        receive_buffer_size=self.receive_buffer_size,
+        proxy=self.proxy,
+        proxy_headers=self.proxy_headers,
+        on_message_listener=self._on_message,
+        on_error_listener=self._on_error,
+        on_close_listener=self._on_close,
+        ssl_context=self.web_client.ssl,
+    )
+    current_session.connect()
+
+    if old_current_session_state is not None:
+        old_current_session_state.terminated = True
+    if old_session is not None:
+        old_session.close()
+
+    self.current_session = current_session
+    self.current_session_state = ConnectionState()
+    self.auto_reconnect_enabled = self.default_auto_reconnect_enabled
+
+    if not self.current_app_monitor_started:
+        self.current_app_monitor_started = True
+        self.current_app_monitor.start()
+
+    self.logger.info(f"A new session has been established (session id: {self.session_id()})")
+
+
+
+
+def disconnect(self) ‑> None +
+
+
+ +Expand source code + +
def disconnect(self) -> None:
+    if self.current_session is not None:
+        self.current_session.close()
+
+
+
+
+def is_connected(self) ‑> bool +
+
+
+ +Expand source code + +
def is_connected(self) -> bool:
+    return self.current_session is not None and self.current_session.is_active()
+
+
+
+
+def send_message(self, message: str) ‑> None +
+
+
+ +Expand source code + +
def send_message(self, message: str) -> None:
+    if self.logger.level <= logging.DEBUG:
+        self.logger.debug(f"Sending a message (session id: {self.session_id()}, message: {message})")
+    try:
+        self.current_session.send(message)  # type: ignore[union-attr]
+    except SlackClientNotConnectedError as e:
+        # We rarely get this exception while replacing the underlying WebSocket connections.
+        # We can do one more try here as the self.current_session should be ready now.
+        if self.logger.level <= logging.DEBUG:
+            self.logger.debug(
+                f"Failed to send a message (session id: {self.session_id()}, error: {e}, message: {message})"
+                " as the underlying connection was replaced. Retrying the same request only one time..."
+            )
+        # Although acquiring self.connect_operation_lock also for the first method call is the safest way,
+        # we avoid synchronizing a lot for better performance. That's why we are doing a retry here.
+        with self.connect_operation_lock:
+            if self.is_connected():
+                self.current_session.send(message)  # type: ignore[union-attr]
+            else:
+                self.logger.warning(
+                    f"The current session (session id: {self.session_id()}) is no longer active. "
+                    "Failed to send a message"
+                )
+                raise e
+
+
+
+
+def session_id(self) ‑> str | None +
+
+
+ +Expand source code + +
def session_id(self) -> Optional[str]:
+    if self.current_session is not None:
+        return self.current_session.session_id
+    return None
+
+
+
+
+

Inherited members

+ +
+
+
+
+ +
+ + + diff --git a/docs/reference/socket_mode/builtin/connection.html b/docs/reference/socket_mode/builtin/connection.html new file mode 100644 index 000000000..5c85bef91 --- /dev/null +++ b/docs/reference/socket_mode/builtin/connection.html @@ -0,0 +1,1061 @@ + + + + + + +slack_sdk.socket_mode.builtin.connection API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.socket_mode.builtin.connection

+
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class Connection +(url: str,
logger: logging.Logger,
proxy: str | None = None,
proxy_headers: Dict[str, str] | None = None,
ping_interval: float = 5,
receive_timeout: float = 3,
receive_buffer_size: int = 1024,
trace_enabled: bool = False,
all_message_trace_enabled: bool = False,
ping_pong_trace_enabled: bool = False,
on_message_listener: Callable[[str], None] | None = None,
on_error_listener: Callable[[Exception], None] | None = None,
on_close_listener: Callable[[int, str | None], None] | None = None,
connection_type_name: str = 'Socket Mode',
ssl_context: ssl.SSLContext | None = None)
+
+
+
+ +Expand source code + +
class Connection:
+    url: str
+    logger: Logger
+    proxy: Optional[str]
+    proxy_headers: Optional[Dict[str, str]]
+
+    trace_enabled: bool
+    ping_pong_trace_enabled: bool
+    last_ping_pong_time: Optional[float]
+
+    session_id: str
+    sock: Optional[ssl.SSLSocket]
+
+    on_message_listener: Optional[Callable[[str], None]]
+    on_error_listener: Optional[Callable[[Exception], None]]
+    on_close_listener: Optional[Callable[[int, Optional[str]], None]]
+
+    def __init__(
+        self,
+        url: str,
+        logger: Logger,
+        proxy: Optional[str] = None,
+        proxy_headers: Optional[Dict[str, str]] = None,
+        ping_interval: float = 5,  # seconds
+        receive_timeout: float = 3,
+        receive_buffer_size: int = 1024,
+        trace_enabled: bool = False,
+        all_message_trace_enabled: bool = False,
+        ping_pong_trace_enabled: bool = False,
+        on_message_listener: Optional[Callable[[str], None]] = None,
+        on_error_listener: Optional[Callable[[Exception], None]] = None,
+        on_close_listener: Optional[Callable[[int, Optional[str]], None]] = None,
+        connection_type_name: str = "Socket Mode",
+        ssl_context: Optional[ssl.SSLContext] = None,
+    ):
+        self.url = url
+        self.logger = logger
+        self.proxy = proxy
+        self.proxy_headers = proxy_headers
+
+        self.ping_interval = ping_interval
+        self.receive_timeout = receive_timeout
+        self.receive_buffer_size = receive_buffer_size
+        if self.receive_buffer_size < 16:
+            raise SlackClientConfigurationError("Too small receive_buffer_size detected.")
+
+        self.session_id = str(uuid4())
+        self.trace_enabled = trace_enabled
+        self.all_message_trace_enabled = all_message_trace_enabled
+        self.ping_pong_trace_enabled = ping_pong_trace_enabled
+        self.last_ping_pong_time = None
+        self.consecutive_check_state_error_count = 0
+        self.sock = None
+        # To avoid ssl.SSLError: [SSL: BAD_LENGTH] bad length
+        self.sock_receive_lock = Lock()
+        self.sock_send_lock = Lock()
+
+        self.on_message_listener = on_message_listener
+        self.on_error_listener = on_error_listener
+        self.on_close_listener = on_close_listener
+        self.connection_type_name = connection_type_name
+
+        self.ssl_context = ssl_context
+
+    def connect(self) -> None:
+        try:
+            parsed_url = urlparse(self.url.strip())
+            hostname: str = parsed_url.hostname  # type: ignore[assignment]
+            port: int = parsed_url.port or (443 if parsed_url.scheme == "wss" else 80)
+            if self.trace_enabled:
+                self.logger.debug(
+                    f"Connecting to the address for handshake: {hostname}:{port} " f"(session id: {self.session_id})"
+                )
+            sock: Union[ssl.SSLSocket, socket] = _establish_new_socket_connection(  # type: ignore[valid-type]
+                session_id=self.session_id,
+                server_hostname=hostname,
+                server_port=port,
+                logger=self.logger,
+                sock_send_lock=self.sock_send_lock,
+                receive_timeout=self.receive_timeout,
+                proxy=self.proxy,
+                proxy_headers=self.proxy_headers,
+                trace_enabled=self.trace_enabled,
+                ssl_context=self.ssl_context,
+            )
+
+            # WebSocket handshake
+            try:
+                path = f"{parsed_url.path}?{parsed_url.query}"
+                sec_websocket_key = _generate_sec_websocket_key()
+                message = f"""GET {path} HTTP/1.1
+                    Host: {parsed_url.hostname}
+                    Upgrade: websocket
+                    Connection: Upgrade
+                    Sec-WebSocket-Key: {sec_websocket_key}
+                    Sec-WebSocket-Version: 13
+
+                """
+                req: str = "\r\n".join([line.lstrip() for line in message.split("\n")])
+                if self.trace_enabled:
+                    self.logger.debug(
+                        f"{self.connection_type_name} handshake request (session id: {self.session_id}):\n{req}"
+                    )
+                with self.sock_send_lock:
+                    sock.send(req.encode("utf-8"))  # type: ignore[union-attr]
+
+                status, headers, text = _parse_handshake_response(sock)
+                if self.trace_enabled:
+                    self.logger.debug(
+                        f"{self.connection_type_name} handshake response (session id: {self.session_id}):\n{text}"
+                    )
+                # HTTP/1.1 101 Switching Protocols
+                if status == 101:
+                    if not _validate_sec_websocket_accept(sec_websocket_key, headers):
+                        raise SlackClientNotConnectedError(
+                            f"Invalid response header detected in {self.connection_type_name} handshake response"
+                            f" (session id: {self.session_id})"
+                        )
+                    # set this successfully connected socket
+                    self.sock = sock
+                    self.ping(f"{self.session_id}:{time.time()}")
+                else:
+                    message = (
+                        f"Received an unexpected response for handshake "
+                        f"(status: {status}, response: {text}, session id: {self.session_id})"
+                    )
+                    self.logger.warning(message)
+
+            except socket.error as e:
+                code: Optional[int] = None
+                if e.args and len(e.args) > 1 and isinstance(e.args[0], int):
+                    code = e.args[0]
+                if code is not None:
+                    error_message = f"Error code: {code} (session id: {self.session_id}, error: {e})"
+                    if self.trace_enabled:
+                        self.logger.exception(error_message)
+                    else:
+                        self.logger.error(error_message)
+                raise
+
+        except Exception as e:
+            error_message = f"Failed to establish a connection (session id: {self.session_id}, error: {e})"
+            if self.trace_enabled:
+                self.logger.exception(error_message)
+            else:
+                self.logger.error(error_message)
+
+            if self.on_error_listener is not None:
+                self.on_error_listener(e)
+
+            self.disconnect()
+
+    def disconnect(self) -> None:
+        if self.sock is not None:
+            with self.sock_send_lock:
+                with self.sock_receive_lock:
+                    # Synchronize before closing this instance's socket
+                    self.sock.close()
+                    self.sock = None
+                    # After this, all operations using self.sock will be skipped
+
+        self.logger.info(f"The connection has been closed (session id: {self.session_id})")
+
+    def is_active(self) -> bool:
+        return self.sock is not None
+
+    def close(self) -> None:
+        self.disconnect()
+
+    def ping(self, payload: Union[str, bytes] = "") -> None:
+        if self.trace_enabled and self.ping_pong_trace_enabled:
+            if isinstance(payload, bytes):
+                payload = payload.decode("utf-8")
+            self.logger.debug("Sending a ping data frame " f"(session id: {self.session_id}, payload: {payload})")
+        data = _build_data_frame_for_sending(payload, FrameHeader.OPCODE_PING)
+        with self.sock_send_lock:
+            if self.sock is not None:
+                self.sock.send(data)
+            else:
+                if self.ping_pong_trace_enabled:
+                    self.logger.debug("Skipped sending a ping message as the underlying socket is no longer available.")
+
+    def pong(self, payload: Union[str, bytes] = "") -> None:
+        if self.trace_enabled and self.ping_pong_trace_enabled:
+            if isinstance(payload, bytes):
+                payload = payload.decode("utf-8")
+            self.logger.debug("Sending a pong data frame " f"(session id: {self.session_id}, payload: {payload})")
+        data = _build_data_frame_for_sending(payload, FrameHeader.OPCODE_PONG)
+        with self.sock_send_lock:
+            if self.sock is not None:
+                self.sock.send(data)
+            else:
+                if self.ping_pong_trace_enabled:
+                    self.logger.debug("Skipped sending a pong message as the underlying socket is no longer available.")
+
+    def send(self, payload: str) -> None:
+        if self.trace_enabled:
+            if isinstance(payload, bytes):
+                payload = payload.decode("utf-8")
+            self.logger.debug("Sending a text data frame " f"(session id: {self.session_id}, payload: {payload})")
+        data = _build_data_frame_for_sending(payload, FrameHeader.OPCODE_TEXT)
+        with self.sock_send_lock:
+            try:
+                self.sock.send(data)  # type: ignore[union-attr]
+            except Exception as e:
+                # In most cases, we want to retry this operation with a newly established connection.
+                # Getting this exception means that this connection has been replaced with a new one
+                # and it's no longer usable.
+                # The SocketModeClient implementation can do one retry when it gets this exception.
+                raise SlackClientNotConnectedError(
+                    f"Failed to send a message as the connection is no longer active "
+                    f"(session_id: {self.session_id}, error: {e})"
+                )
+
+    def check_state(self) -> None:
+        try:
+            if self.sock is not None:
+                try:
+                    self.ping(f"{self.session_id}:{time.time()}")
+                except ssl.SSLZeroReturnError as e:
+                    self.logger.info(
+                        "Unable to send a ping message. Closing the connection..."
+                        f" (session id: {self.session_id}, reason: {e})"
+                    )
+                    self.disconnect()
+                    return
+
+                if self.last_ping_pong_time is not None:
+                    disconnected_seconds = int(time.time() - self.last_ping_pong_time)
+                    if self.trace_enabled and disconnected_seconds > self.ping_interval:
+                        message = (
+                            f"{disconnected_seconds} seconds have passed "
+                            f"since this client last received a pong response from the server "
+                            f"(session id: {self.session_id})"
+                        )
+                        self.logger.debug(message)
+
+                    is_stale = disconnected_seconds > self.ping_interval * 4
+                    if is_stale:
+                        self.logger.info(
+                            "The connection seems to be stale. Disconnecting..."
+                            f" (session id: {self.session_id},"
+                            f" reason: disconnected for {disconnected_seconds}+ seconds)"
+                        )
+                        self.disconnect()
+                        return
+            else:
+                self.logger.debug("This connection is already closed." f" (session id: {self.session_id})")
+            self.consecutive_check_state_error_count = 0
+        except Exception as e:
+            error_message = (
+                "Failed to check the state of sock "
+                f"(session id: {self.session_id}, error: {type(e).__name__}, message: {e})"
+            )
+            if self.trace_enabled:
+                self.logger.exception(error_message)
+            else:
+                self.logger.error(error_message)
+
+            self.consecutive_check_state_error_count += 1
+            if self.consecutive_check_state_error_count >= 5:
+                self.disconnect()
+
+    def run_until_completion(self, state: ConnectionState) -> None:
+        repeated_messages = {"payload": 0}
+        ping_count = 0
+        pong_count = 0
+        ping_pong_log_summary_size = 1000
+        while not state.terminated:
+            try:
+                if self.is_active():
+                    received_messages: List[Tuple[Optional[FrameHeader], bytes]] = _receive_messages(
+                        sock=self.sock,  # type: ignore[arg-type]
+                        sock_receive_lock=self.sock_receive_lock,
+                        logger=self.logger,
+                        receive_buffer_size=self.receive_buffer_size,
+                        all_message_trace_enabled=self.all_message_trace_enabled,
+                    )
+                    for message in received_messages:
+                        header, data = message
+
+                        # -----------------
+                        # trace logging
+
+                        if self.trace_enabled is True:
+                            opcode: str = _to_readable_opcode(header.opcode) if header else "-"
+                            payload: str = _parse_text_payload(data, self.logger)
+                            count: Optional[int] = repeated_messages.get(payload)
+                            if count is None:
+                                count = 1
+                            else:
+                                count += 1
+                            repeated_messages = {payload: count}
+                            if not self.ping_pong_trace_enabled and header is not None and header.opcode is not None:
+                                if header.opcode == FrameHeader.OPCODE_PING:
+                                    ping_count += 1
+                                    if ping_count % ping_pong_log_summary_size == 0:
+                                        self.logger.debug(
+                                            f"Received {ping_pong_log_summary_size} ping data frame "
+                                            f"(session id: {self.session_id})"
+                                        )
+                                        ping_count = 0
+                                if header.opcode == FrameHeader.OPCODE_PONG:
+                                    pong_count += 1
+                                    if pong_count % ping_pong_log_summary_size == 0:
+                                        self.logger.debug(
+                                            f"Received {ping_pong_log_summary_size} pong data frame "
+                                            f"(session id: {self.session_id})"
+                                        )
+                                        pong_count = 0
+
+                            ping_pong_to_skip = (
+                                header is not None
+                                and header.opcode is not None
+                                and (header.opcode == FrameHeader.OPCODE_PING or header.opcode == FrameHeader.OPCODE_PONG)
+                                and not self.ping_pong_trace_enabled
+                            )
+                            if not ping_pong_to_skip and count < 5:
+                                # if so many same payloads came in, the trace logging should be skipped.
+                                # e.g., after receiving "UNAUTHENTICATED: cache_error", many "opcode: -, payload: "
+                                self.logger.debug(
+                                    "Received a new data frame "
+                                    f"(session id: {self.session_id}, opcode: {opcode}, payload: {payload})"
+                                )
+
+                        if header is None:
+                            # Skip no header message
+                            continue
+
+                        # -----------------
+                        # message with opcode
+
+                        if header.opcode == FrameHeader.OPCODE_PING:
+                            self.pong(data)
+                        elif header.opcode == FrameHeader.OPCODE_PONG:
+                            str_message = data.decode("utf-8")
+                            elements = str_message.split(":")
+                            if len(elements) >= 2:
+                                session_id, ping_time = elements[0], elements[1]
+                                if self.session_id == session_id:
+                                    try:
+                                        self.last_ping_pong_time = float(ping_time)
+                                    except Exception as e:
+                                        self.logger.debug(
+                                            "Failed to parse a pong message " f" (message: {str_message}, error: {e}"
+                                        )
+                        elif header.opcode == FrameHeader.OPCODE_TEXT:
+                            if self.on_message_listener is not None:
+                                text = data.decode("utf-8")
+                                self.on_message_listener(text)
+                        elif header.opcode == FrameHeader.OPCODE_CLOSE:
+                            if self.on_close_listener is not None:
+                                if len(data) >= 2:
+                                    (code,) = struct.unpack("!H", data[:2])
+                                    reason = data[2:].decode("utf-8")
+                                    self.on_close_listener(code, reason)
+                                else:
+                                    self.on_close_listener(1005, "")
+                            self.disconnect()
+                            state.terminated = True
+                        else:
+                            # Just warn logging
+                            opcode = _to_readable_opcode(header.opcode) if header else "-"
+                            payload: Union[bytes, str] = data  # type: ignore[no-redef]
+                            if header.opcode != FrameHeader.OPCODE_BINARY:
+                                try:
+                                    payload = data.decode("utf-8") if data is not None else ""
+                                except Exception as e:
+                                    self.logger.info(f"Failed to convert the data to text {e}")
+                            message = (
+                                "Received an unsupported data frame "  # type: ignore[assignment]
+                                f"(session id: {self.session_id}, opcode: {opcode}, payload: {payload})"
+                            )
+                            self.logger.warning(message)
+                else:
+                    time.sleep(0.2)
+            except socket.timeout:
+                time.sleep(0.01)
+            except OSError as e:
+                # getting errno.EBADF and the socket is no longer available
+                if e.errno == 9 and state.terminated:
+                    self.logger.debug(
+                        "The reason why you got [Errno 9] Bad file descriptor here is " "the socket is no longer available."
+                    )
+                else:
+                    if self.on_error_listener is not None:
+                        self.on_error_listener(e)
+                    else:
+                        error_message = "Got an OSError while receiving data" f" (session id: {self.session_id}, error: {e})"
+                        if self.trace_enabled:
+                            self.logger.exception(error_message)
+                        else:
+                            self.logger.error(error_message)
+
+                # As this connection no longer works in any way, terminating it
+                if self.is_active():
+                    try:
+                        self.disconnect()
+                    except Exception as disconnection_error:
+                        error_message = (
+                            "Failed to disconnect" f" (session id: {self.session_id}, error: {disconnection_error})"
+                        )
+                        if self.trace_enabled:
+                            self.logger.exception(error_message)
+                        else:
+                            self.logger.error(error_message)
+                state.terminated = True
+                break
+            except Exception as e:
+                if self.on_error_listener is not None:
+                    self.on_error_listener(e)
+                else:
+                    error_message = "Got an exception while receiving data" f" (session id: {self.session_id}, error: {e})"
+                    if self.trace_enabled:
+                        self.logger.exception(error_message)
+                    else:
+                        self.logger.error(error_message)
+
+        state.terminated = True
+
+
+

Class variables

+
+
var last_ping_pong_time : float | None
+
+

The type of the None singleton.

+
+
var logger : logging.Logger
+
+

The type of the None singleton.

+
+
var on_close_listener : Callable[[int, str | None], None] | None
+
+

The type of the None singleton.

+
+
var on_error_listener : Callable[[Exception], None] | None
+
+

The type of the None singleton.

+
+
var on_message_listener : Callable[[str], None] | None
+
+

The type of the None singleton.

+
+
var ping_pong_trace_enabled : bool
+
+

The type of the None singleton.

+
+
var proxy : str | None
+
+

The type of the None singleton.

+
+
var proxy_headers : Dict[str, str] | None
+
+

The type of the None singleton.

+
+
var session_id : str
+
+

The type of the None singleton.

+
+
var sock : ssl.SSLSocket | None
+
+

The type of the None singleton.

+
+
var trace_enabled : bool
+
+

The type of the None singleton.

+
+
var url : str
+
+

The type of the None singleton.

+
+
+

Methods

+
+
+def check_state(self) ‑> None +
+
+
+ +Expand source code + +
def check_state(self) -> None:
+    try:
+        if self.sock is not None:
+            try:
+                self.ping(f"{self.session_id}:{time.time()}")
+            except ssl.SSLZeroReturnError as e:
+                self.logger.info(
+                    "Unable to send a ping message. Closing the connection..."
+                    f" (session id: {self.session_id}, reason: {e})"
+                )
+                self.disconnect()
+                return
+
+            if self.last_ping_pong_time is not None:
+                disconnected_seconds = int(time.time() - self.last_ping_pong_time)
+                if self.trace_enabled and disconnected_seconds > self.ping_interval:
+                    message = (
+                        f"{disconnected_seconds} seconds have passed "
+                        f"since this client last received a pong response from the server "
+                        f"(session id: {self.session_id})"
+                    )
+                    self.logger.debug(message)
+
+                is_stale = disconnected_seconds > self.ping_interval * 4
+                if is_stale:
+                    self.logger.info(
+                        "The connection seems to be stale. Disconnecting..."
+                        f" (session id: {self.session_id},"
+                        f" reason: disconnected for {disconnected_seconds}+ seconds)"
+                    )
+                    self.disconnect()
+                    return
+        else:
+            self.logger.debug("This connection is already closed." f" (session id: {self.session_id})")
+        self.consecutive_check_state_error_count = 0
+    except Exception as e:
+        error_message = (
+            "Failed to check the state of sock "
+            f"(session id: {self.session_id}, error: {type(e).__name__}, message: {e})"
+        )
+        if self.trace_enabled:
+            self.logger.exception(error_message)
+        else:
+            self.logger.error(error_message)
+
+        self.consecutive_check_state_error_count += 1
+        if self.consecutive_check_state_error_count >= 5:
+            self.disconnect()
+
+
+
+
+def close(self) ‑> None +
+
+
+ +Expand source code + +
def close(self) -> None:
+    self.disconnect()
+
+
+
+
+def connect(self) ‑> None +
+
+
+ +Expand source code + +
def connect(self) -> None:
+    try:
+        parsed_url = urlparse(self.url.strip())
+        hostname: str = parsed_url.hostname  # type: ignore[assignment]
+        port: int = parsed_url.port or (443 if parsed_url.scheme == "wss" else 80)
+        if self.trace_enabled:
+            self.logger.debug(
+                f"Connecting to the address for handshake: {hostname}:{port} " f"(session id: {self.session_id})"
+            )
+        sock: Union[ssl.SSLSocket, socket] = _establish_new_socket_connection(  # type: ignore[valid-type]
+            session_id=self.session_id,
+            server_hostname=hostname,
+            server_port=port,
+            logger=self.logger,
+            sock_send_lock=self.sock_send_lock,
+            receive_timeout=self.receive_timeout,
+            proxy=self.proxy,
+            proxy_headers=self.proxy_headers,
+            trace_enabled=self.trace_enabled,
+            ssl_context=self.ssl_context,
+        )
+
+        # WebSocket handshake
+        try:
+            path = f"{parsed_url.path}?{parsed_url.query}"
+            sec_websocket_key = _generate_sec_websocket_key()
+            message = f"""GET {path} HTTP/1.1
+                Host: {parsed_url.hostname}
+                Upgrade: websocket
+                Connection: Upgrade
+                Sec-WebSocket-Key: {sec_websocket_key}
+                Sec-WebSocket-Version: 13
+
+            """
+            req: str = "\r\n".join([line.lstrip() for line in message.split("\n")])
+            if self.trace_enabled:
+                self.logger.debug(
+                    f"{self.connection_type_name} handshake request (session id: {self.session_id}):\n{req}"
+                )
+            with self.sock_send_lock:
+                sock.send(req.encode("utf-8"))  # type: ignore[union-attr]
+
+            status, headers, text = _parse_handshake_response(sock)
+            if self.trace_enabled:
+                self.logger.debug(
+                    f"{self.connection_type_name} handshake response (session id: {self.session_id}):\n{text}"
+                )
+            # HTTP/1.1 101 Switching Protocols
+            if status == 101:
+                if not _validate_sec_websocket_accept(sec_websocket_key, headers):
+                    raise SlackClientNotConnectedError(
+                        f"Invalid response header detected in {self.connection_type_name} handshake response"
+                        f" (session id: {self.session_id})"
+                    )
+                # set this successfully connected socket
+                self.sock = sock
+                self.ping(f"{self.session_id}:{time.time()}")
+            else:
+                message = (
+                    f"Received an unexpected response for handshake "
+                    f"(status: {status}, response: {text}, session id: {self.session_id})"
+                )
+                self.logger.warning(message)
+
+        except socket.error as e:
+            code: Optional[int] = None
+            if e.args and len(e.args) > 1 and isinstance(e.args[0], int):
+                code = e.args[0]
+            if code is not None:
+                error_message = f"Error code: {code} (session id: {self.session_id}, error: {e})"
+                if self.trace_enabled:
+                    self.logger.exception(error_message)
+                else:
+                    self.logger.error(error_message)
+            raise
+
+    except Exception as e:
+        error_message = f"Failed to establish a connection (session id: {self.session_id}, error: {e})"
+        if self.trace_enabled:
+            self.logger.exception(error_message)
+        else:
+            self.logger.error(error_message)
+
+        if self.on_error_listener is not None:
+            self.on_error_listener(e)
+
+        self.disconnect()
+
+
+
+
+def disconnect(self) ‑> None +
+
+
+ +Expand source code + +
def disconnect(self) -> None:
+    if self.sock is not None:
+        with self.sock_send_lock:
+            with self.sock_receive_lock:
+                # Synchronize before closing this instance's socket
+                self.sock.close()
+                self.sock = None
+                # After this, all operations using self.sock will be skipped
+
+    self.logger.info(f"The connection has been closed (session id: {self.session_id})")
+
+
+
+
+def is_active(self) ‑> bool +
+
+
+ +Expand source code + +
def is_active(self) -> bool:
+    return self.sock is not None
+
+
+
+
+def ping(self, payload: str | bytes = '') ‑> None +
+
+
+ +Expand source code + +
def ping(self, payload: Union[str, bytes] = "") -> None:
+    if self.trace_enabled and self.ping_pong_trace_enabled:
+        if isinstance(payload, bytes):
+            payload = payload.decode("utf-8")
+        self.logger.debug("Sending a ping data frame " f"(session id: {self.session_id}, payload: {payload})")
+    data = _build_data_frame_for_sending(payload, FrameHeader.OPCODE_PING)
+    with self.sock_send_lock:
+        if self.sock is not None:
+            self.sock.send(data)
+        else:
+            if self.ping_pong_trace_enabled:
+                self.logger.debug("Skipped sending a ping message as the underlying socket is no longer available.")
+
+
+
+
+def pong(self, payload: str | bytes = '') ‑> None +
+
+
+ +Expand source code + +
def pong(self, payload: Union[str, bytes] = "") -> None:
+    if self.trace_enabled and self.ping_pong_trace_enabled:
+        if isinstance(payload, bytes):
+            payload = payload.decode("utf-8")
+        self.logger.debug("Sending a pong data frame " f"(session id: {self.session_id}, payload: {payload})")
+    data = _build_data_frame_for_sending(payload, FrameHeader.OPCODE_PONG)
+    with self.sock_send_lock:
+        if self.sock is not None:
+            self.sock.send(data)
+        else:
+            if self.ping_pong_trace_enabled:
+                self.logger.debug("Skipped sending a pong message as the underlying socket is no longer available.")
+
+
+
+
+def run_until_completion(self,
state: ConnectionState) ‑> None
+
+
+
+ +Expand source code + +
def run_until_completion(self, state: ConnectionState) -> None:
+    repeated_messages = {"payload": 0}
+    ping_count = 0
+    pong_count = 0
+    ping_pong_log_summary_size = 1000
+    while not state.terminated:
+        try:
+            if self.is_active():
+                received_messages: List[Tuple[Optional[FrameHeader], bytes]] = _receive_messages(
+                    sock=self.sock,  # type: ignore[arg-type]
+                    sock_receive_lock=self.sock_receive_lock,
+                    logger=self.logger,
+                    receive_buffer_size=self.receive_buffer_size,
+                    all_message_trace_enabled=self.all_message_trace_enabled,
+                )
+                for message in received_messages:
+                    header, data = message
+
+                    # -----------------
+                    # trace logging
+
+                    if self.trace_enabled is True:
+                        opcode: str = _to_readable_opcode(header.opcode) if header else "-"
+                        payload: str = _parse_text_payload(data, self.logger)
+                        count: Optional[int] = repeated_messages.get(payload)
+                        if count is None:
+                            count = 1
+                        else:
+                            count += 1
+                        repeated_messages = {payload: count}
+                        if not self.ping_pong_trace_enabled and header is not None and header.opcode is not None:
+                            if header.opcode == FrameHeader.OPCODE_PING:
+                                ping_count += 1
+                                if ping_count % ping_pong_log_summary_size == 0:
+                                    self.logger.debug(
+                                        f"Received {ping_pong_log_summary_size} ping data frame "
+                                        f"(session id: {self.session_id})"
+                                    )
+                                    ping_count = 0
+                            if header.opcode == FrameHeader.OPCODE_PONG:
+                                pong_count += 1
+                                if pong_count % ping_pong_log_summary_size == 0:
+                                    self.logger.debug(
+                                        f"Received {ping_pong_log_summary_size} pong data frame "
+                                        f"(session id: {self.session_id})"
+                                    )
+                                    pong_count = 0
+
+                        ping_pong_to_skip = (
+                            header is not None
+                            and header.opcode is not None
+                            and (header.opcode == FrameHeader.OPCODE_PING or header.opcode == FrameHeader.OPCODE_PONG)
+                            and not self.ping_pong_trace_enabled
+                        )
+                        if not ping_pong_to_skip and count < 5:
+                            # if so many same payloads came in, the trace logging should be skipped.
+                            # e.g., after receiving "UNAUTHENTICATED: cache_error", many "opcode: -, payload: "
+                            self.logger.debug(
+                                "Received a new data frame "
+                                f"(session id: {self.session_id}, opcode: {opcode}, payload: {payload})"
+                            )
+
+                    if header is None:
+                        # Skip no header message
+                        continue
+
+                    # -----------------
+                    # message with opcode
+
+                    if header.opcode == FrameHeader.OPCODE_PING:
+                        self.pong(data)
+                    elif header.opcode == FrameHeader.OPCODE_PONG:
+                        str_message = data.decode("utf-8")
+                        elements = str_message.split(":")
+                        if len(elements) >= 2:
+                            session_id, ping_time = elements[0], elements[1]
+                            if self.session_id == session_id:
+                                try:
+                                    self.last_ping_pong_time = float(ping_time)
+                                except Exception as e:
+                                    self.logger.debug(
+                                        "Failed to parse a pong message " f" (message: {str_message}, error: {e}"
+                                    )
+                    elif header.opcode == FrameHeader.OPCODE_TEXT:
+                        if self.on_message_listener is not None:
+                            text = data.decode("utf-8")
+                            self.on_message_listener(text)
+                    elif header.opcode == FrameHeader.OPCODE_CLOSE:
+                        if self.on_close_listener is not None:
+                            if len(data) >= 2:
+                                (code,) = struct.unpack("!H", data[:2])
+                                reason = data[2:].decode("utf-8")
+                                self.on_close_listener(code, reason)
+                            else:
+                                self.on_close_listener(1005, "")
+                        self.disconnect()
+                        state.terminated = True
+                    else:
+                        # Just warn logging
+                        opcode = _to_readable_opcode(header.opcode) if header else "-"
+                        payload: Union[bytes, str] = data  # type: ignore[no-redef]
+                        if header.opcode != FrameHeader.OPCODE_BINARY:
+                            try:
+                                payload = data.decode("utf-8") if data is not None else ""
+                            except Exception as e:
+                                self.logger.info(f"Failed to convert the data to text {e}")
+                        message = (
+                            "Received an unsupported data frame "  # type: ignore[assignment]
+                            f"(session id: {self.session_id}, opcode: {opcode}, payload: {payload})"
+                        )
+                        self.logger.warning(message)
+            else:
+                time.sleep(0.2)
+        except socket.timeout:
+            time.sleep(0.01)
+        except OSError as e:
+            # getting errno.EBADF and the socket is no longer available
+            if e.errno == 9 and state.terminated:
+                self.logger.debug(
+                    "The reason why you got [Errno 9] Bad file descriptor here is " "the socket is no longer available."
+                )
+            else:
+                if self.on_error_listener is not None:
+                    self.on_error_listener(e)
+                else:
+                    error_message = "Got an OSError while receiving data" f" (session id: {self.session_id}, error: {e})"
+                    if self.trace_enabled:
+                        self.logger.exception(error_message)
+                    else:
+                        self.logger.error(error_message)
+
+            # As this connection no longer works in any way, terminating it
+            if self.is_active():
+                try:
+                    self.disconnect()
+                except Exception as disconnection_error:
+                    error_message = (
+                        "Failed to disconnect" f" (session id: {self.session_id}, error: {disconnection_error})"
+                    )
+                    if self.trace_enabled:
+                        self.logger.exception(error_message)
+                    else:
+                        self.logger.error(error_message)
+            state.terminated = True
+            break
+        except Exception as e:
+            if self.on_error_listener is not None:
+                self.on_error_listener(e)
+            else:
+                error_message = "Got an exception while receiving data" f" (session id: {self.session_id}, error: {e})"
+                if self.trace_enabled:
+                    self.logger.exception(error_message)
+                else:
+                    self.logger.error(error_message)
+
+    state.terminated = True
+
+
+
+
+def send(self, payload: str) ‑> None +
+
+
+ +Expand source code + +
def send(self, payload: str) -> None:
+    if self.trace_enabled:
+        if isinstance(payload, bytes):
+            payload = payload.decode("utf-8")
+        self.logger.debug("Sending a text data frame " f"(session id: {self.session_id}, payload: {payload})")
+    data = _build_data_frame_for_sending(payload, FrameHeader.OPCODE_TEXT)
+    with self.sock_send_lock:
+        try:
+            self.sock.send(data)  # type: ignore[union-attr]
+        except Exception as e:
+            # In most cases, we want to retry this operation with a newly established connection.
+            # Getting this exception means that this connection has been replaced with a new one
+            # and it's no longer usable.
+            # The SocketModeClient implementation can do one retry when it gets this exception.
+            raise SlackClientNotConnectedError(
+                f"Failed to send a message as the connection is no longer active "
+                f"(session_id: {self.session_id}, error: {e})"
+            )
+
+
+
+
+
+
+class ConnectionState +
+
+
+ +Expand source code + +
class ConnectionState:
+    # The flag supposed to be used for telling SocketModeClient
+    # when this connection is no longer available
+    terminated: bool
+
+    def __init__(self):
+        self.terminated = False
+
+
+

Class variables

+
+
var terminated : bool
+
+

The type of the None singleton.

+
+
+
+
+
+
+ +
+ + + diff --git a/docs/reference/socket_mode/builtin/frame_header.html b/docs/reference/socket_mode/builtin/frame_header.html new file mode 100644 index 000000000..b728e0926 --- /dev/null +++ b/docs/reference/socket_mode/builtin/frame_header.html @@ -0,0 +1,205 @@ + + + + + + +slack_sdk.socket_mode.builtin.frame_header API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.socket_mode.builtin.frame_header

+
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class FrameHeader +(opcode: int,
fin: int = 1,
rsv1: int = 0,
rsv2: int = 0,
rsv3: int = 0,
masked: int = 0,
length: int = 0)
+
+
+
+ +Expand source code + +
class FrameHeader:
+    fin: int
+    rsv1: int
+    rsv2: int
+    rsv3: int
+    opcode: int
+    masked: int
+    length: int
+
+    # Opcode
+    # https://tools.ietf.org/html/rfc6455#section-5.2
+    # Non-control frames
+    # %x0 denotes a continuation frame
+    OPCODE_CONTINUATION = 0x0
+    # %x1 denotes a text frame
+    OPCODE_TEXT = 0x1
+    # %x2 denotes a binary frame
+    OPCODE_BINARY = 0x2
+    # %x3-7 are reserved for further non-control frames
+
+    # Control frames
+    # %x8 denotes a connection close
+    OPCODE_CLOSE = 0x8
+    # %x9 denotes a ping
+    OPCODE_PING = 0x9
+    # %xA denotes a pong
+    OPCODE_PONG = 0xA
+
+    # %xB-F are reserved for further control frames
+
+    def __init__(
+        self,
+        opcode: int,
+        fin: int = 1,
+        rsv1: int = 0,
+        rsv2: int = 0,
+        rsv3: int = 0,
+        masked: int = 0,
+        length: int = 0,
+    ):
+        self.opcode = opcode
+        self.fin = fin
+        self.rsv1 = rsv1
+        self.rsv2 = rsv2
+        self.rsv3 = rsv3
+        self.masked = masked
+        self.length = length
+
+
+

Class variables

+
+
var OPCODE_BINARY
+
+

The type of the None singleton.

+
+
var OPCODE_CLOSE
+
+

The type of the None singleton.

+
+
var OPCODE_CONTINUATION
+
+

The type of the None singleton.

+
+
var OPCODE_PING
+
+

The type of the None singleton.

+
+
var OPCODE_PONG
+
+

The type of the None singleton.

+
+
var OPCODE_TEXT
+
+

The type of the None singleton.

+
+
var fin : int
+
+

The type of the None singleton.

+
+
var length : int
+
+

The type of the None singleton.

+
+
var masked : int
+
+

The type of the None singleton.

+
+
var opcode : int
+
+

The type of the None singleton.

+
+
var rsv1 : int
+
+

The type of the None singleton.

+
+
var rsv2 : int
+
+

The type of the None singleton.

+
+
var rsv3 : int
+
+

The type of the None singleton.

+
+
+
+
+
+
+ +
+ + + diff --git a/docs/reference/socket_mode/builtin/index.html b/docs/reference/socket_mode/builtin/index.html new file mode 100644 index 000000000..9f710eaf6 --- /dev/null +++ b/docs/reference/socket_mode/builtin/index.html @@ -0,0 +1,653 @@ + + + + + + +slack_sdk.socket_mode.builtin API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.socket_mode.builtin

+
+
+
+
+

Sub-modules

+
+
slack_sdk.socket_mode.builtin.client
+
+

The built-in Socket Mode client …

+
+
slack_sdk.socket_mode.builtin.connection
+
+
+
+
slack_sdk.socket_mode.builtin.frame_header
+
+
+
+
slack_sdk.socket_mode.builtin.internals
+
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class SocketModeClient +(app_token: str,
logger: logging.Logger | None = None,
web_client: WebClient | None = None,
auto_reconnect_enabled: bool = True,
trace_enabled: bool = False,
all_message_trace_enabled: bool = False,
ping_pong_trace_enabled: bool = False,
ping_interval: float = 5,
receive_buffer_size: int = 1024,
concurrency: int = 10,
proxy: str | None = None,
proxy_headers: Dict[str, str] | None = None,
on_message_listeners: List[Callable[[str], None]] | None = None,
on_error_listeners: List[Callable[[Exception], None]] | None = None,
on_close_listeners: List[Callable[[int, str | None], None]] | None = None)
+
+
+
+ +Expand source code + +
class SocketModeClient(BaseSocketModeClient):
+    logger: Logger
+    web_client: WebClient
+    app_token: str
+    wss_uri: Optional[str]  # type: ignore[assignment]
+    message_queue: Queue
+    message_listeners: List[
+        Union[
+            WebSocketMessageListener,
+            Callable[["BaseSocketModeClient", dict, Optional[str]], None],
+        ]
+    ]
+    socket_mode_request_listeners: List[
+        Union[
+            SocketModeRequestListener,
+            Callable[["BaseSocketModeClient", SocketModeRequest], None],
+        ]
+    ]
+
+    current_session: Optional[Connection]
+    current_session_state: ConnectionState
+    current_session_runner: IntervalRunner
+
+    current_app_monitor: IntervalRunner
+    current_app_monitor_started: bool
+
+    message_processor: IntervalRunner
+    message_workers: ThreadPoolExecutor
+
+    auto_reconnect_enabled: bool
+    default_auto_reconnect_enabled: bool
+    trace_enabled: bool
+    receive_buffer_size: int  # bytes size
+
+    connect_operation_lock: Lock
+
+    on_message_listeners: List[Callable[[str], None]]
+    on_error_listeners: List[Callable[[Exception], None]]
+    on_close_listeners: List[Callable[[int, Optional[str]], None]]
+
+    def __init__(
+        self,
+        app_token: str,
+        logger: Optional[Logger] = None,
+        web_client: Optional[WebClient] = None,
+        auto_reconnect_enabled: bool = True,
+        trace_enabled: bool = False,
+        all_message_trace_enabled: bool = False,
+        ping_pong_trace_enabled: bool = False,
+        ping_interval: float = 5,
+        receive_buffer_size: int = 1024,
+        concurrency: int = 10,
+        proxy: Optional[str] = None,
+        proxy_headers: Optional[Dict[str, str]] = None,
+        on_message_listeners: Optional[List[Callable[[str], None]]] = None,
+        on_error_listeners: Optional[List[Callable[[Exception], None]]] = None,
+        on_close_listeners: Optional[List[Callable[[int, Optional[str]], None]]] = None,
+    ):
+        """Socket Mode client
+
+        Args:
+            app_token: App-level token
+            logger: Custom logger
+            web_client: Web API client
+            auto_reconnect_enabled: True if automatic reconnection is enabled (default: True)
+            trace_enabled: True if more detailed debug-logging is enabled (default: False)
+            all_message_trace_enabled: True if all message dump in debug logs is enabled (default: False)
+            ping_pong_trace_enabled: True if trace logging for all ping-pong communications is enabled (default: False)
+            ping_interval: interval for ping-pong with Slack servers (seconds)
+            receive_buffer_size: the chunk size of a single socket recv operation (default: 1024)
+            concurrency: the size of thread pool (default: 10)
+            proxy: the HTTP proxy URL
+            proxy_headers: additional HTTP header for proxy connection
+            on_message_listeners: listener functions for on_message
+            on_error_listeners: listener functions for on_error
+            on_close_listeners: listener functions for on_close
+        """
+        self.app_token = app_token
+        self.logger = logger or logging.getLogger(__name__)
+        self.web_client = web_client or WebClient()
+        self.default_auto_reconnect_enabled = auto_reconnect_enabled
+        self.auto_reconnect_enabled = self.default_auto_reconnect_enabled
+        self.trace_enabled = trace_enabled
+        self.all_message_trace_enabled = all_message_trace_enabled
+        self.ping_pong_trace_enabled = ping_pong_trace_enabled
+        self.ping_interval = ping_interval
+        self.receive_buffer_size = receive_buffer_size
+        if self.receive_buffer_size < 16:
+            raise SlackClientConfigurationError("Too small receive_buffer_size detected.")
+
+        self.wss_uri = None
+        self.message_queue = Queue()
+        self.message_listeners = []
+        self.socket_mode_request_listeners = []
+
+        self.current_session = None
+        self.current_session_state = ConnectionState()
+        self.current_session_runner = IntervalRunner(self._run_current_session, 0.1).start()
+
+        self.current_app_monitor_started = False
+        self.current_app_monitor = IntervalRunner(self._monitor_current_session, self.ping_interval)
+
+        self.closed = False
+        self.connect_operation_lock = Lock()
+
+        self.message_processor = IntervalRunner(self.process_messages, 0.001).start()
+        self.message_workers = ThreadPoolExecutor(max_workers=concurrency)
+
+        self.proxy = proxy
+        if self.proxy is None or len(self.proxy.strip()) == 0:
+            env_variable = load_http_proxy_from_env(self.logger)
+            if env_variable is not None:
+                self.proxy = env_variable
+        self.proxy_headers = proxy_headers
+
+        self.on_message_listeners = on_message_listeners or []
+        self.on_error_listeners = on_error_listeners or []
+        self.on_close_listeners = on_close_listeners or []
+
+    def session_id(self) -> Optional[str]:
+        if self.current_session is not None:
+            return self.current_session.session_id
+        return None
+
+    def is_connected(self) -> bool:
+        return self.current_session is not None and self.current_session.is_active()
+
+    def connect(self) -> None:
+        old_session: Optional[Connection] = self.current_session
+        old_current_session_state: ConnectionState = self.current_session_state
+
+        if self.wss_uri is None:
+            self.wss_uri = self.issue_new_wss_url()
+
+        current_session = Connection(
+            url=self.wss_uri,
+            logger=self.logger,
+            ping_interval=self.ping_interval,
+            trace_enabled=self.trace_enabled,
+            all_message_trace_enabled=self.all_message_trace_enabled,
+            ping_pong_trace_enabled=self.ping_pong_trace_enabled,
+            receive_buffer_size=self.receive_buffer_size,
+            proxy=self.proxy,
+            proxy_headers=self.proxy_headers,
+            on_message_listener=self._on_message,
+            on_error_listener=self._on_error,
+            on_close_listener=self._on_close,
+            ssl_context=self.web_client.ssl,
+        )
+        current_session.connect()
+
+        if old_current_session_state is not None:
+            old_current_session_state.terminated = True
+        if old_session is not None:
+            old_session.close()
+
+        self.current_session = current_session
+        self.current_session_state = ConnectionState()
+        self.auto_reconnect_enabled = self.default_auto_reconnect_enabled
+
+        if not self.current_app_monitor_started:
+            self.current_app_monitor_started = True
+            self.current_app_monitor.start()
+
+        self.logger.info(f"A new session has been established (session id: {self.session_id()})")
+
+    def disconnect(self) -> None:
+        if self.current_session is not None:
+            self.current_session.close()
+
+    def send_message(self, message: str) -> None:
+        if self.logger.level <= logging.DEBUG:
+            self.logger.debug(f"Sending a message (session id: {self.session_id()}, message: {message})")
+        try:
+            self.current_session.send(message)  # type: ignore[union-attr]
+        except SlackClientNotConnectedError as e:
+            # We rarely get this exception while replacing the underlying WebSocket connections.
+            # We can do one more try here as the self.current_session should be ready now.
+            if self.logger.level <= logging.DEBUG:
+                self.logger.debug(
+                    f"Failed to send a message (session id: {self.session_id()}, error: {e}, message: {message})"
+                    " as the underlying connection was replaced. Retrying the same request only one time..."
+                )
+            # Although acquiring self.connect_operation_lock also for the first method call is the safest way,
+            # we avoid synchronizing a lot for better performance. That's why we are doing a retry here.
+            with self.connect_operation_lock:
+                if self.is_connected():
+                    self.current_session.send(message)  # type: ignore[union-attr]
+                else:
+                    self.logger.warning(
+                        f"The current session (session id: {self.session_id()}) is no longer active. "
+                        "Failed to send a message"
+                    )
+                    raise e
+
+    def close(self):
+        self.closed = True
+        self.auto_reconnect_enabled = False
+        self.disconnect()
+        if self.current_app_monitor.is_alive():
+            self.current_app_monitor.shutdown()
+        if self.message_processor.is_alive():
+            self.message_processor.shutdown()
+        self.message_workers.shutdown()
+
+    def _on_message(self, message: str):
+        if self.logger.level <= logging.DEBUG:
+            self.logger.debug(f"on_message invoked: (message: {debug_redacted_message_string(message)})")
+        self.enqueue_message(message)
+        for listener in self.on_message_listeners:
+            listener(message)
+
+    def _on_error(self, error: Exception):
+        error_message = (
+            f"on_error invoked (session id: {self.session_id()}, " f"error: {type(error).__name__}, message: {error})"
+        )
+        if self.trace_enabled:
+            self.logger.exception(error_message)
+        else:
+            self.logger.error(error_message)
+
+        for listener in self.on_error_listeners:
+            listener(error)
+
+    def _on_close(self, code: int, reason: Optional[str] = None):
+        if self.logger.level <= logging.DEBUG:
+            self.logger.debug(f"on_close invoked (session id: {self.session_id()})")
+        if self.auto_reconnect_enabled:
+            self.logger.info("Received CLOSE event. Reconnecting... " f"(session id: {self.session_id()})")
+            self.connect_to_new_endpoint()
+        for listener in self.on_close_listeners:
+            listener(code, reason)
+
+    def _run_current_session(self):
+        if self.current_session is not None and self.current_session.is_active():
+            session_id = self.session_id()
+            try:
+                self.logger.info("Starting to receive messages from a new connection" f" (session id: {session_id})")
+                self.current_session_state.terminated = False
+                self.current_session.run_until_completion(self.current_session_state)
+                self.logger.info("Stopped receiving messages from a connection" f" (session id: {session_id})")
+            except Exception as e:
+                error_message = "Failed to start or stop the current session" f" (session id: {session_id}, error: {e})"
+                if self.trace_enabled:
+                    self.logger.exception(error_message)
+                else:
+                    self.logger.error(error_message)
+
+    def _monitor_current_session(self):
+        if self.current_app_monitor_started:
+            try:
+                self.current_session.check_state()
+
+                if self.auto_reconnect_enabled and (self.current_session is None or not self.current_session.is_active()):
+                    self.logger.info(
+                        "The session seems to be already closed. Reconnecting... " f"(session id: {self.session_id()})"
+                    )
+                    self.connect_to_new_endpoint()
+            except Exception as e:
+                self.logger.error(
+                    "Failed to check the current session or reconnect to the server "
+                    f"(session id: {self.session_id()}, error: {type(e).__name__}, message: {e})"
+                )
+
+

Socket Mode client

+

Args

+
+
app_token
+
App-level token
+
logger
+
Custom logger
+
web_client
+
Web API client
+
auto_reconnect_enabled
+
True if automatic reconnection is enabled (default: True)
+
trace_enabled
+
True if more detailed debug-logging is enabled (default: False)
+
all_message_trace_enabled
+
True if all message dump in debug logs is enabled (default: False)
+
ping_pong_trace_enabled
+
True if trace logging for all ping-pong communications is enabled (default: False)
+
ping_interval
+
interval for ping-pong with Slack servers (seconds)
+
receive_buffer_size
+
the chunk size of a single socket recv operation (default: 1024)
+
concurrency
+
the size of thread pool (default: 10)
+
proxy
+
the HTTP proxy URL
+
proxy_headers
+
additional HTTP header for proxy connection
+
on_message_listeners
+
listener functions for on_message
+
on_error_listeners
+
listener functions for on_error
+
on_close_listeners
+
listener functions for on_close
+
+

Ancestors

+ +

Class variables

+
+
var auto_reconnect_enabled : bool
+
+

The type of the None singleton.

+
+
var current_app_monitorIntervalRunner
+
+

The type of the None singleton.

+
+
var current_app_monitor_started : bool
+
+

The type of the None singleton.

+
+
var current_sessionConnection | None
+
+

The type of the None singleton.

+
+
var current_session_runnerIntervalRunner
+
+

The type of the None singleton.

+
+
var current_session_stateConnectionState
+
+

The type of the None singleton.

+
+
var default_auto_reconnect_enabled : bool
+
+

The type of the None singleton.

+
+
var on_close_listeners : List[Callable[[int, str | None], None]]
+
+

The type of the None singleton.

+
+
var on_error_listeners : List[Callable[[Exception], None]]
+
+

The type of the None singleton.

+
+
var on_message_listeners : List[Callable[[str], None]]
+
+

The type of the None singleton.

+
+
var receive_buffer_size : int
+
+

The type of the None singleton.

+
+
var trace_enabled : bool
+
+

The type of the None singleton.

+
+
+

Methods

+
+
+def close(self) +
+
+
+ +Expand source code + +
def close(self):
+    self.closed = True
+    self.auto_reconnect_enabled = False
+    self.disconnect()
+    if self.current_app_monitor.is_alive():
+        self.current_app_monitor.shutdown()
+    if self.message_processor.is_alive():
+        self.message_processor.shutdown()
+    self.message_workers.shutdown()
+
+
+
+
+def connect(self) ‑> None +
+
+
+ +Expand source code + +
def connect(self) -> None:
+    old_session: Optional[Connection] = self.current_session
+    old_current_session_state: ConnectionState = self.current_session_state
+
+    if self.wss_uri is None:
+        self.wss_uri = self.issue_new_wss_url()
+
+    current_session = Connection(
+        url=self.wss_uri,
+        logger=self.logger,
+        ping_interval=self.ping_interval,
+        trace_enabled=self.trace_enabled,
+        all_message_trace_enabled=self.all_message_trace_enabled,
+        ping_pong_trace_enabled=self.ping_pong_trace_enabled,
+        receive_buffer_size=self.receive_buffer_size,
+        proxy=self.proxy,
+        proxy_headers=self.proxy_headers,
+        on_message_listener=self._on_message,
+        on_error_listener=self._on_error,
+        on_close_listener=self._on_close,
+        ssl_context=self.web_client.ssl,
+    )
+    current_session.connect()
+
+    if old_current_session_state is not None:
+        old_current_session_state.terminated = True
+    if old_session is not None:
+        old_session.close()
+
+    self.current_session = current_session
+    self.current_session_state = ConnectionState()
+    self.auto_reconnect_enabled = self.default_auto_reconnect_enabled
+
+    if not self.current_app_monitor_started:
+        self.current_app_monitor_started = True
+        self.current_app_monitor.start()
+
+    self.logger.info(f"A new session has been established (session id: {self.session_id()})")
+
+
+
+
+def disconnect(self) ‑> None +
+
+
+ +Expand source code + +
def disconnect(self) -> None:
+    if self.current_session is not None:
+        self.current_session.close()
+
+
+
+
+def is_connected(self) ‑> bool +
+
+
+ +Expand source code + +
def is_connected(self) -> bool:
+    return self.current_session is not None and self.current_session.is_active()
+
+
+
+
+def send_message(self, message: str) ‑> None +
+
+
+ +Expand source code + +
def send_message(self, message: str) -> None:
+    if self.logger.level <= logging.DEBUG:
+        self.logger.debug(f"Sending a message (session id: {self.session_id()}, message: {message})")
+    try:
+        self.current_session.send(message)  # type: ignore[union-attr]
+    except SlackClientNotConnectedError as e:
+        # We rarely get this exception while replacing the underlying WebSocket connections.
+        # We can do one more try here as the self.current_session should be ready now.
+        if self.logger.level <= logging.DEBUG:
+            self.logger.debug(
+                f"Failed to send a message (session id: {self.session_id()}, error: {e}, message: {message})"
+                " as the underlying connection was replaced. Retrying the same request only one time..."
+            )
+        # Although acquiring self.connect_operation_lock also for the first method call is the safest way,
+        # we avoid synchronizing a lot for better performance. That's why we are doing a retry here.
+        with self.connect_operation_lock:
+            if self.is_connected():
+                self.current_session.send(message)  # type: ignore[union-attr]
+            else:
+                self.logger.warning(
+                    f"The current session (session id: {self.session_id()}) is no longer active. "
+                    "Failed to send a message"
+                )
+                raise e
+
+
+
+
+def session_id(self) ‑> str | None +
+
+
+ +Expand source code + +
def session_id(self) -> Optional[str]:
+    if self.current_session is not None:
+        return self.current_session.session_id
+    return None
+
+
+
+
+

Inherited members

+ +
+
+
+
+ +
+ + + diff --git a/docs/reference/socket_mode/builtin/internals.html b/docs/reference/socket_mode/builtin/internals.html new file mode 100644 index 000000000..2977bc6c4 --- /dev/null +++ b/docs/reference/socket_mode/builtin/internals.html @@ -0,0 +1,66 @@ + + + + + + +slack_sdk.socket_mode.builtin.internals API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.socket_mode.builtin.internals

+
+
+
+
+
+
+
+
+
+
+
+
+ +
+ + + diff --git a/docs/reference/socket_mode/client.html b/docs/reference/socket_mode/client.html new file mode 100644 index 000000000..3c8c615f9 --- /dev/null +++ b/docs/reference/socket_mode/client.html @@ -0,0 +1,537 @@ + + + + + + +slack_sdk.socket_mode.client API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.socket_mode.client

+
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class BaseSocketModeClient +
+
+
+ +Expand source code + +
class BaseSocketModeClient:
+    logger: Logger
+    web_client: WebClient
+    app_token: str
+    wss_uri: str
+    message_queue: Queue
+    message_listeners: List[
+        Union[
+            WebSocketMessageListener,
+            Callable[["BaseSocketModeClient", dict, Optional[str]], None],
+        ]
+    ]
+    socket_mode_request_listeners: List[
+        Union[
+            SocketModeRequestListener,
+            Callable[["BaseSocketModeClient", SocketModeRequest], None],
+        ]
+    ]
+
+    message_processor: IntervalRunner
+    message_workers: ThreadPoolExecutor
+
+    closed: bool
+    connect_operation_lock: Lock
+
+    def issue_new_wss_url(self) -> str:
+        try:
+            response = self.web_client.apps_connections_open(app_token=self.app_token)
+            return response["url"]
+        except SlackApiError as e:
+            if e.response["error"] == "ratelimited":
+                # NOTE: ratelimited errors rarely occur with this endpoint
+                delay = int(e.response.headers.get("Retry-After", "30"))  # Tier1
+                self.logger.info(f"Rate limited. Retrying in {delay} seconds...")
+                time.sleep(delay)
+                # Retry to issue a new WSS URL
+                return self.issue_new_wss_url()
+            else:
+                # other errors
+                self.logger.error(f"Failed to retrieve WSS URL: {e}")
+                raise e
+
+    def is_connected(self) -> bool:
+        return False
+
+    def connect(self) -> None:
+        raise NotImplementedError()
+
+    def disconnect(self) -> None:
+        raise NotImplementedError()
+
+    def connect_to_new_endpoint(self, force: bool = False):
+        try:
+            self.connect_operation_lock.acquire(blocking=True, timeout=5)
+            if force or not self.is_connected():
+                self.logger.info("Connecting to a new endpoint...")
+                self.wss_uri = self.issue_new_wss_url()
+                self.connect()
+                self.logger.info("Connected to a new endpoint...")
+        finally:
+            self.connect_operation_lock.release()
+
+    def close(self) -> None:
+        self.closed = True
+        self.disconnect()
+
+    def send_message(self, message: str) -> None:
+        raise NotImplementedError()
+
+    def send_socket_mode_response(self, response: Union[Dict[str, Any], SocketModeResponse]) -> None:
+        if isinstance(response, SocketModeResponse):
+            self.send_message(json.dumps(response.to_dict()))
+        else:
+            self.send_message(json.dumps(response))
+
+    def enqueue_message(self, message: str):
+        self.message_queue.put(message)
+        if self.logger.level <= logging.DEBUG:
+            self.logger.debug(f"A new message enqueued (current queue size: {self.message_queue.qsize()})")
+
+    def process_message(self):
+        try:
+            raw_message = self.message_queue.get(timeout=1)
+            if self.logger.level <= logging.DEBUG:
+                self.logger.debug(f"A message dequeued (current queue size: {self.message_queue.qsize()})")
+
+            if raw_message is not None:
+                message: dict = {}
+                if raw_message.startswith("{"):
+                    message = json.loads(raw_message)
+                if message.get("type") == "disconnect":
+                    self.connect_to_new_endpoint(force=True)
+                else:
+
+                    def _run_message_listeners():
+                        self.run_message_listeners(message, raw_message)
+
+                    self.message_workers.submit(_run_message_listeners)
+        except Empty:
+            pass
+
+    def run_message_listeners(self, message: dict, raw_message: str) -> None:
+        type, envelope_id = message.get("type"), message.get("envelope_id")
+        if self.logger.level <= logging.DEBUG:
+            self.logger.debug(f"Message processing started (type: {type}, envelope_id: {envelope_id})")
+        try:
+            # just in case, adding the same logic to reconnect here
+            if message.get("type") == "disconnect":
+                self.connect_to_new_endpoint(force=True)
+                return
+
+            for listener in self.message_listeners:
+                try:
+                    listener(self, message, raw_message)  # type: ignore[call-arg, arg-type, misc]
+                except Exception as e:
+                    self.logger.exception(f"Failed to run a message listener: {e}")
+
+            if len(self.socket_mode_request_listeners) > 0:
+                request = SocketModeRequest.from_dict(message)
+                if request is not None:
+                    for listener in self.socket_mode_request_listeners:  # type: ignore[assignment]
+                        try:
+                            listener(self, request)  # type: ignore[call-arg, arg-type]
+                        except Exception as e:
+                            self.logger.exception(f"Failed to run a request listener: {e}")
+        except Exception as e:
+            self.logger.exception(f"Failed to run message listeners: {e}")
+        finally:
+            if self.logger.level <= logging.DEBUG:
+                self.logger.debug(f"Message processing completed (type: {type}, envelope_id: {envelope_id})")
+
+    def process_messages(self) -> None:
+        while not self.closed:
+            try:
+                self.process_message()
+            except Exception as e:
+                self.logger.exception(f"Failed to process a message: {e}")
+
+
+

Subclasses

+ +

Class variables

+
+
var app_token : str
+
+

The type of the None singleton.

+
+
var closed : bool
+
+

The type of the None singleton.

+
+
var connect_operation_lock : _thread.lock
+
+

The type of the None singleton.

+
+
var logger : logging.Logger
+
+

The type of the None singleton.

+
+
var message_listeners : List[WebSocketMessageListener | Callable[[BaseSocketModeClient, dict, str | None], None]]
+
+

The type of the None singleton.

+
+
var message_processorIntervalRunner
+
+

The type of the None singleton.

+
+
var message_queue : queue.Queue
+
+

The type of the None singleton.

+
+
var message_workers : concurrent.futures.thread.ThreadPoolExecutor
+
+

The type of the None singleton.

+
+
var socket_mode_request_listeners : List[SocketModeRequestListener | Callable[[BaseSocketModeClientSocketModeRequest], None]]
+
+

The type of the None singleton.

+
+
var web_clientWebClient
+
+

The type of the None singleton.

+
+
var wss_uri : str
+
+

The type of the None singleton.

+
+
+

Methods

+
+
+def close(self) ‑> None +
+
+
+ +Expand source code + +
def close(self) -> None:
+    self.closed = True
+    self.disconnect()
+
+
+
+
+def connect(self) ‑> None +
+
+
+ +Expand source code + +
def connect(self) -> None:
+    raise NotImplementedError()
+
+
+
+
+def connect_to_new_endpoint(self, force: bool = False) +
+
+
+ +Expand source code + +
def connect_to_new_endpoint(self, force: bool = False):
+    try:
+        self.connect_operation_lock.acquire(blocking=True, timeout=5)
+        if force or not self.is_connected():
+            self.logger.info("Connecting to a new endpoint...")
+            self.wss_uri = self.issue_new_wss_url()
+            self.connect()
+            self.logger.info("Connected to a new endpoint...")
+    finally:
+        self.connect_operation_lock.release()
+
+
+
+
+def disconnect(self) ‑> None +
+
+
+ +Expand source code + +
def disconnect(self) -> None:
+    raise NotImplementedError()
+
+
+
+
+def enqueue_message(self, message: str) +
+
+
+ +Expand source code + +
def enqueue_message(self, message: str):
+    self.message_queue.put(message)
+    if self.logger.level <= logging.DEBUG:
+        self.logger.debug(f"A new message enqueued (current queue size: {self.message_queue.qsize()})")
+
+
+
+
+def is_connected(self) ‑> bool +
+
+
+ +Expand source code + +
def is_connected(self) -> bool:
+    return False
+
+
+
+
+def issue_new_wss_url(self) ‑> str +
+
+
+ +Expand source code + +
def issue_new_wss_url(self) -> str:
+    try:
+        response = self.web_client.apps_connections_open(app_token=self.app_token)
+        return response["url"]
+    except SlackApiError as e:
+        if e.response["error"] == "ratelimited":
+            # NOTE: ratelimited errors rarely occur with this endpoint
+            delay = int(e.response.headers.get("Retry-After", "30"))  # Tier1
+            self.logger.info(f"Rate limited. Retrying in {delay} seconds...")
+            time.sleep(delay)
+            # Retry to issue a new WSS URL
+            return self.issue_new_wss_url()
+        else:
+            # other errors
+            self.logger.error(f"Failed to retrieve WSS URL: {e}")
+            raise e
+
+
+
+
+def process_message(self) +
+
+
+ +Expand source code + +
def process_message(self):
+    try:
+        raw_message = self.message_queue.get(timeout=1)
+        if self.logger.level <= logging.DEBUG:
+            self.logger.debug(f"A message dequeued (current queue size: {self.message_queue.qsize()})")
+
+        if raw_message is not None:
+            message: dict = {}
+            if raw_message.startswith("{"):
+                message = json.loads(raw_message)
+            if message.get("type") == "disconnect":
+                self.connect_to_new_endpoint(force=True)
+            else:
+
+                def _run_message_listeners():
+                    self.run_message_listeners(message, raw_message)
+
+                self.message_workers.submit(_run_message_listeners)
+    except Empty:
+        pass
+
+
+
+
+def process_messages(self) ‑> None +
+
+
+ +Expand source code + +
def process_messages(self) -> None:
+    while not self.closed:
+        try:
+            self.process_message()
+        except Exception as e:
+            self.logger.exception(f"Failed to process a message: {e}")
+
+
+
+
+def run_message_listeners(self, message: dict, raw_message: str) ‑> None +
+
+
+ +Expand source code + +
def run_message_listeners(self, message: dict, raw_message: str) -> None:
+    type, envelope_id = message.get("type"), message.get("envelope_id")
+    if self.logger.level <= logging.DEBUG:
+        self.logger.debug(f"Message processing started (type: {type}, envelope_id: {envelope_id})")
+    try:
+        # just in case, adding the same logic to reconnect here
+        if message.get("type") == "disconnect":
+            self.connect_to_new_endpoint(force=True)
+            return
+
+        for listener in self.message_listeners:
+            try:
+                listener(self, message, raw_message)  # type: ignore[call-arg, arg-type, misc]
+            except Exception as e:
+                self.logger.exception(f"Failed to run a message listener: {e}")
+
+        if len(self.socket_mode_request_listeners) > 0:
+            request = SocketModeRequest.from_dict(message)
+            if request is not None:
+                for listener in self.socket_mode_request_listeners:  # type: ignore[assignment]
+                    try:
+                        listener(self, request)  # type: ignore[call-arg, arg-type]
+                    except Exception as e:
+                        self.logger.exception(f"Failed to run a request listener: {e}")
+    except Exception as e:
+        self.logger.exception(f"Failed to run message listeners: {e}")
+    finally:
+        if self.logger.level <= logging.DEBUG:
+            self.logger.debug(f"Message processing completed (type: {type}, envelope_id: {envelope_id})")
+
+
+
+
+def send_message(self, message: str) ‑> None +
+
+
+ +Expand source code + +
def send_message(self, message: str) -> None:
+    raise NotImplementedError()
+
+
+
+
+def send_socket_mode_response(self,
response: Dict[str, Any] | SocketModeResponse) ‑> None
+
+
+
+ +Expand source code + +
def send_socket_mode_response(self, response: Union[Dict[str, Any], SocketModeResponse]) -> None:
+    if isinstance(response, SocketModeResponse):
+        self.send_message(json.dumps(response.to_dict()))
+    else:
+        self.send_message(json.dumps(response))
+
+
+
+
+
+
+
+
+ +
+ + + diff --git a/docs/reference/socket_mode/index.html b/docs/reference/socket_mode/index.html new file mode 100644 index 000000000..381e969e5 --- /dev/null +++ b/docs/reference/socket_mode/index.html @@ -0,0 +1,698 @@ + + + + + + +slack_sdk.socket_mode API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.socket_mode

+
+
+

Socket Mode is a method of connecting your app to Slack’s APIs using WebSockets instead of HTTP. +You can use slack_sdk.socket_mode.SocketModeClient for managing Socket Mode connections +and performing interactions with Slack.

+

https://docs.slack.dev/apis/events-api/using-socket-mode/

+
+
+

Sub-modules

+
+
slack_sdk.socket_mode.aiohttp
+
+

aiohttp based Socket Mode client …

+
+
slack_sdk.socket_mode.async_client
+
+
+
+
slack_sdk.socket_mode.async_listeners
+
+
+
+
slack_sdk.socket_mode.builtin
+
+
+
+
slack_sdk.socket_mode.client
+
+
+
+
slack_sdk.socket_mode.interval_runner
+
+
+
+
slack_sdk.socket_mode.listeners
+
+
+
+
slack_sdk.socket_mode.logger
+
+
+
+
slack_sdk.socket_mode.request
+
+
+
+
slack_sdk.socket_mode.response
+
+
+
+
slack_sdk.socket_mode.websocket_client
+
+

websocket-client bassd Socket Mode client …

+
+
slack_sdk.socket_mode.websockets
+
+

websockets bassd Socket Mode client …

+
+
+
+
+
+
+
+
+

Classes

+
+
+class SocketModeClient +(app_token: str,
logger: logging.Logger | None = None,
web_client: WebClient | None = None,
auto_reconnect_enabled: bool = True,
trace_enabled: bool = False,
all_message_trace_enabled: bool = False,
ping_pong_trace_enabled: bool = False,
ping_interval: float = 5,
receive_buffer_size: int = 1024,
concurrency: int = 10,
proxy: str | None = None,
proxy_headers: Dict[str, str] | None = None,
on_message_listeners: List[Callable[[str], None]] | None = None,
on_error_listeners: List[Callable[[Exception], None]] | None = None,
on_close_listeners: List[Callable[[int, str | None], None]] | None = None)
+
+
+
+ +Expand source code + +
class SocketModeClient(BaseSocketModeClient):
+    logger: Logger
+    web_client: WebClient
+    app_token: str
+    wss_uri: Optional[str]  # type: ignore[assignment]
+    message_queue: Queue
+    message_listeners: List[
+        Union[
+            WebSocketMessageListener,
+            Callable[["BaseSocketModeClient", dict, Optional[str]], None],
+        ]
+    ]
+    socket_mode_request_listeners: List[
+        Union[
+            SocketModeRequestListener,
+            Callable[["BaseSocketModeClient", SocketModeRequest], None],
+        ]
+    ]
+
+    current_session: Optional[Connection]
+    current_session_state: ConnectionState
+    current_session_runner: IntervalRunner
+
+    current_app_monitor: IntervalRunner
+    current_app_monitor_started: bool
+
+    message_processor: IntervalRunner
+    message_workers: ThreadPoolExecutor
+
+    auto_reconnect_enabled: bool
+    default_auto_reconnect_enabled: bool
+    trace_enabled: bool
+    receive_buffer_size: int  # bytes size
+
+    connect_operation_lock: Lock
+
+    on_message_listeners: List[Callable[[str], None]]
+    on_error_listeners: List[Callable[[Exception], None]]
+    on_close_listeners: List[Callable[[int, Optional[str]], None]]
+
+    def __init__(
+        self,
+        app_token: str,
+        logger: Optional[Logger] = None,
+        web_client: Optional[WebClient] = None,
+        auto_reconnect_enabled: bool = True,
+        trace_enabled: bool = False,
+        all_message_trace_enabled: bool = False,
+        ping_pong_trace_enabled: bool = False,
+        ping_interval: float = 5,
+        receive_buffer_size: int = 1024,
+        concurrency: int = 10,
+        proxy: Optional[str] = None,
+        proxy_headers: Optional[Dict[str, str]] = None,
+        on_message_listeners: Optional[List[Callable[[str], None]]] = None,
+        on_error_listeners: Optional[List[Callable[[Exception], None]]] = None,
+        on_close_listeners: Optional[List[Callable[[int, Optional[str]], None]]] = None,
+    ):
+        """Socket Mode client
+
+        Args:
+            app_token: App-level token
+            logger: Custom logger
+            web_client: Web API client
+            auto_reconnect_enabled: True if automatic reconnection is enabled (default: True)
+            trace_enabled: True if more detailed debug-logging is enabled (default: False)
+            all_message_trace_enabled: True if all message dump in debug logs is enabled (default: False)
+            ping_pong_trace_enabled: True if trace logging for all ping-pong communications is enabled (default: False)
+            ping_interval: interval for ping-pong with Slack servers (seconds)
+            receive_buffer_size: the chunk size of a single socket recv operation (default: 1024)
+            concurrency: the size of thread pool (default: 10)
+            proxy: the HTTP proxy URL
+            proxy_headers: additional HTTP header for proxy connection
+            on_message_listeners: listener functions for on_message
+            on_error_listeners: listener functions for on_error
+            on_close_listeners: listener functions for on_close
+        """
+        self.app_token = app_token
+        self.logger = logger or logging.getLogger(__name__)
+        self.web_client = web_client or WebClient()
+        self.default_auto_reconnect_enabled = auto_reconnect_enabled
+        self.auto_reconnect_enabled = self.default_auto_reconnect_enabled
+        self.trace_enabled = trace_enabled
+        self.all_message_trace_enabled = all_message_trace_enabled
+        self.ping_pong_trace_enabled = ping_pong_trace_enabled
+        self.ping_interval = ping_interval
+        self.receive_buffer_size = receive_buffer_size
+        if self.receive_buffer_size < 16:
+            raise SlackClientConfigurationError("Too small receive_buffer_size detected.")
+
+        self.wss_uri = None
+        self.message_queue = Queue()
+        self.message_listeners = []
+        self.socket_mode_request_listeners = []
+
+        self.current_session = None
+        self.current_session_state = ConnectionState()
+        self.current_session_runner = IntervalRunner(self._run_current_session, 0.1).start()
+
+        self.current_app_monitor_started = False
+        self.current_app_monitor = IntervalRunner(self._monitor_current_session, self.ping_interval)
+
+        self.closed = False
+        self.connect_operation_lock = Lock()
+
+        self.message_processor = IntervalRunner(self.process_messages, 0.001).start()
+        self.message_workers = ThreadPoolExecutor(max_workers=concurrency)
+
+        self.proxy = proxy
+        if self.proxy is None or len(self.proxy.strip()) == 0:
+            env_variable = load_http_proxy_from_env(self.logger)
+            if env_variable is not None:
+                self.proxy = env_variable
+        self.proxy_headers = proxy_headers
+
+        self.on_message_listeners = on_message_listeners or []
+        self.on_error_listeners = on_error_listeners or []
+        self.on_close_listeners = on_close_listeners or []
+
+    def session_id(self) -> Optional[str]:
+        if self.current_session is not None:
+            return self.current_session.session_id
+        return None
+
+    def is_connected(self) -> bool:
+        return self.current_session is not None and self.current_session.is_active()
+
+    def connect(self) -> None:
+        old_session: Optional[Connection] = self.current_session
+        old_current_session_state: ConnectionState = self.current_session_state
+
+        if self.wss_uri is None:
+            self.wss_uri = self.issue_new_wss_url()
+
+        current_session = Connection(
+            url=self.wss_uri,
+            logger=self.logger,
+            ping_interval=self.ping_interval,
+            trace_enabled=self.trace_enabled,
+            all_message_trace_enabled=self.all_message_trace_enabled,
+            ping_pong_trace_enabled=self.ping_pong_trace_enabled,
+            receive_buffer_size=self.receive_buffer_size,
+            proxy=self.proxy,
+            proxy_headers=self.proxy_headers,
+            on_message_listener=self._on_message,
+            on_error_listener=self._on_error,
+            on_close_listener=self._on_close,
+            ssl_context=self.web_client.ssl,
+        )
+        current_session.connect()
+
+        if old_current_session_state is not None:
+            old_current_session_state.terminated = True
+        if old_session is not None:
+            old_session.close()
+
+        self.current_session = current_session
+        self.current_session_state = ConnectionState()
+        self.auto_reconnect_enabled = self.default_auto_reconnect_enabled
+
+        if not self.current_app_monitor_started:
+            self.current_app_monitor_started = True
+            self.current_app_monitor.start()
+
+        self.logger.info(f"A new session has been established (session id: {self.session_id()})")
+
+    def disconnect(self) -> None:
+        if self.current_session is not None:
+            self.current_session.close()
+
+    def send_message(self, message: str) -> None:
+        if self.logger.level <= logging.DEBUG:
+            self.logger.debug(f"Sending a message (session id: {self.session_id()}, message: {message})")
+        try:
+            self.current_session.send(message)  # type: ignore[union-attr]
+        except SlackClientNotConnectedError as e:
+            # We rarely get this exception while replacing the underlying WebSocket connections.
+            # We can do one more try here as the self.current_session should be ready now.
+            if self.logger.level <= logging.DEBUG:
+                self.logger.debug(
+                    f"Failed to send a message (session id: {self.session_id()}, error: {e}, message: {message})"
+                    " as the underlying connection was replaced. Retrying the same request only one time..."
+                )
+            # Although acquiring self.connect_operation_lock also for the first method call is the safest way,
+            # we avoid synchronizing a lot for better performance. That's why we are doing a retry here.
+            with self.connect_operation_lock:
+                if self.is_connected():
+                    self.current_session.send(message)  # type: ignore[union-attr]
+                else:
+                    self.logger.warning(
+                        f"The current session (session id: {self.session_id()}) is no longer active. "
+                        "Failed to send a message"
+                    )
+                    raise e
+
+    def close(self):
+        self.closed = True
+        self.auto_reconnect_enabled = False
+        self.disconnect()
+        if self.current_app_monitor.is_alive():
+            self.current_app_monitor.shutdown()
+        if self.message_processor.is_alive():
+            self.message_processor.shutdown()
+        self.message_workers.shutdown()
+
+    def _on_message(self, message: str):
+        if self.logger.level <= logging.DEBUG:
+            self.logger.debug(f"on_message invoked: (message: {debug_redacted_message_string(message)})")
+        self.enqueue_message(message)
+        for listener in self.on_message_listeners:
+            listener(message)
+
+    def _on_error(self, error: Exception):
+        error_message = (
+            f"on_error invoked (session id: {self.session_id()}, " f"error: {type(error).__name__}, message: {error})"
+        )
+        if self.trace_enabled:
+            self.logger.exception(error_message)
+        else:
+            self.logger.error(error_message)
+
+        for listener in self.on_error_listeners:
+            listener(error)
+
+    def _on_close(self, code: int, reason: Optional[str] = None):
+        if self.logger.level <= logging.DEBUG:
+            self.logger.debug(f"on_close invoked (session id: {self.session_id()})")
+        if self.auto_reconnect_enabled:
+            self.logger.info("Received CLOSE event. Reconnecting... " f"(session id: {self.session_id()})")
+            self.connect_to_new_endpoint()
+        for listener in self.on_close_listeners:
+            listener(code, reason)
+
+    def _run_current_session(self):
+        if self.current_session is not None and self.current_session.is_active():
+            session_id = self.session_id()
+            try:
+                self.logger.info("Starting to receive messages from a new connection" f" (session id: {session_id})")
+                self.current_session_state.terminated = False
+                self.current_session.run_until_completion(self.current_session_state)
+                self.logger.info("Stopped receiving messages from a connection" f" (session id: {session_id})")
+            except Exception as e:
+                error_message = "Failed to start or stop the current session" f" (session id: {session_id}, error: {e})"
+                if self.trace_enabled:
+                    self.logger.exception(error_message)
+                else:
+                    self.logger.error(error_message)
+
+    def _monitor_current_session(self):
+        if self.current_app_monitor_started:
+            try:
+                self.current_session.check_state()
+
+                if self.auto_reconnect_enabled and (self.current_session is None or not self.current_session.is_active()):
+                    self.logger.info(
+                        "The session seems to be already closed. Reconnecting... " f"(session id: {self.session_id()})"
+                    )
+                    self.connect_to_new_endpoint()
+            except Exception as e:
+                self.logger.error(
+                    "Failed to check the current session or reconnect to the server "
+                    f"(session id: {self.session_id()}, error: {type(e).__name__}, message: {e})"
+                )
+
+

Socket Mode client

+

Args

+
+
app_token
+
App-level token
+
logger
+
Custom logger
+
web_client
+
Web API client
+
auto_reconnect_enabled
+
True if automatic reconnection is enabled (default: True)
+
trace_enabled
+
True if more detailed debug-logging is enabled (default: False)
+
all_message_trace_enabled
+
True if all message dump in debug logs is enabled (default: False)
+
ping_pong_trace_enabled
+
True if trace logging for all ping-pong communications is enabled (default: False)
+
ping_interval
+
interval for ping-pong with Slack servers (seconds)
+
receive_buffer_size
+
the chunk size of a single socket recv operation (default: 1024)
+
concurrency
+
the size of thread pool (default: 10)
+
proxy
+
the HTTP proxy URL
+
proxy_headers
+
additional HTTP header for proxy connection
+
on_message_listeners
+
listener functions for on_message
+
on_error_listeners
+
listener functions for on_error
+
on_close_listeners
+
listener functions for on_close
+
+

Ancestors

+ +

Class variables

+
+
var auto_reconnect_enabled : bool
+
+

The type of the None singleton.

+
+
var current_app_monitorIntervalRunner
+
+

The type of the None singleton.

+
+
var current_app_monitor_started : bool
+
+

The type of the None singleton.

+
+
var current_sessionConnection | None
+
+

The type of the None singleton.

+
+
var current_session_runnerIntervalRunner
+
+

The type of the None singleton.

+
+
var current_session_stateConnectionState
+
+

The type of the None singleton.

+
+
var default_auto_reconnect_enabled : bool
+
+

The type of the None singleton.

+
+
var on_close_listeners : List[Callable[[int, str | None], None]]
+
+

The type of the None singleton.

+
+
var on_error_listeners : List[Callable[[Exception], None]]
+
+

The type of the None singleton.

+
+
var on_message_listeners : List[Callable[[str], None]]
+
+

The type of the None singleton.

+
+
var receive_buffer_size : int
+
+

The type of the None singleton.

+
+
var trace_enabled : bool
+
+

The type of the None singleton.

+
+
+

Methods

+
+
+def close(self) +
+
+
+ +Expand source code + +
def close(self):
+    self.closed = True
+    self.auto_reconnect_enabled = False
+    self.disconnect()
+    if self.current_app_monitor.is_alive():
+        self.current_app_monitor.shutdown()
+    if self.message_processor.is_alive():
+        self.message_processor.shutdown()
+    self.message_workers.shutdown()
+
+
+
+
+def connect(self) ‑> None +
+
+
+ +Expand source code + +
def connect(self) -> None:
+    old_session: Optional[Connection] = self.current_session
+    old_current_session_state: ConnectionState = self.current_session_state
+
+    if self.wss_uri is None:
+        self.wss_uri = self.issue_new_wss_url()
+
+    current_session = Connection(
+        url=self.wss_uri,
+        logger=self.logger,
+        ping_interval=self.ping_interval,
+        trace_enabled=self.trace_enabled,
+        all_message_trace_enabled=self.all_message_trace_enabled,
+        ping_pong_trace_enabled=self.ping_pong_trace_enabled,
+        receive_buffer_size=self.receive_buffer_size,
+        proxy=self.proxy,
+        proxy_headers=self.proxy_headers,
+        on_message_listener=self._on_message,
+        on_error_listener=self._on_error,
+        on_close_listener=self._on_close,
+        ssl_context=self.web_client.ssl,
+    )
+    current_session.connect()
+
+    if old_current_session_state is not None:
+        old_current_session_state.terminated = True
+    if old_session is not None:
+        old_session.close()
+
+    self.current_session = current_session
+    self.current_session_state = ConnectionState()
+    self.auto_reconnect_enabled = self.default_auto_reconnect_enabled
+
+    if not self.current_app_monitor_started:
+        self.current_app_monitor_started = True
+        self.current_app_monitor.start()
+
+    self.logger.info(f"A new session has been established (session id: {self.session_id()})")
+
+
+
+
+def disconnect(self) ‑> None +
+
+
+ +Expand source code + +
def disconnect(self) -> None:
+    if self.current_session is not None:
+        self.current_session.close()
+
+
+
+
+def is_connected(self) ‑> bool +
+
+
+ +Expand source code + +
def is_connected(self) -> bool:
+    return self.current_session is not None and self.current_session.is_active()
+
+
+
+
+def send_message(self, message: str) ‑> None +
+
+
+ +Expand source code + +
def send_message(self, message: str) -> None:
+    if self.logger.level <= logging.DEBUG:
+        self.logger.debug(f"Sending a message (session id: {self.session_id()}, message: {message})")
+    try:
+        self.current_session.send(message)  # type: ignore[union-attr]
+    except SlackClientNotConnectedError as e:
+        # We rarely get this exception while replacing the underlying WebSocket connections.
+        # We can do one more try here as the self.current_session should be ready now.
+        if self.logger.level <= logging.DEBUG:
+            self.logger.debug(
+                f"Failed to send a message (session id: {self.session_id()}, error: {e}, message: {message})"
+                " as the underlying connection was replaced. Retrying the same request only one time..."
+            )
+        # Although acquiring self.connect_operation_lock also for the first method call is the safest way,
+        # we avoid synchronizing a lot for better performance. That's why we are doing a retry here.
+        with self.connect_operation_lock:
+            if self.is_connected():
+                self.current_session.send(message)  # type: ignore[union-attr]
+            else:
+                self.logger.warning(
+                    f"The current session (session id: {self.session_id()}) is no longer active. "
+                    "Failed to send a message"
+                )
+                raise e
+
+
+
+
+def session_id(self) ‑> str | None +
+
+
+ +Expand source code + +
def session_id(self) -> Optional[str]:
+    if self.current_session is not None:
+        return self.current_session.session_id
+    return None
+
+
+
+
+

Inherited members

+ +
+
+
+
+ +
+ + + diff --git a/docs/reference/socket_mode/interval_runner.html b/docs/reference/socket_mode/interval_runner.html new file mode 100644 index 000000000..54838d22f --- /dev/null +++ b/docs/reference/socket_mode/interval_runner.html @@ -0,0 +1,180 @@ + + + + + + +slack_sdk.socket_mode.interval_runner API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.socket_mode.interval_runner

+
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class IntervalRunner +(target: Callable[[], None], interval_seconds: float = 0.1) +
+
+
+ +Expand source code + +
class IntervalRunner:
+    event: Event
+    thread: Thread
+
+    def __init__(self, target: Callable[[], None], interval_seconds: float = 0.1):
+        self.event = threading.Event()
+        self.target = target
+        self.interval_seconds = interval_seconds
+        self.thread = threading.Thread(target=self._run)
+        self.thread.daemon = True
+
+    def _run(self) -> None:
+        while not self.event.is_set():
+            self.target()
+            self.event.wait(self.interval_seconds)
+
+    def start(self) -> "IntervalRunner":
+        self.thread.start()
+        return self
+
+    def is_alive(self) -> bool:
+        return self.thread is not None and self.thread.is_alive()
+
+    def shutdown(self):
+        if self.is_alive():
+            self.event.set()
+            self.thread.join()
+        self.thread = None
+
+
+

Class variables

+
+
var event : threading.Event
+
+

The type of the None singleton.

+
+
var thread : threading.Thread
+
+

The type of the None singleton.

+
+
+

Methods

+
+
+def is_alive(self) ‑> bool +
+
+
+ +Expand source code + +
def is_alive(self) -> bool:
+    return self.thread is not None and self.thread.is_alive()
+
+
+
+
+def shutdown(self) +
+
+
+ +Expand source code + +
def shutdown(self):
+    if self.is_alive():
+        self.event.set()
+        self.thread.join()
+    self.thread = None
+
+
+
+
+def start(self) ‑> IntervalRunner +
+
+
+ +Expand source code + +
def start(self) -> "IntervalRunner":
+    self.thread.start()
+    return self
+
+
+
+
+
+
+
+
+ +
+ + + diff --git a/docs/reference/socket_mode/listeners.html b/docs/reference/socket_mode/listeners.html new file mode 100644 index 000000000..daabff8fd --- /dev/null +++ b/docs/reference/socket_mode/listeners.html @@ -0,0 +1,111 @@ + + + + + + +slack_sdk.socket_mode.listeners API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.socket_mode.listeners

+
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class SocketModeRequestListener +
+
+
+ +Expand source code + +
class SocketModeRequestListener:
+    def __call__(client: "BaseSocketModeClient", request: SocketModeRequest):  # type: ignore[name-defined]  # noqa: F821, F821, E501
+        raise NotImplementedError()
+
+
+
+
+class WebSocketMessageListener +
+
+
+ +Expand source code + +
class WebSocketMessageListener:
+    def __call__(
+        client: "BaseSocketModeClient",  # type: ignore[name-defined] # noqa: F821
+        message: dict,
+        raw_message: Optional[str] = None,
+    ):  # noqa: F821
+        raise NotImplementedError()
+
+
+
+
+
+
+ +
+ + + diff --git a/docs/reference/socket_mode/logger/index.html b/docs/reference/socket_mode/logger/index.html new file mode 100644 index 000000000..0d02b13f3 --- /dev/null +++ b/docs/reference/socket_mode/logger/index.html @@ -0,0 +1,78 @@ + + + + + + +slack_sdk.socket_mode.logger API documentation + + + + + + + + + + + +
+ + +
+ + + diff --git a/docs/reference/socket_mode/logger/messages.html b/docs/reference/socket_mode/logger/messages.html new file mode 100644 index 000000000..eb2a0dffe --- /dev/null +++ b/docs/reference/socket_mode/logger/messages.html @@ -0,0 +1,88 @@ + + + + + + +slack_sdk.socket_mode.logger.messages API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.socket_mode.logger.messages

+
+
+
+
+
+
+
+
+

Functions

+
+
+def debug_redacted_message_string(message: str) ‑> str +
+
+
+ +Expand source code + +
def debug_redacted_message_string(message: str) -> str:
+    xwfp_token_pattern = re.compile(r"\"xwfp-[A-Za-z0-9\-]+\"")  # ex: "xwfp-abc-ABC-1234"
+    return re.sub(xwfp_token_pattern, "[[REDACTED]]", message)
+
+
+
+
+
+
+
+
+ +
+ + + diff --git a/docs/reference/socket_mode/request.html b/docs/reference/socket_mode/request.html new file mode 100644 index 000000000..75945aa40 --- /dev/null +++ b/docs/reference/socket_mode/request.html @@ -0,0 +1,205 @@ + + + + + + +slack_sdk.socket_mode.request API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.socket_mode.request

+
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class SocketModeRequest +(type: str,
envelope_id: str,
payload: dict | JsonObject | str,
accepts_response_payload: bool | None = None,
retry_attempt: int | None = None,
retry_reason: str | None = None)
+
+
+
+ +Expand source code + +
class SocketModeRequest:
+    type: str
+    envelope_id: str
+    payload: dict
+    accepts_response_payload: bool
+    retry_attempt: Optional[int]  # events_api
+    retry_reason: Optional[str]  # events_api
+
+    def __init__(
+        self,
+        type: str,
+        envelope_id: str,
+        payload: Union[dict, JsonObject, str],
+        accepts_response_payload: Optional[bool] = None,
+        retry_attempt: Optional[int] = None,
+        retry_reason: Optional[str] = None,
+    ):
+        self.type = type
+        self.envelope_id = envelope_id
+
+        if isinstance(payload, JsonObject):
+            self.payload = payload.to_dict()
+        elif isinstance(payload, dict):
+            self.payload = payload
+        elif isinstance(payload, str):
+            self.payload = {"text": payload}
+        else:
+            unexpected_payload_type = type(payload)
+            raise ValueError(f"Unsupported payload data type ({unexpected_payload_type})")
+
+        self.accepts_response_payload = accepts_response_payload or False
+        self.retry_attempt = retry_attempt
+        self.retry_reason = retry_reason
+
+    @classmethod
+    def from_dict(cls, message: dict) -> Optional["SocketModeRequest"]:
+        if all(k in message for k in ("type", "envelope_id", "payload")):
+            return SocketModeRequest(
+                type=message["type"],
+                envelope_id=message["envelope_id"],
+                payload=message["payload"],
+                accepts_response_payload=message.get("accepts_response_payload") or False,
+                retry_attempt=message.get("retry_attempt"),
+                retry_reason=message.get("retry_reason"),
+            )
+        return None
+
+    def to_dict(self) -> dict:
+        d = {"envelope_id": self.envelope_id}
+        if self.payload is not None:
+            d["payload"] = self.payload  # type: ignore[assignment]
+        return d
+
+
+

Class variables

+
+
var accepts_response_payload : bool
+
+

The type of the None singleton.

+
+
var envelope_id : str
+
+

The type of the None singleton.

+
+
var payload : dict
+
+

The type of the None singleton.

+
+
var retry_attempt : int | None
+
+

The type of the None singleton.

+
+
var retry_reason : str | None
+
+

The type of the None singleton.

+
+
var type : str
+
+

The type of the None singleton.

+
+
+

Static methods

+
+
+def from_dict(message: dict) ‑> SocketModeRequest | None +
+
+
+
+
+

Methods

+
+
+def to_dict(self) ‑> dict +
+
+
+ +Expand source code + +
def to_dict(self) -> dict:
+    d = {"envelope_id": self.envelope_id}
+    if self.payload is not None:
+        d["payload"] = self.payload  # type: ignore[assignment]
+    return d
+
+
+
+
+
+
+
+
+ +
+ + + diff --git a/docs/reference/socket_mode/response.html b/docs/reference/socket_mode/response.html new file mode 100644 index 000000000..0142f7c9e --- /dev/null +++ b/docs/reference/socket_mode/response.html @@ -0,0 +1,146 @@ + + + + + + +slack_sdk.socket_mode.response API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.socket_mode.response

+
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class SocketModeResponse +(envelope_id: str,
payload: dict | JsonObject | str | None = None)
+
+
+
+ +Expand source code + +
class SocketModeResponse:
+    envelope_id: str
+    payload: Optional[dict]
+
+    def __init__(self, envelope_id: str, payload: Optional[Union[dict, JsonObject, str]] = None):
+        self.envelope_id = envelope_id
+
+        if payload is None:
+            self.payload = None
+        elif isinstance(payload, JsonObject):
+            self.payload = payload.to_dict()
+        elif isinstance(payload, dict):
+            self.payload = payload
+        elif isinstance(payload, str):
+            self.payload = {"text": payload}
+        else:
+            raise ValueError(f"Unsupported payload data type ({type(payload)})")
+
+    def to_dict(self) -> dict:
+        d = {"envelope_id": self.envelope_id}
+        if self.payload is not None:
+            d["payload"] = self.payload  # type: ignore[assignment]
+        return d
+
+
+

Class variables

+
+
var envelope_id : str
+
+

The type of the None singleton.

+
+
var payload : dict | None
+
+

The type of the None singleton.

+
+
+

Methods

+
+
+def to_dict(self) ‑> dict +
+
+
+ +Expand source code + +
def to_dict(self) -> dict:
+    d = {"envelope_id": self.envelope_id}
+    if self.payload is not None:
+        d["payload"] = self.payload  # type: ignore[assignment]
+    return d
+
+
+
+
+
+
+
+
+ +
+ + + diff --git a/docs/reference/socket_mode/websocket_client/index.html b/docs/reference/socket_mode/websocket_client/index.html new file mode 100644 index 000000000..b90aa9137 --- /dev/null +++ b/docs/reference/socket_mode/websocket_client/index.html @@ -0,0 +1,588 @@ + + + + + + +slack_sdk.socket_mode.websocket_client API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.socket_mode.websocket_client

+
+
+

websocket-client bassd Socket Mode client

+ +
+
+
+
+
+
+
+
+

Classes

+
+
+class SocketModeClient +(app_token: str,
logger: logging.Logger | None = None,
web_client: WebClient | None = None,
auto_reconnect_enabled: bool = True,
ping_interval: float = 10,
concurrency: int = 10,
trace_enabled: bool = False,
http_proxy_host: str | None = None,
http_proxy_port: int | None = None,
http_proxy_auth: Tuple[str, str] | None = None,
proxy_type: str | None = None,
on_open_listeners: List[Callable[[websocket._app.WebSocketApp], None]] | None = None,
on_message_listeners: List[Callable[[websocket._app.WebSocketApp, str], None]] | None = None,
on_error_listeners: List[Callable[[websocket._app.WebSocketApp, Exception], None]] | None = None,
on_close_listeners: List[Callable[[websocket._app.WebSocketApp], None]] | None = None)
+
+
+
+ +Expand source code + +
class SocketModeClient(BaseSocketModeClient):
+    logger: Logger
+    web_client: WebClient
+    app_token: str
+    wss_uri: Optional[str]  # type: ignore[assignment]
+    message_queue: Queue
+    message_listeners: List[
+        Union[
+            WebSocketMessageListener,
+            Callable[["BaseSocketModeClient", dict, Optional[str]], None],
+        ]
+    ]
+    socket_mode_request_listeners: List[
+        Union[
+            SocketModeRequestListener,
+            Callable[["BaseSocketModeClient", SocketModeRequest], None],
+        ]
+    ]
+
+    current_app_monitor: IntervalRunner
+    current_app_monitor_started: bool
+    message_processor: IntervalRunner
+    message_workers: ThreadPoolExecutor
+
+    current_session: Optional[WebSocketApp]
+    current_session_runner: IntervalRunner
+
+    auto_reconnect_enabled: bool
+    default_auto_reconnect_enabled: bool
+
+    closed: bool
+    connect_operation_lock: Lock
+
+    on_open_listeners: List[Callable[[WebSocketApp], None]]
+    on_message_listeners: List[Callable[[WebSocketApp, str], None]]
+    on_error_listeners: List[Callable[[WebSocketApp, Exception], None]]
+    on_close_listeners: List[Callable[[WebSocketApp], None]]
+
+    def __init__(
+        self,
+        app_token: str,
+        logger: Optional[Logger] = None,
+        web_client: Optional[WebClient] = None,
+        auto_reconnect_enabled: bool = True,
+        ping_interval: float = 10,
+        concurrency: int = 10,
+        trace_enabled: bool = False,
+        http_proxy_host: Optional[str] = None,
+        http_proxy_port: Optional[int] = None,
+        http_proxy_auth: Optional[Tuple[str, str]] = None,
+        proxy_type: Optional[str] = None,
+        on_open_listeners: Optional[List[Callable[[WebSocketApp], None]]] = None,
+        on_message_listeners: Optional[List[Callable[[WebSocketApp, str], None]]] = None,
+        on_error_listeners: Optional[List[Callable[[WebSocketApp, Exception], None]]] = None,
+        on_close_listeners: Optional[List[Callable[[WebSocketApp], None]]] = None,
+    ):
+        """
+
+        Args:
+            app_token: App-level token
+            logger: Custom logger
+            web_client: Web API client
+            auto_reconnect_enabled: True if automatic reconnection is enabled (default: True)
+            ping_interval: interval for ping-pong with Slack servers (seconds)
+            concurrency: the size of thread pool (default: 10)
+            http_proxy_host: the HTTP proxy host
+            http_proxy_port: the HTTP proxy port
+            http_proxy_auth: the HTTP proxy username & password
+            proxy_type: the HTTP proxy type
+            on_open_listeners: listener functions for on_open
+            on_message_listeners: listener functions for on_message
+            on_error_listeners: listener functions for on_error
+            on_close_listeners: listener functions for on_close
+        """
+        self.app_token = app_token
+        self.logger = logger or logging.getLogger(__name__)
+        self.web_client = web_client or WebClient()
+        self.default_auto_reconnect_enabled = auto_reconnect_enabled
+        self.auto_reconnect_enabled = self.default_auto_reconnect_enabled
+        self.ping_interval = ping_interval
+        self.wss_uri = None
+        self.message_queue = Queue()
+        self.message_listeners = []
+        self.socket_mode_request_listeners = []
+
+        self.current_session = None
+        self.current_session_runner = IntervalRunner(self._run_current_session, 0.5).start()
+
+        self.current_app_monitor_started = False
+        self.current_app_monitor = IntervalRunner(self._monitor_current_session, self.ping_interval)
+
+        self.closed = False
+        self.connect_operation_lock = Lock()
+
+        self.message_processor = IntervalRunner(self.process_messages, 0.001).start()
+        self.message_workers = ThreadPoolExecutor(max_workers=concurrency)
+
+        # NOTE: only global settings is provided by the library
+        websocket.enableTrace(trace_enabled)
+
+        self.http_proxy_host = http_proxy_host
+        self.http_proxy_port = http_proxy_port
+        self.http_proxy_auth = http_proxy_auth
+        self.proxy_type = proxy_type
+
+        self.on_open_listeners = on_open_listeners or []
+        self.on_message_listeners = on_message_listeners or []
+        self.on_error_listeners = on_error_listeners or []
+        self.on_close_listeners = on_close_listeners or []
+
+    def is_connected(self) -> bool:
+        return self.current_session is not None and self.current_session.sock is not None
+
+    def connect(self) -> None:
+        def on_open(ws: WebSocketApp):
+            if self.logger.level <= logging.DEBUG:
+                self.logger.debug("on_open invoked")
+            for listener in self.on_open_listeners:
+                listener(ws)
+
+        def on_message(ws: WebSocketApp, message: str):
+            if self.logger.level <= logging.DEBUG:
+                self.logger.debug(f"on_message invoked: (message: {debug_redacted_message_string(message)})")
+            self.enqueue_message(message)
+            for listener in self.on_message_listeners:
+                listener(ws, message)
+
+        def on_error(ws: WebSocketApp, error: Exception):
+            self.logger.error(f"on_error invoked (error: {type(error).__name__}, message: {error})")
+            for listener in self.on_error_listeners:
+                listener(ws, error)
+
+        def on_close(
+            ws: WebSocketApp,
+            close_status_code: Optional[int] = None,
+            close_msg: Optional[str] = None,
+        ):
+            if self.logger.level <= logging.DEBUG:
+                self.logger.debug(f"on_close invoked: (code: {close_status_code}, message: {close_msg})")
+            if self.auto_reconnect_enabled:
+                self.logger.info("Received CLOSE event. Reconnecting...")
+                self.connect_to_new_endpoint()
+            for listener in self.on_close_listeners:
+                listener(ws)
+
+        old_session: Optional[WebSocketApp] = self.current_session
+
+        if self.wss_uri is None:
+            self.wss_uri = self.issue_new_wss_url()
+
+        self.current_session = websocket.WebSocketApp(
+            self.wss_uri,
+            on_open=on_open,
+            on_message=on_message,
+            on_error=on_error,
+            on_close=on_close,
+        )
+        self.auto_reconnect_enabled = self.default_auto_reconnect_enabled
+
+        if not self.current_app_monitor_started:
+            self.current_app_monitor_started = True
+            self.current_app_monitor.start()
+
+        if old_session is not None:
+            old_session.close()
+
+        self.logger.info("A new session has been established")
+
+    def disconnect(self) -> None:
+        if self.current_session is not None:
+            self.current_session.close()
+
+    def send_message(self, message: str) -> None:
+        if self.logger.level <= logging.DEBUG:
+            self.logger.debug(f"Sending a message: {message}")
+        try:
+            self.current_session.send(message)  # type: ignore[union-attr]
+        except WebSocketException as e:
+            # We rarely get this exception while replacing the underlying WebSocket connections.
+            # We can do one more try here as the self.current_session should be ready now.
+            if self.logger.level <= logging.DEBUG:
+                self.logger.debug(
+                    f"Failed to send a message (error: {e}, message: {message})"
+                    " as the underlying connection was replaced. Retrying the same request only one time..."
+                )
+            # Although acquiring self.connect_operation_lock also for the first method call is the safest way,
+            # we avoid synchronizing a lot for better performance. That's why we are doing a retry here.
+            with self.connect_operation_lock:
+                if self.is_connected():
+                    self.current_session.send(message)  # type: ignore[union-attr]
+                else:
+                    self.logger.warning(
+                        f"The current session (session id: {self.session_id()}) is no longer active. "  # type: ignore[attr-defined] # noqa: E501
+                        "Failed to send a message"
+                    )
+                    raise e
+
+    def close(self) -> None:
+        self.closed = True
+        self.auto_reconnect_enabled = False
+        self.disconnect()
+        self.current_app_monitor.shutdown()
+        self.message_processor.shutdown()
+        self.message_workers.shutdown()
+
+    def _run_current_session(self):
+        if self.current_session is not None:
+            try:
+                self.logger.info("Starting to receive messages from a new connection")
+                self.current_session.run_forever(
+                    ping_interval=self.ping_interval,
+                    http_proxy_host=self.http_proxy_host,
+                    http_proxy_port=self.http_proxy_port,
+                    http_proxy_auth=self.http_proxy_auth,
+                    proxy_type=self.proxy_type,
+                )
+                self.logger.info("Stopped receiving messages from a connection")
+            except Exception as e:
+                self.logger.exception(f"Failed to start or stop the current session: {e}")
+                # To let the monitoring job detect the connection issue, closing this session
+                if self.current_session is not None:
+                    self.current_session.close()
+
+    def _monitor_current_session(self):
+        if self.current_app_monitor_started:
+            try:
+                if self.auto_reconnect_enabled and (self.current_session is None or self.current_session.sock is None):
+                    self.logger.info("The session seems to be already closed. Reconnecting...")
+                    self.connect_to_new_endpoint()
+            except Exception as e:
+                self.logger.error(
+                    "Failed to check the current session or reconnect to the server "
+                    f"(error: {type(e).__name__}, message: {e})"
+                )
+
+

Args

+
+
app_token
+
App-level token
+
logger
+
Custom logger
+
web_client
+
Web API client
+
auto_reconnect_enabled
+
True if automatic reconnection is enabled (default: True)
+
ping_interval
+
interval for ping-pong with Slack servers (seconds)
+
concurrency
+
the size of thread pool (default: 10)
+
http_proxy_host
+
the HTTP proxy host
+
http_proxy_port
+
the HTTP proxy port
+
http_proxy_auth
+
the HTTP proxy username & password
+
proxy_type
+
the HTTP proxy type
+
on_open_listeners
+
listener functions for on_open
+
on_message_listeners
+
listener functions for on_message
+
on_error_listeners
+
listener functions for on_error
+
on_close_listeners
+
listener functions for on_close
+
+

Ancestors

+ +

Class variables

+
+
var auto_reconnect_enabled : bool
+
+

The type of the None singleton.

+
+
var current_app_monitorIntervalRunner
+
+

The type of the None singleton.

+
+
var current_app_monitor_started : bool
+
+

The type of the None singleton.

+
+
var current_session : websocket._app.WebSocketApp | None
+
+

The type of the None singleton.

+
+
var current_session_runnerIntervalRunner
+
+

The type of the None singleton.

+
+
var default_auto_reconnect_enabled : bool
+
+

The type of the None singleton.

+
+
var on_close_listeners : List[Callable[[websocket._app.WebSocketApp], None]]
+
+

The type of the None singleton.

+
+
var on_error_listeners : List[Callable[[websocket._app.WebSocketApp, Exception], None]]
+
+

The type of the None singleton.

+
+
var on_message_listeners : List[Callable[[websocket._app.WebSocketApp, str], None]]
+
+

The type of the None singleton.

+
+
var on_open_listeners : List[Callable[[websocket._app.WebSocketApp], None]]
+
+

The type of the None singleton.

+
+
+

Methods

+
+
+def close(self) ‑> None +
+
+
+ +Expand source code + +
def close(self) -> None:
+    self.closed = True
+    self.auto_reconnect_enabled = False
+    self.disconnect()
+    self.current_app_monitor.shutdown()
+    self.message_processor.shutdown()
+    self.message_workers.shutdown()
+
+
+
+
+def connect(self) ‑> None +
+
+
+ +Expand source code + +
def connect(self) -> None:
+    def on_open(ws: WebSocketApp):
+        if self.logger.level <= logging.DEBUG:
+            self.logger.debug("on_open invoked")
+        for listener in self.on_open_listeners:
+            listener(ws)
+
+    def on_message(ws: WebSocketApp, message: str):
+        if self.logger.level <= logging.DEBUG:
+            self.logger.debug(f"on_message invoked: (message: {debug_redacted_message_string(message)})")
+        self.enqueue_message(message)
+        for listener in self.on_message_listeners:
+            listener(ws, message)
+
+    def on_error(ws: WebSocketApp, error: Exception):
+        self.logger.error(f"on_error invoked (error: {type(error).__name__}, message: {error})")
+        for listener in self.on_error_listeners:
+            listener(ws, error)
+
+    def on_close(
+        ws: WebSocketApp,
+        close_status_code: Optional[int] = None,
+        close_msg: Optional[str] = None,
+    ):
+        if self.logger.level <= logging.DEBUG:
+            self.logger.debug(f"on_close invoked: (code: {close_status_code}, message: {close_msg})")
+        if self.auto_reconnect_enabled:
+            self.logger.info("Received CLOSE event. Reconnecting...")
+            self.connect_to_new_endpoint()
+        for listener in self.on_close_listeners:
+            listener(ws)
+
+    old_session: Optional[WebSocketApp] = self.current_session
+
+    if self.wss_uri is None:
+        self.wss_uri = self.issue_new_wss_url()
+
+    self.current_session = websocket.WebSocketApp(
+        self.wss_uri,
+        on_open=on_open,
+        on_message=on_message,
+        on_error=on_error,
+        on_close=on_close,
+    )
+    self.auto_reconnect_enabled = self.default_auto_reconnect_enabled
+
+    if not self.current_app_monitor_started:
+        self.current_app_monitor_started = True
+        self.current_app_monitor.start()
+
+    if old_session is not None:
+        old_session.close()
+
+    self.logger.info("A new session has been established")
+
+
+
+
+def disconnect(self) ‑> None +
+
+
+ +Expand source code + +
def disconnect(self) -> None:
+    if self.current_session is not None:
+        self.current_session.close()
+
+
+
+
+def is_connected(self) ‑> bool +
+
+
+ +Expand source code + +
def is_connected(self) -> bool:
+    return self.current_session is not None and self.current_session.sock is not None
+
+
+
+
+def send_message(self, message: str) ‑> None +
+
+
+ +Expand source code + +
def send_message(self, message: str) -> None:
+    if self.logger.level <= logging.DEBUG:
+        self.logger.debug(f"Sending a message: {message}")
+    try:
+        self.current_session.send(message)  # type: ignore[union-attr]
+    except WebSocketException as e:
+        # We rarely get this exception while replacing the underlying WebSocket connections.
+        # We can do one more try here as the self.current_session should be ready now.
+        if self.logger.level <= logging.DEBUG:
+            self.logger.debug(
+                f"Failed to send a message (error: {e}, message: {message})"
+                " as the underlying connection was replaced. Retrying the same request only one time..."
+            )
+        # Although acquiring self.connect_operation_lock also for the first method call is the safest way,
+        # we avoid synchronizing a lot for better performance. That's why we are doing a retry here.
+        with self.connect_operation_lock:
+            if self.is_connected():
+                self.current_session.send(message)  # type: ignore[union-attr]
+            else:
+                self.logger.warning(
+                    f"The current session (session id: {self.session_id()}) is no longer active. "  # type: ignore[attr-defined] # noqa: E501
+                    "Failed to send a message"
+                )
+                raise e
+
+
+
+
+

Inherited members

+ +
+
+
+
+ +
+ + + diff --git a/docs/reference/socket_mode/websockets/index.html b/docs/reference/socket_mode/websockets/index.html new file mode 100644 index 000000000..e38594eee --- /dev/null +++ b/docs/reference/socket_mode/websockets/index.html @@ -0,0 +1,639 @@ + + + + + + +slack_sdk.socket_mode.websockets API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.socket_mode.websockets

+
+
+

websockets bassd Socket Mode client

+ +
+
+
+
+
+
+
+
+

Classes

+
+
+class SocketModeClient +(app_token: str,
logger: logging.Logger | None = None,
web_client: AsyncWebClient | None = None,
auto_reconnect_enabled: bool = True,
ping_interval: float = 10,
trace_enabled: bool = False)
+
+
+
+ +Expand source code + +
class SocketModeClient(AsyncBaseSocketModeClient):
+    logger: Logger
+    web_client: AsyncWebClient
+    app_token: str
+    wss_uri: Optional[str]  # type: ignore[assignment]
+    auto_reconnect_enabled: bool
+    message_queue: Queue
+    message_listeners: List[
+        Union[
+            AsyncWebSocketMessageListener,
+            Callable[["AsyncBaseSocketModeClient", dict, Optional[str]], Awaitable[None]],
+        ]
+    ]
+    socket_mode_request_listeners: List[
+        Union[
+            AsyncSocketModeRequestListener,
+            Callable[["AsyncBaseSocketModeClient", SocketModeRequest], Awaitable[None]],
+        ]
+    ]
+
+    message_receiver: Optional[Future]
+    message_processor: Future
+
+    ping_interval: float
+    trace_enabled: bool
+
+    current_session: Optional[ClientConnection]
+    current_session_monitor: Optional[Future]
+
+    default_auto_reconnect_enabled: bool
+    closed: bool
+    connect_operation_lock: Lock
+
+    def __init__(
+        self,
+        app_token: str,
+        logger: Optional[Logger] = None,
+        web_client: Optional[AsyncWebClient] = None,
+        auto_reconnect_enabled: bool = True,
+        ping_interval: float = 10,
+        trace_enabled: bool = False,
+    ):
+        """Socket Mode client
+
+        Args:
+            app_token: App-level token
+            logger: Custom logger
+            web_client: Web API client
+            auto_reconnect_enabled: True if automatic reconnection is enabled (default: True)
+            ping_interval: interval for ping-pong with Slack servers (seconds)
+            trace_enabled: True if more verbose logs to see what's happening under the hood
+        """
+        self.app_token = app_token
+        self.logger = logger or logging.getLogger(__name__)
+        self.web_client = web_client or AsyncWebClient()
+        self.closed = False
+        self.connect_operation_lock = Lock()
+        self.default_auto_reconnect_enabled = auto_reconnect_enabled
+        self.auto_reconnect_enabled = self.default_auto_reconnect_enabled
+        self.ping_interval = ping_interval
+        self.trace_enabled = trace_enabled
+        self.wss_uri = None
+        self.message_queue = Queue()
+        self.message_listeners = []
+        self.socket_mode_request_listeners = []
+        self.current_session = None
+        self.current_session_monitor = None
+
+        self.message_receiver = None
+        self.message_processor = asyncio.ensure_future(self.process_messages())
+
+    async def monitor_current_session(self) -> None:
+        # In the asyncio runtime, accessing a shared object (self.current_session here) from
+        # multiple tasks can cause race conditions and errors.
+        # To avoid such, we access only the session that is active when this loop starts.
+        session: ClientConnection = self.current_session  # type: ignore[assignment]
+        session_id: str = await self.session_id()
+        if self.logger.level <= logging.DEBUG:
+            self.logger.debug(f"A new monitor_current_session() execution loop for {session_id} started")
+        try:
+            while not self.closed:
+                if session != self.current_session:
+                    if self.logger.level <= logging.DEBUG:
+                        self.logger.debug(f"The monitor_current_session task for {session_id} is now cancelled")
+                    break
+                await asyncio.sleep(self.ping_interval)
+                try:
+                    if self.auto_reconnect_enabled and _session_closed(session=session):
+                        self.logger.info(f"The session ({session_id}) seems to be already closed. Reconnecting...")
+                        await self.connect_to_new_endpoint()
+                except Exception as e:
+                    self.logger.error(
+                        "Failed to check the current session or reconnect to the server "
+                        f"(error: {type(e).__name__}, message: {e}, session: {session_id})"
+                    )
+        except asyncio.CancelledError:
+            if self.logger.level <= logging.DEBUG:
+                self.logger.debug(f"The monitor_current_session task for {session_id} is now cancelled")
+            raise
+
+    async def receive_messages(self) -> None:
+        # In the asyncio runtime, accessing a shared object (self.current_session here) from
+        # multiple tasks can cause race conditions and errors.
+        # To avoid such, we access only the session that is active when this loop starts.
+        session: ClientConnection = self.current_session  # type: ignore[assignment]
+        session_id: str = await self.session_id()
+        consecutive_error_count = 0
+        if self.logger.level <= logging.DEBUG:
+            self.logger.debug(f"A new receive_messages() execution loop with {session_id} started")
+        try:
+            while not self.closed:
+                if session != self.current_session:
+                    if self.logger.level <= logging.DEBUG:
+                        self.logger.debug(f"The running receive_messages task for {session_id} is now cancelled")
+                    break
+                try:
+                    message = await session.recv()
+                    if message is not None:
+                        if isinstance(message, bytes):
+                            message = message.decode("utf-8")
+                        if self.logger.level <= logging.DEBUG:
+                            self.logger.debug(
+                                f"Received message: {debug_redacted_message_string(message)}, session: {session_id}"
+                            )
+                        await self.enqueue_message(message)
+                    consecutive_error_count = 0
+                except Exception as e:
+                    consecutive_error_count += 1
+                    self.logger.error(
+                        f"Failed to receive or enqueue a message: {type(e).__name__}, error: {e}, session: {session_id}"
+                    )
+                    if isinstance(e, websockets.ConnectionClosedError):
+                        await asyncio.sleep(self.ping_interval)
+                    else:
+                        await asyncio.sleep(consecutive_error_count)
+        except asyncio.CancelledError:
+            if self.logger.level <= logging.DEBUG:
+                self.logger.debug(f"The running receive_messages task for {session_id} is now cancelled")
+            raise
+
+    async def is_connected(self) -> bool:
+        return not self.closed and not _session_closed(self.current_session)
+
+    async def session_id(self) -> str:
+        return self.build_session_id(self.current_session)  # type: ignore[arg-type]
+
+    async def connect(self):
+        if self.wss_uri is None:
+            self.wss_uri = await self.issue_new_wss_url()
+        old_session: Optional[ClientConnection] = None if self.current_session is None else self.current_session
+        # NOTE: websockets does not support proxy settings
+        self.current_session = await websockets.connect(
+            uri=self.wss_uri,
+            ping_interval=self.ping_interval,
+        )
+        session_id = await self.session_id()
+        self.auto_reconnect_enabled = self.default_auto_reconnect_enabled
+        self.logger.info(f"A new session ({session_id}) has been established")
+
+        if self.current_session_monitor is not None:
+            self.current_session_monitor.cancel()
+        self.current_session_monitor = asyncio.ensure_future(self.monitor_current_session())
+
+        if self.logger.level <= logging.DEBUG:
+            self.logger.debug(f"A new monitor_current_session() executor has been recreated for {session_id}")
+
+        if self.message_receiver is not None:
+            self.message_receiver.cancel()
+        self.message_receiver = asyncio.ensure_future(self.receive_messages())
+
+        if self.logger.level <= logging.DEBUG:
+            self.logger.debug(f"A new receive_messages() executor has been recreated for {session_id}")
+
+        if old_session is not None:
+            await old_session.close()
+            old_session_id = self.build_session_id(old_session)
+            self.logger.info(f"The old session ({old_session_id}) has been abandoned")
+
+    async def disconnect(self):
+        if self.current_session is not None:
+            await self.current_session.close()
+
+    async def send_message(self, message: str):
+        session = self.current_session
+        session_id = self.build_session_id(session)  # type: ignore[arg-type]
+        if self.logger.level <= logging.DEBUG:
+            self.logger.debug(f"Sending a message: {message}, session: {session_id}")
+        try:
+            await session.send(message)  # type: ignore[union-attr]
+        except WebSocketException as e:
+            # We rarely get this exception while replacing the underlying WebSocket connections.
+            # We can do one more try here as the self.current_session should be ready now.
+            if self.logger.level <= logging.DEBUG:
+                self.logger.debug(
+                    f"Failed to send a message (error: {e}, message: {message}, session: {session_id})"
+                    " as the underlying connection was replaced. Retrying the same request only one time..."
+                )
+            # Although acquiring self.connect_operation_lock also for the first method call is the safest way,
+            # we avoid synchronizing a lot for better performance. That's why we are doing a retry here.
+            try:
+                if await self.is_connected():
+                    await self.current_session.send(message)  # type: ignore[union-attr]
+                else:
+                    self.logger.warning(f"The current session ({session_id}) is no longer active. Failed to send a message")
+                    raise e
+            finally:
+                if self.connect_operation_lock.locked() is True:
+                    self.connect_operation_lock.release()
+
+    async def close(self):
+        self.closed = True
+        self.auto_reconnect_enabled = False
+        await self.disconnect()
+        self.message_processor.cancel()
+        if self.current_session_monitor is not None:
+            self.current_session_monitor.cancel()
+        if self.message_receiver is not None:
+            self.message_receiver.cancel()
+
+    @classmethod
+    def build_session_id(cls, session: ClientConnection) -> str:
+        if session is None:
+            return ""
+        return "s_" + str(hash(session))
+
+

Socket Mode client

+

Args

+
+
app_token
+
App-level token
+
logger
+
Custom logger
+
web_client
+
Web API client
+
auto_reconnect_enabled
+
True if automatic reconnection is enabled (default: True)
+
ping_interval
+
interval for ping-pong with Slack servers (seconds)
+
trace_enabled
+
True if more verbose logs to see what's happening under the hood
+
+

Ancestors

+ +

Class variables

+
+
var current_session : websockets.asyncio.client.ClientConnection | None
+
+

The type of the None singleton.

+
+
var current_session_monitor : _asyncio.Future | None
+
+

The type of the None singleton.

+
+
var default_auto_reconnect_enabled : bool
+
+

The type of the None singleton.

+
+
var message_processor : _asyncio.Future
+
+

The type of the None singleton.

+
+
var message_receiver : _asyncio.Future | None
+
+

The type of the None singleton.

+
+
var ping_interval : float
+
+

The type of the None singleton.

+
+
+

Static methods

+
+
+def build_session_id(session: websockets.asyncio.client.ClientConnection) ‑> str +
+
+
+
+
+

Methods

+
+
+async def close(self) +
+
+
+ +Expand source code + +
async def close(self):
+    self.closed = True
+    self.auto_reconnect_enabled = False
+    await self.disconnect()
+    self.message_processor.cancel()
+    if self.current_session_monitor is not None:
+        self.current_session_monitor.cancel()
+    if self.message_receiver is not None:
+        self.message_receiver.cancel()
+
+
+
+
+async def connect(self) +
+
+
+ +Expand source code + +
async def connect(self):
+    if self.wss_uri is None:
+        self.wss_uri = await self.issue_new_wss_url()
+    old_session: Optional[ClientConnection] = None if self.current_session is None else self.current_session
+    # NOTE: websockets does not support proxy settings
+    self.current_session = await websockets.connect(
+        uri=self.wss_uri,
+        ping_interval=self.ping_interval,
+    )
+    session_id = await self.session_id()
+    self.auto_reconnect_enabled = self.default_auto_reconnect_enabled
+    self.logger.info(f"A new session ({session_id}) has been established")
+
+    if self.current_session_monitor is not None:
+        self.current_session_monitor.cancel()
+    self.current_session_monitor = asyncio.ensure_future(self.monitor_current_session())
+
+    if self.logger.level <= logging.DEBUG:
+        self.logger.debug(f"A new monitor_current_session() executor has been recreated for {session_id}")
+
+    if self.message_receiver is not None:
+        self.message_receiver.cancel()
+    self.message_receiver = asyncio.ensure_future(self.receive_messages())
+
+    if self.logger.level <= logging.DEBUG:
+        self.logger.debug(f"A new receive_messages() executor has been recreated for {session_id}")
+
+    if old_session is not None:
+        await old_session.close()
+        old_session_id = self.build_session_id(old_session)
+        self.logger.info(f"The old session ({old_session_id}) has been abandoned")
+
+
+
+
+async def disconnect(self) +
+
+
+ +Expand source code + +
async def disconnect(self):
+    if self.current_session is not None:
+        await self.current_session.close()
+
+
+
+
+async def is_connected(self) ‑> bool +
+
+
+ +Expand source code + +
async def is_connected(self) -> bool:
+    return not self.closed and not _session_closed(self.current_session)
+
+
+
+
+async def monitor_current_session(self) ‑> None +
+
+
+ +Expand source code + +
async def monitor_current_session(self) -> None:
+    # In the asyncio runtime, accessing a shared object (self.current_session here) from
+    # multiple tasks can cause race conditions and errors.
+    # To avoid such, we access only the session that is active when this loop starts.
+    session: ClientConnection = self.current_session  # type: ignore[assignment]
+    session_id: str = await self.session_id()
+    if self.logger.level <= logging.DEBUG:
+        self.logger.debug(f"A new monitor_current_session() execution loop for {session_id} started")
+    try:
+        while not self.closed:
+            if session != self.current_session:
+                if self.logger.level <= logging.DEBUG:
+                    self.logger.debug(f"The monitor_current_session task for {session_id} is now cancelled")
+                break
+            await asyncio.sleep(self.ping_interval)
+            try:
+                if self.auto_reconnect_enabled and _session_closed(session=session):
+                    self.logger.info(f"The session ({session_id}) seems to be already closed. Reconnecting...")
+                    await self.connect_to_new_endpoint()
+            except Exception as e:
+                self.logger.error(
+                    "Failed to check the current session or reconnect to the server "
+                    f"(error: {type(e).__name__}, message: {e}, session: {session_id})"
+                )
+    except asyncio.CancelledError:
+        if self.logger.level <= logging.DEBUG:
+            self.logger.debug(f"The monitor_current_session task for {session_id} is now cancelled")
+        raise
+
+
+
+
+async def receive_messages(self) ‑> None +
+
+
+ +Expand source code + +
async def receive_messages(self) -> None:
+    # In the asyncio runtime, accessing a shared object (self.current_session here) from
+    # multiple tasks can cause race conditions and errors.
+    # To avoid such, we access only the session that is active when this loop starts.
+    session: ClientConnection = self.current_session  # type: ignore[assignment]
+    session_id: str = await self.session_id()
+    consecutive_error_count = 0
+    if self.logger.level <= logging.DEBUG:
+        self.logger.debug(f"A new receive_messages() execution loop with {session_id} started")
+    try:
+        while not self.closed:
+            if session != self.current_session:
+                if self.logger.level <= logging.DEBUG:
+                    self.logger.debug(f"The running receive_messages task for {session_id} is now cancelled")
+                break
+            try:
+                message = await session.recv()
+                if message is not None:
+                    if isinstance(message, bytes):
+                        message = message.decode("utf-8")
+                    if self.logger.level <= logging.DEBUG:
+                        self.logger.debug(
+                            f"Received message: {debug_redacted_message_string(message)}, session: {session_id}"
+                        )
+                    await self.enqueue_message(message)
+                consecutive_error_count = 0
+            except Exception as e:
+                consecutive_error_count += 1
+                self.logger.error(
+                    f"Failed to receive or enqueue a message: {type(e).__name__}, error: {e}, session: {session_id}"
+                )
+                if isinstance(e, websockets.ConnectionClosedError):
+                    await asyncio.sleep(self.ping_interval)
+                else:
+                    await asyncio.sleep(consecutive_error_count)
+    except asyncio.CancelledError:
+        if self.logger.level <= logging.DEBUG:
+            self.logger.debug(f"The running receive_messages task for {session_id} is now cancelled")
+        raise
+
+
+
+
+async def send_message(self, message: str) +
+
+
+ +Expand source code + +
async def send_message(self, message: str):
+    session = self.current_session
+    session_id = self.build_session_id(session)  # type: ignore[arg-type]
+    if self.logger.level <= logging.DEBUG:
+        self.logger.debug(f"Sending a message: {message}, session: {session_id}")
+    try:
+        await session.send(message)  # type: ignore[union-attr]
+    except WebSocketException as e:
+        # We rarely get this exception while replacing the underlying WebSocket connections.
+        # We can do one more try here as the self.current_session should be ready now.
+        if self.logger.level <= logging.DEBUG:
+            self.logger.debug(
+                f"Failed to send a message (error: {e}, message: {message}, session: {session_id})"
+                " as the underlying connection was replaced. Retrying the same request only one time..."
+            )
+        # Although acquiring self.connect_operation_lock also for the first method call is the safest way,
+        # we avoid synchronizing a lot for better performance. That's why we are doing a retry here.
+        try:
+            if await self.is_connected():
+                await self.current_session.send(message)  # type: ignore[union-attr]
+            else:
+                self.logger.warning(f"The current session ({session_id}) is no longer active. Failed to send a message")
+                raise e
+        finally:
+            if self.connect_operation_lock.locked() is True:
+                self.connect_operation_lock.release()
+
+
+
+
+async def session_id(self) ‑> str +
+
+
+ +Expand source code + +
async def session_id(self) -> str:
+    return self.build_session_id(self.current_session)  # type: ignore[arg-type]
+
+
+
+
+

Inherited members

+ +
+
+
+
+ +
+ + + diff --git a/docs/reference/version.html b/docs/reference/version.html new file mode 100644 index 000000000..9f8e3c5bb --- /dev/null +++ b/docs/reference/version.html @@ -0,0 +1,67 @@ + + + + + + +slack_sdk.version API documentation + + + + + + + + + + + +
+ + +
+ + + diff --git a/docs/reference/web/async_base_client.html b/docs/reference/web/async_base_client.html new file mode 100644 index 000000000..97615d924 --- /dev/null +++ b/docs/reference/web/async_base_client.html @@ -0,0 +1,535 @@ + + + + + + +slack_sdk.web.async_base_client API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.web.async_base_client

+
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class AsyncBaseClient +(token: str | None = None,
base_url: str = 'https://slack.com/api/',
timeout: int = 30,
ssl: ssl.SSLContext | None = None,
proxy: str | None = None,
session: aiohttp.client.ClientSession | None = None,
trust_env_in_session: bool = False,
headers: dict | None = None,
user_agent_prefix: str | None = None,
user_agent_suffix: str | None = None,
team_id: str | None = None,
logger: logging.Logger | None = None,
retry_handlers: List[slack_sdk.http_retry.async_handler.AsyncRetryHandler] | None = None)
+
+
+
+ +Expand source code + +
class AsyncBaseClient:
+    BASE_URL = "https://slack.com/api/"
+
+    def __init__(
+        self,
+        token: Optional[str] = None,
+        base_url: str = BASE_URL,
+        timeout: int = 30,
+        ssl: Optional[SSLContext] = None,
+        proxy: Optional[str] = None,
+        session: Optional[aiohttp.ClientSession] = None,
+        trust_env_in_session: bool = False,
+        headers: Optional[dict] = None,
+        user_agent_prefix: Optional[str] = None,
+        user_agent_suffix: Optional[str] = None,
+        # for Org-Wide App installation
+        team_id: Optional[str] = None,
+        logger: Optional[logging.Logger] = None,
+        retry_handlers: Optional[List[AsyncRetryHandler]] = None,
+    ):
+        self.token = None if token is None else token.strip()
+        """A string specifying an `xoxp-*` or `xoxb-*` token."""
+        if not base_url.endswith("/"):
+            base_url += "/"
+        self.base_url = base_url
+        """A string representing the Slack API base URL.
+        Default is `'https://slack.com/api/'`."""
+        self.timeout = timeout
+        """The maximum number of seconds the client will wait
+        to connect and receive a response from Slack.
+        Default is 30 seconds."""
+        self.ssl = ssl
+        """An [`ssl.SSLContext`](https://docs.python.org/3/library/ssl.html#ssl.SSLContext)
+        instance, helpful for specifying your own custom
+        certificate chain."""
+        self.proxy = proxy
+        """String representing a fully-qualified URL to a proxy through which
+        to route all requests to the Slack API. Even if this parameter
+        is not specified, if any of the following environment variables are
+        present, they will be loaded into this parameter: `HTTPS_PROXY`,
+        `https_proxy`, `HTTP_PROXY` or `http_proxy`."""
+        self.session = session
+        """An [`aiohttp.ClientSession`](https://docs.aiohttp.org/en/stable/client_reference.html#client-session)
+        to attach to all outgoing requests."""
+        # https://github.com/slackapi/python-slack-sdk/issues/738
+        self.trust_env_in_session = trust_env_in_session
+        """Boolean setting whether aiohttp outgoing requests
+        are allowed to read environment variables. Commonly used in conjunction
+        with proxy support via the `HTTPS_PROXY`, `https_proxy`, `HTTP_PROXY` and
+        `http_proxy` environment variables."""
+        self.headers = headers or {}
+        """`dict` representing additional request headers to attach to all requests."""
+        self.headers["User-Agent"] = get_user_agent(user_agent_prefix, user_agent_suffix)
+        self.default_params = {}
+        if team_id is not None:
+            self.default_params["team_id"] = team_id
+        self._logger = logger if logger is not None else logging.getLogger(__name__)
+        self.retry_handlers = retry_handlers if retry_handlers is not None else async_default_handlers()
+
+        if self.proxy is None or len(self.proxy.strip()) == 0:
+            env_variable = load_http_proxy_from_env(self._logger)
+            if env_variable is not None:
+                self.proxy = env_variable
+
+    # -------------------------
+    # accessors
+
+    @property
+    def logger(self) -> logging.Logger:
+        """The logger this client uses."""
+        return self._logger
+
+    # -------------------------
+    # api call
+
+    async def api_call(
+        self,
+        api_method: str,
+        *,
+        http_verb: str = "POST",
+        files: Optional[dict] = None,
+        data: Optional[Union[dict, FormData]] = None,
+        params: Optional[dict] = None,
+        json: Optional[dict] = None,
+        headers: Optional[dict] = None,
+        auth: Optional[dict] = None,
+    ) -> AsyncSlackResponse:
+        """Create a request and execute the API call to Slack.
+
+        Args:
+            api_method (str): The target Slack API method.
+                e.g. 'chat.postMessage'
+            http_verb (str): HTTP Verb. e.g. 'POST'
+            files (dict): Files to multipart upload.
+                e.g. {image OR file: file_object OR file_path}
+            data: The body to attach to the request. If a dictionary is
+                provided, form-encoding will take place.
+                e.g. {'key1': 'value1', 'key2': 'value2'}
+            params (dict): The URL parameters to append to the URL.
+                e.g. {'key1': 'value1', 'key2': 'value2'}
+            json (dict): JSON for the body to attach to the request
+                (if files or data is not specified).
+                e.g. {'key1': 'value1', 'key2': 'value2'}
+            headers (dict): Additional request headers
+            auth (dict): A dictionary that consists of client_id and client_secret
+
+        Returns:
+            (AsyncSlackResponse)
+                The server's response to an HTTP request. Data
+                from the response can be accessed like a dict.
+                If the response included 'next_cursor' it can
+                be iterated on to execute subsequent requests.
+
+        Raises:
+            SlackApiError: The following Slack API call failed:
+                'chat.postMessage'.
+            SlackRequestError: Json data can only be submitted as
+                POST requests.
+        """
+
+        api_url = _get_url(self.base_url, api_method)
+        if auth is not None:
+            if isinstance(auth, Dict):
+                auth = BasicAuth(auth["client_id"], auth["client_secret"])  # type: ignore[assignment]
+            if isinstance(auth, BasicAuth):
+                if headers is None:
+                    headers = {}
+                headers["Authorization"] = auth.encode()
+                auth = None
+
+        headers = headers or {}
+        headers.update(self.headers)
+        req_args = _build_req_args(
+            token=self.token,
+            http_verb=http_verb,
+            files=files,  # type: ignore[arg-type]
+            data=data,  # type: ignore[arg-type]
+            default_params=self.default_params,
+            params=params,  # type: ignore[arg-type]
+            json=json,  # type: ignore[arg-type]
+            headers=headers,
+            auth=auth,  # type: ignore[arg-type]
+            ssl=self.ssl,
+            proxy=self.proxy,
+        )
+
+        show_deprecation_warning_if_any(api_method)
+
+        return await self._send(
+            http_verb=http_verb,
+            api_url=api_url,
+            req_args=req_args,
+        )
+
+    async def _send(self, http_verb: str, api_url: str, req_args: dict) -> AsyncSlackResponse:
+        """Sends the request out for transmission.
+
+        Args:
+            http_verb (str): The HTTP verb. e.g. 'GET' or 'POST'.
+            api_url (str): The Slack API url. e.g. 'https://slack.com/api/chat.postMessage'
+            req_args (dict): The request arguments to be attached to the request.
+            e.g.
+            {
+                json: {
+                    'attachments': [{"pretext": "pre-hello", "text": "text-world"}],
+                    'channel': '#random'
+                }
+            }
+        Returns:
+            The response parsed into a AsyncSlackResponse object.
+        """
+        open_files = _files_to_data(req_args)
+        try:
+            if "params" in req_args:
+                # True/False -> "1"/"0"
+                req_args["params"] = convert_bool_to_0_or_1(req_args["params"])
+
+            res = await self._request(http_verb=http_verb, api_url=api_url, req_args=req_args)
+        finally:
+            for f in open_files:
+                f.close()
+
+        data = {
+            "client": self,
+            "http_verb": http_verb,
+            "api_url": api_url,
+            "req_args": req_args,
+        }
+        return AsyncSlackResponse(**{**data, **res}).validate()
+
+    async def _request(self, *, http_verb, api_url, req_args) -> Dict[str, Any]:
+        """Submit the HTTP request with the running session or a new session.
+        Returns:
+            A dictionary of the response data.
+        """
+        return await _request_with_session(
+            current_session=self.session,
+            timeout=self.timeout,
+            logger=self._logger,
+            http_verb=http_verb,
+            api_url=api_url,
+            req_args=req_args,
+            retry_handlers=self.retry_handlers,
+        )
+
+    async def _upload_file(
+        self,
+        *,
+        url: str,
+        data: bytes,
+        logger: logging.Logger,
+        timeout: int,
+        proxy: Optional[str],
+        ssl: Optional[SSLContext],
+    ) -> FileUploadV2Result:
+        """Upload a file using the issued upload URL"""
+        result = await _request_with_session(
+            current_session=self.session,
+            timeout=timeout,
+            logger=logger,
+            http_verb="POST",
+            api_url=url,
+            req_args={"data": data, "proxy": proxy, "ssl": ssl},
+            retry_handlers=self.retry_handlers,
+        )
+        return FileUploadV2Result(
+            status=result.get("status_code"),  # type: ignore[arg-type]
+            body=result.get("body"),  # type: ignore[arg-type]
+        )
+
+
+

Subclasses

+ +

Class variables

+
+
var BASE_URL
+
+

The type of the None singleton.

+
+
+

Instance variables

+
+
var base_url
+
+

A string representing the Slack API base URL. +Default is 'https://slack.com/api/'.

+
+
var headers
+
+

dict representing additional request headers to attach to all requests.

+
+
prop logger : logging.Logger
+
+
+ +Expand source code + +
@property
+def logger(self) -> logging.Logger:
+    """The logger this client uses."""
+    return self._logger
+
+

The logger this client uses.

+
+
var proxy
+
+

String representing a fully-qualified URL to a proxy through which +to route all requests to the Slack API. Even if this parameter +is not specified, if any of the following environment variables are +present, they will be loaded into this parameter: HTTPS_PROXY, +https_proxy, HTTP_PROXY or http_proxy.

+
+
var session
+
+

An aiohttp.ClientSession +to attach to all outgoing requests.

+
+
var ssl
+
+

An ssl.SSLContext +instance, helpful for specifying your own custom +certificate chain.

+
+
var timeout
+
+

The maximum number of seconds the client will wait +to connect and receive a response from Slack. +Default is 30 seconds.

+
+
var token
+
+

A string specifying an xoxp-* or xoxb-* token.

+
+
var trust_env_in_session
+
+

Boolean setting whether aiohttp outgoing requests +are allowed to read environment variables. Commonly used in conjunction +with proxy support via the HTTPS_PROXY, https_proxy, HTTP_PROXY and +http_proxy environment variables.

+
+
+

Methods

+
+
+async def api_call(self,
api_method: str,
*,
http_verb: str = 'POST',
files: dict | None = None,
data: dict | aiohttp.formdata.FormData | None = None,
params: dict | None = None,
json: dict | None = None,
headers: dict | None = None,
auth: dict | None = None) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def api_call(
+    self,
+    api_method: str,
+    *,
+    http_verb: str = "POST",
+    files: Optional[dict] = None,
+    data: Optional[Union[dict, FormData]] = None,
+    params: Optional[dict] = None,
+    json: Optional[dict] = None,
+    headers: Optional[dict] = None,
+    auth: Optional[dict] = None,
+) -> AsyncSlackResponse:
+    """Create a request and execute the API call to Slack.
+
+    Args:
+        api_method (str): The target Slack API method.
+            e.g. 'chat.postMessage'
+        http_verb (str): HTTP Verb. e.g. 'POST'
+        files (dict): Files to multipart upload.
+            e.g. {image OR file: file_object OR file_path}
+        data: The body to attach to the request. If a dictionary is
+            provided, form-encoding will take place.
+            e.g. {'key1': 'value1', 'key2': 'value2'}
+        params (dict): The URL parameters to append to the URL.
+            e.g. {'key1': 'value1', 'key2': 'value2'}
+        json (dict): JSON for the body to attach to the request
+            (if files or data is not specified).
+            e.g. {'key1': 'value1', 'key2': 'value2'}
+        headers (dict): Additional request headers
+        auth (dict): A dictionary that consists of client_id and client_secret
+
+    Returns:
+        (AsyncSlackResponse)
+            The server's response to an HTTP request. Data
+            from the response can be accessed like a dict.
+            If the response included 'next_cursor' it can
+            be iterated on to execute subsequent requests.
+
+    Raises:
+        SlackApiError: The following Slack API call failed:
+            'chat.postMessage'.
+        SlackRequestError: Json data can only be submitted as
+            POST requests.
+    """
+
+    api_url = _get_url(self.base_url, api_method)
+    if auth is not None:
+        if isinstance(auth, Dict):
+            auth = BasicAuth(auth["client_id"], auth["client_secret"])  # type: ignore[assignment]
+        if isinstance(auth, BasicAuth):
+            if headers is None:
+                headers = {}
+            headers["Authorization"] = auth.encode()
+            auth = None
+
+    headers = headers or {}
+    headers.update(self.headers)
+    req_args = _build_req_args(
+        token=self.token,
+        http_verb=http_verb,
+        files=files,  # type: ignore[arg-type]
+        data=data,  # type: ignore[arg-type]
+        default_params=self.default_params,
+        params=params,  # type: ignore[arg-type]
+        json=json,  # type: ignore[arg-type]
+        headers=headers,
+        auth=auth,  # type: ignore[arg-type]
+        ssl=self.ssl,
+        proxy=self.proxy,
+    )
+
+    show_deprecation_warning_if_any(api_method)
+
+    return await self._send(
+        http_verb=http_verb,
+        api_url=api_url,
+        req_args=req_args,
+    )
+
+

Create a request and execute the API call to Slack.

+

Args

+
+
api_method : str
+
The target Slack API method. +e.g. 'chat.postMessage'
+
http_verb : str
+
HTTP Verb. e.g. 'POST'
+
files : dict
+
Files to multipart upload. +e.g. {image OR file: file_object OR file_path}
+
data
+
The body to attach to the request. If a dictionary is +provided, form-encoding will take place. +e.g. {'key1': 'value1', 'key2': 'value2'}
+
params : dict
+
The URL parameters to append to the URL. +e.g. {'key1': 'value1', 'key2': 'value2'}
+
json : dict
+
JSON for the body to attach to the request +(if files or data is not specified). +e.g. {'key1': 'value1', 'key2': 'value2'}
+
headers : dict
+
Additional request headers
+
auth : dict
+
A dictionary that consists of client_id and client_secret
+
+

Returns

+

(AsyncSlackResponse) +The server's response to an HTTP request. Data +from the response can be accessed like a dict. +If the response included 'next_cursor' it can +be iterated on to execute subsequent requests.

+

Raises

+
+
SlackApiError
+
The following Slack API call failed: +'chat.postMessage'.
+
SlackRequestError
+
Json data can only be submitted as +POST requests.
+
+
+
+
+
+
+
+ +
+ + + diff --git a/docs/reference/web/async_chat_stream.html b/docs/reference/web/async_chat_stream.html new file mode 100644 index 000000000..ca7bf2506 --- /dev/null +++ b/docs/reference/web/async_chat_stream.html @@ -0,0 +1,506 @@ + + + + + + +slack_sdk.web.async_chat_stream API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.web.async_chat_stream

+
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class AsyncChatStream +(client: AsyncWebClient,
*,
channel: str,
logger: logging.Logger,
thread_ts: str,
buffer_size: int,
recipient_team_id: str | None = None,
recipient_user_id: str | None = None,
**kwargs)
+
+
+
+ +Expand source code + +
class AsyncChatStream:
+    """A helper class for streaming markdown text into a conversation using the chat streaming APIs.
+
+    This class provides a convenient interface for the chat.startStream, chat.appendStream, and chat.stopStream API
+    methods, with automatic buffering and state management.
+    """
+
+    def __init__(
+        self,
+        client: "AsyncWebClient",
+        *,
+        channel: str,
+        logger: logging.Logger,
+        thread_ts: str,
+        buffer_size: int,
+        recipient_team_id: Optional[str] = None,
+        recipient_user_id: Optional[str] = None,
+        **kwargs,
+    ):
+        """Initialize a new ChatStream instance.
+
+        The __init__ method creates a unique ChatStream instance that keeps track of one chat stream.
+
+        Args:
+            client: The WebClient instance to use for API calls.
+            channel: An encoded ID that represents a channel, private group, or DM.
+            logger: A logging channel for outputs.
+            thread_ts: Provide another message's ts value to reply to. Streamed messages should always be replies to a user
+              request.
+            recipient_team_id: The encoded ID of the team the user receiving the streaming text belongs to. Required when
+              streaming to channels.
+            recipient_user_id: The encoded ID of the user to receive the streaming text. Required when streaming to channels.
+            buffer_size: The length of markdown_text to buffer in-memory before calling a method. Increasing this value
+              decreases the number of method calls made for the same amount of text, which is useful to avoid rate limits.
+            **kwargs: Additional arguments passed to the underlying API calls.
+        """
+        self._client = client
+        self._logger = logger
+        self._token: Optional[str] = kwargs.pop("token", None)
+        self._stream_args = {
+            "channel": channel,
+            "thread_ts": thread_ts,
+            "recipient_team_id": recipient_team_id,
+            "recipient_user_id": recipient_user_id,
+            **kwargs,
+        }
+        self._buffer = ""
+        self._state = "starting"
+        self._stream_ts: Optional[str] = None
+        self._buffer_size = buffer_size
+
+    async def append(
+        self,
+        *,
+        markdown_text: str,
+        **kwargs,
+    ) -> Optional[AsyncSlackResponse]:
+        """Append to the stream.
+
+        The "append" method appends to the chat stream being used. This method can be called multiple times. After the stream
+        is stopped this method cannot be called.
+
+        Args:
+            markdown_text: Accepts message text formatted in markdown. Limit this field to 12,000 characters. This text is
+              what will be appended to the message received so far.
+            **kwargs: Additional arguments passed to the underlying API calls.
+
+        Returns:
+            AsyncSlackResponse if the buffer was flushed, None if buffering.
+
+        Raises:
+            SlackRequestError: If the stream is already completed.
+
+        Example:
+            ```python
+            streamer = client.chat_stream(
+                channel="C0123456789",
+                thread_ts="1700000001.123456",
+                recipient_team_id="T0123456789",
+                recipient_user_id="U0123456789",
+            )
+            streamer.append(markdown_text="**hello wo")
+            streamer.append(markdown_text="rld!**")
+            streamer.stop()
+            ```
+        """
+        if self._state == "completed":
+            raise e.SlackRequestError(f"Cannot append to stream: stream state is {self._state}")
+        if kwargs.get("token"):
+            self._token = kwargs.pop("token")
+        self._buffer += markdown_text
+        if len(self._buffer) >= self._buffer_size:
+            return await self._flush_buffer(**kwargs)
+        details = {
+            "buffer_length": len(self._buffer),
+            "buffer_size": self._buffer_size,
+            "channel": self._stream_args.get("channel"),
+            "recipient_team_id": self._stream_args.get("recipient_team_id"),
+            "recipient_user_id": self._stream_args.get("recipient_user_id"),
+            "thread_ts": self._stream_args.get("thread_ts"),
+        }
+        self._logger.debug(f"ChatStream appended to buffer: {json.dumps(details)}")
+        return None
+
+    async def stop(
+        self,
+        *,
+        markdown_text: Optional[str] = None,
+        blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None,
+        metadata: Optional[Union[Dict, Metadata]] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Stop the stream and finalize the message.
+
+        Args:
+            blocks: A list of blocks that will be rendered at the bottom of the finalized message.
+            markdown_text: Accepts message text formatted in markdown. Limit this field to 12,000 characters. This text is
+              what will be appended to the message received so far.
+            metadata: JSON object with event_type and event_payload fields, presented as a URL-encoded string. Metadata you
+              post to Slack is accessible to any app or user who is a member of that workspace.
+            **kwargs: Additional arguments passed to the underlying API calls.
+
+        Returns:
+            AsyncSlackResponse from the chat.stopStream API call.
+
+        Raises:
+            SlackRequestError: If the stream is already completed.
+
+        Example:
+            ```python
+            streamer = client.chat_stream(
+                channel="C0123456789",
+                thread_ts="1700000001.123456",
+                recipient_team_id="T0123456789",
+                recipient_user_id="U0123456789",
+            )
+            streamer.append(markdown_text="**hello wo")
+            streamer.append(markdown_text="rld!**")
+            streamer.stop()
+            ```
+        """
+        if self._state == "completed":
+            raise e.SlackRequestError(f"Cannot stop stream: stream state is {self._state}")
+        if kwargs.get("token"):
+            self._token = kwargs.pop("token")
+        if markdown_text:
+            self._buffer += markdown_text
+        if not self._stream_ts:
+            response = await self._client.chat_startStream(
+                **self._stream_args,
+                token=self._token,
+            )
+            if not response.get("ts"):
+                raise e.SlackRequestError("Failed to stop stream: stream not started")
+            self._stream_ts = str(response["ts"])
+            self._state = "in_progress"
+        response = await self._client.chat_stopStream(
+            token=self._token,
+            channel=self._stream_args["channel"],
+            ts=self._stream_ts,
+            blocks=blocks,
+            markdown_text=self._buffer,
+            metadata=metadata,
+            **kwargs,
+        )
+        self._state = "completed"
+        return response
+
+    async def _flush_buffer(self, **kwargs) -> AsyncSlackResponse:
+        """Flush the internal buffer by making appropriate API calls."""
+        if not self._stream_ts:
+            response = await self._client.chat_startStream(
+                **self._stream_args,
+                token=self._token,
+                **kwargs,
+                markdown_text=self._buffer,
+            )
+            self._stream_ts = response.get("ts")
+            self._state = "in_progress"
+        else:
+            response = await self._client.chat_appendStream(
+                token=self._token,
+                channel=self._stream_args["channel"],
+                ts=self._stream_ts,
+                **kwargs,
+                markdown_text=self._buffer,
+            )
+        self._buffer = ""
+        return response
+
+

A helper class for streaming markdown text into a conversation using the chat streaming APIs.

+

This class provides a convenient interface for the chat.startStream, chat.appendStream, and chat.stopStream API +methods, with automatic buffering and state management.

+

Initialize a new ChatStream instance.

+

The init method creates a unique ChatStream instance that keeps track of one chat stream.

+

Args

+
+
client
+
The WebClient instance to use for API calls.
+
channel
+
An encoded ID that represents a channel, private group, or DM.
+
logger
+
A logging channel for outputs.
+
thread_ts
+
Provide another message's ts value to reply to. Streamed messages should always be replies to a user +request.
+
recipient_team_id
+
The encoded ID of the team the user receiving the streaming text belongs to. Required when +streaming to channels.
+
recipient_user_id
+
The encoded ID of the user to receive the streaming text. Required when streaming to channels.
+
buffer_size
+
The length of markdown_text to buffer in-memory before calling a method. Increasing this value +decreases the number of method calls made for the same amount of text, which is useful to avoid rate limits.
+
**kwargs
+
Additional arguments passed to the underlying API calls.
+
+

Methods

+
+
+async def append(self, *, markdown_text: str, **kwargs) ‑> AsyncSlackResponse | None +
+
+
+ +Expand source code + +
async def append(
+    self,
+    *,
+    markdown_text: str,
+    **kwargs,
+) -> Optional[AsyncSlackResponse]:
+    """Append to the stream.
+
+    The "append" method appends to the chat stream being used. This method can be called multiple times. After the stream
+    is stopped this method cannot be called.
+
+    Args:
+        markdown_text: Accepts message text formatted in markdown. Limit this field to 12,000 characters. This text is
+          what will be appended to the message received so far.
+        **kwargs: Additional arguments passed to the underlying API calls.
+
+    Returns:
+        AsyncSlackResponse if the buffer was flushed, None if buffering.
+
+    Raises:
+        SlackRequestError: If the stream is already completed.
+
+    Example:
+        ```python
+        streamer = client.chat_stream(
+            channel="C0123456789",
+            thread_ts="1700000001.123456",
+            recipient_team_id="T0123456789",
+            recipient_user_id="U0123456789",
+        )
+        streamer.append(markdown_text="**hello wo")
+        streamer.append(markdown_text="rld!**")
+        streamer.stop()
+        ```
+    """
+    if self._state == "completed":
+        raise e.SlackRequestError(f"Cannot append to stream: stream state is {self._state}")
+    if kwargs.get("token"):
+        self._token = kwargs.pop("token")
+    self._buffer += markdown_text
+    if len(self._buffer) >= self._buffer_size:
+        return await self._flush_buffer(**kwargs)
+    details = {
+        "buffer_length": len(self._buffer),
+        "buffer_size": self._buffer_size,
+        "channel": self._stream_args.get("channel"),
+        "recipient_team_id": self._stream_args.get("recipient_team_id"),
+        "recipient_user_id": self._stream_args.get("recipient_user_id"),
+        "thread_ts": self._stream_args.get("thread_ts"),
+    }
+    self._logger.debug(f"ChatStream appended to buffer: {json.dumps(details)}")
+    return None
+
+

Append to the stream.

+

The "append" method appends to the chat stream being used. This method can be called multiple times. After the stream +is stopped this method cannot be called.

+

Args

+
+
markdown_text
+
Accepts message text formatted in markdown. Limit this field to 12,000 characters. This text is +what will be appended to the message received so far.
+
**kwargs
+
Additional arguments passed to the underlying API calls.
+
+

Returns

+

AsyncSlackResponse if the buffer was flushed, None if buffering.

+

Raises

+
+
SlackRequestError
+
If the stream is already completed.
+
+

Example

+
streamer = client.chat_stream(
+    channel="C0123456789",
+    thread_ts="1700000001.123456",
+    recipient_team_id="T0123456789",
+    recipient_user_id="U0123456789",
+)
+streamer.append(markdown_text="**hello wo")
+streamer.append(markdown_text="rld!**")
+streamer.stop()
+
+
+
+async def stop(self,
*,
markdown_text: str | None = None,
blocks: str | Sequence[Dict | Block] | None = None,
metadata: Dict | Metadata | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def stop(
+    self,
+    *,
+    markdown_text: Optional[str] = None,
+    blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None,
+    metadata: Optional[Union[Dict, Metadata]] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Stop the stream and finalize the message.
+
+    Args:
+        blocks: A list of blocks that will be rendered at the bottom of the finalized message.
+        markdown_text: Accepts message text formatted in markdown. Limit this field to 12,000 characters. This text is
+          what will be appended to the message received so far.
+        metadata: JSON object with event_type and event_payload fields, presented as a URL-encoded string. Metadata you
+          post to Slack is accessible to any app or user who is a member of that workspace.
+        **kwargs: Additional arguments passed to the underlying API calls.
+
+    Returns:
+        AsyncSlackResponse from the chat.stopStream API call.
+
+    Raises:
+        SlackRequestError: If the stream is already completed.
+
+    Example:
+        ```python
+        streamer = client.chat_stream(
+            channel="C0123456789",
+            thread_ts="1700000001.123456",
+            recipient_team_id="T0123456789",
+            recipient_user_id="U0123456789",
+        )
+        streamer.append(markdown_text="**hello wo")
+        streamer.append(markdown_text="rld!**")
+        streamer.stop()
+        ```
+    """
+    if self._state == "completed":
+        raise e.SlackRequestError(f"Cannot stop stream: stream state is {self._state}")
+    if kwargs.get("token"):
+        self._token = kwargs.pop("token")
+    if markdown_text:
+        self._buffer += markdown_text
+    if not self._stream_ts:
+        response = await self._client.chat_startStream(
+            **self._stream_args,
+            token=self._token,
+        )
+        if not response.get("ts"):
+            raise e.SlackRequestError("Failed to stop stream: stream not started")
+        self._stream_ts = str(response["ts"])
+        self._state = "in_progress"
+    response = await self._client.chat_stopStream(
+        token=self._token,
+        channel=self._stream_args["channel"],
+        ts=self._stream_ts,
+        blocks=blocks,
+        markdown_text=self._buffer,
+        metadata=metadata,
+        **kwargs,
+    )
+    self._state = "completed"
+    return response
+
+

Stop the stream and finalize the message.

+

Args

+
+
blocks
+
A list of blocks that will be rendered at the bottom of the finalized message.
+
markdown_text
+
Accepts message text formatted in markdown. Limit this field to 12,000 characters. This text is +what will be appended to the message received so far.
+
metadata
+
JSON object with event_type and event_payload fields, presented as a URL-encoded string. Metadata you +post to Slack is accessible to any app or user who is a member of that workspace.
+
**kwargs
+
Additional arguments passed to the underlying API calls.
+
+

Returns

+

AsyncSlackResponse from the chat.stopStream API call.

+

Raises

+
+
SlackRequestError
+
If the stream is already completed.
+
+

Example

+
streamer = client.chat_stream(
+    channel="C0123456789",
+    thread_ts="1700000001.123456",
+    recipient_team_id="T0123456789",
+    recipient_user_id="U0123456789",
+)
+streamer.append(markdown_text="**hello wo")
+streamer.append(markdown_text="rld!**")
+streamer.stop()
+
+
+
+
+
+
+
+ +
+ + + diff --git a/docs/reference/web/async_client.html b/docs/reference/web/async_client.html new file mode 100644 index 000000000..2ec1ba84a --- /dev/null +++ b/docs/reference/web/async_client.html @@ -0,0 +1,15788 @@ + + + + + + +slack_sdk.web.async_client API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.web.async_client

+
+
+

A Python module for interacting with Slack's Web API.

+
+
+
+
+
+
+
+
+

Classes

+
+
+class AsyncWebClient +(token: str | None = None,
base_url: str = 'https://slack.com/api/',
timeout: int = 30,
ssl: ssl.SSLContext | None = None,
proxy: str | None = None,
session: aiohttp.client.ClientSession | None = None,
trust_env_in_session: bool = False,
headers: dict | None = None,
user_agent_prefix: str | None = None,
user_agent_suffix: str | None = None,
team_id: str | None = None,
logger: logging.Logger | None = None,
retry_handlers: List[slack_sdk.http_retry.async_handler.AsyncRetryHandler] | None = None)
+
+
+
+ +Expand source code + +
class AsyncWebClient(AsyncBaseClient):
+    """A WebClient allows apps to communicate with the Slack Platform's Web API.
+
+    https://docs.slack.dev/reference/methods
+
+    The Slack Web API is an interface for querying information from
+    and enacting change in a Slack workspace.
+
+    This client handles constructing and sending HTTP requests to Slack
+    as well as parsing any responses received into a `SlackResponse`.
+
+    Attributes:
+        token (str): A string specifying an `xoxp-*` or `xoxb-*` token.
+        base_url (str): A string representing the Slack API base URL.
+            Default is `'https://slack.com/api/'`
+        timeout (int): The maximum number of seconds the client will wait
+            to connect and receive a response from Slack.
+            Default is 30 seconds.
+        ssl (SSLContext): An [`ssl.SSLContext`][1] instance, helpful for specifying
+            your own custom certificate chain.
+        proxy (str): String representing a fully-qualified URL to a proxy through
+            which to route all requests to the Slack API. Even if this parameter
+            is not specified, if any of the following environment variables are
+            present, they will be loaded into this parameter: `HTTPS_PROXY`,
+            `https_proxy`, `HTTP_PROXY` or `http_proxy`.
+        headers (dict): Additional request headers to attach to all requests.
+
+    Methods:
+        `api_call`: Constructs a request and executes the API call to Slack.
+
+    Example of recommended usage:
+    ```python
+        import os
+        from slack_sdk.web.async_client import AsyncWebClient
+
+        client = AsyncWebClient(token=os.environ['SLACK_API_TOKEN'])
+        response = client.chat_postMessage(
+            channel='#random',
+            text="Hello world!")
+        assert response["ok"]
+        assert response["message"]["text"] == "Hello world!"
+    ```
+
+    Example manually creating an API request:
+    ```python
+        import os
+        from slack_sdk.web.async_client import AsyncWebClient
+
+        client = AsyncWebClient(token=os.environ['SLACK_API_TOKEN'])
+        response = client.api_call(
+            api_method='chat.postMessage',
+            json={'channel': '#random','text': "Hello world!"}
+        )
+        assert response["ok"]
+        assert response["message"]["text"] == "Hello world!"
+    ```
+
+    Note:
+        Any attributes or methods prefixed with _underscores are
+        intended to be "private" internal use only. They may be changed or
+        removed at anytime.
+
+    [1]: https://docs.python.org/3/library/ssl.html#ssl.SSLContext
+    """
+
+    async def admin_analytics_getFile(
+        self,
+        *,
+        type: str,
+        date: Optional[str] = None,
+        metadata_only: Optional[bool] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Retrieve analytics data for a given date, presented as a compressed JSON file
+        https://docs.slack.dev/reference/methods/admin.analytics.getFile
+        """
+        kwargs.update({"type": type})
+        if date is not None:
+            kwargs.update({"date": date})
+        if metadata_only is not None:
+            kwargs.update({"metadata_only": metadata_only})
+        return await self.api_call("admin.analytics.getFile", params=kwargs)
+
+    async def admin_apps_approve(
+        self,
+        *,
+        app_id: Optional[str] = None,
+        request_id: Optional[str] = None,
+        enterprise_id: Optional[str] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Approve an app for installation on a workspace.
+        Either app_id or request_id is required.
+        These IDs can be obtained either directly via the app_requested event,
+        or by the admin.apps.requests.list method.
+        https://docs.slack.dev/reference/methods/admin.apps.approve
+        """
+        if app_id:
+            kwargs.update({"app_id": app_id})
+        elif request_id:
+            kwargs.update({"request_id": request_id})
+        else:
+            raise e.SlackRequestError("The app_id or request_id argument must be specified.")
+
+        kwargs.update(
+            {
+                "enterprise_id": enterprise_id,
+                "team_id": team_id,
+            }
+        )
+        return await self.api_call("admin.apps.approve", params=kwargs)
+
+    async def admin_apps_approved_list(
+        self,
+        *,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        enterprise_id: Optional[str] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """List approved apps for an org or workspace.
+        https://docs.slack.dev/reference/methods/admin.apps.approved.list
+        """
+        kwargs.update(
+            {
+                "cursor": cursor,
+                "limit": limit,
+                "enterprise_id": enterprise_id,
+                "team_id": team_id,
+            }
+        )
+        return await self.api_call("admin.apps.approved.list", http_verb="GET", params=kwargs)
+
+    async def admin_apps_clearResolution(
+        self,
+        *,
+        app_id: str,
+        enterprise_id: Optional[str] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Clear an app resolution
+        https://docs.slack.dev/reference/methods/admin.apps.clearResolution
+        """
+        kwargs.update(
+            {
+                "app_id": app_id,
+                "enterprise_id": enterprise_id,
+                "team_id": team_id,
+            }
+        )
+        return await self.api_call("admin.apps.clearResolution", http_verb="POST", params=kwargs)
+
+    async def admin_apps_requests_cancel(
+        self,
+        *,
+        request_id: str,
+        enterprise_id: Optional[str] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """List app requests for a team/workspace.
+        https://docs.slack.dev/reference/methods/admin.apps.requests.cancel
+        """
+        kwargs.update(
+            {
+                "request_id": request_id,
+                "enterprise_id": enterprise_id,
+                "team_id": team_id,
+            }
+        )
+        return await self.api_call("admin.apps.requests.cancel", http_verb="POST", params=kwargs)
+
+    async def admin_apps_requests_list(
+        self,
+        *,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """List app requests for a team/workspace.
+        https://docs.slack.dev/reference/methods/admin.apps.requests.list
+        """
+        kwargs.update(
+            {
+                "cursor": cursor,
+                "limit": limit,
+                "team_id": team_id,
+            }
+        )
+        return await self.api_call("admin.apps.requests.list", http_verb="GET", params=kwargs)
+
+    async def admin_apps_restrict(
+        self,
+        *,
+        app_id: Optional[str] = None,
+        request_id: Optional[str] = None,
+        enterprise_id: Optional[str] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Restrict an app for installation on a workspace.
+        Exactly one of the team_id or enterprise_id arguments is required, not both.
+        Either app_id or request_id is required. These IDs can be obtained either directly
+        via the app_requested event, or by the admin.apps.requests.list method.
+        https://docs.slack.dev/reference/methods/admin.apps.restrict
+        """
+        if app_id:
+            kwargs.update({"app_id": app_id})
+        elif request_id:
+            kwargs.update({"request_id": request_id})
+        else:
+            raise e.SlackRequestError("The app_id or request_id argument must be specified.")
+
+        kwargs.update(
+            {
+                "enterprise_id": enterprise_id,
+                "team_id": team_id,
+            }
+        )
+        return await self.api_call("admin.apps.restrict", params=kwargs)
+
+    async def admin_apps_restricted_list(
+        self,
+        *,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        enterprise_id: Optional[str] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """List restricted apps for an org or workspace.
+        https://docs.slack.dev/reference/methods/admin.apps.restricted.list
+        """
+        kwargs.update(
+            {
+                "cursor": cursor,
+                "limit": limit,
+                "enterprise_id": enterprise_id,
+                "team_id": team_id,
+            }
+        )
+        return await self.api_call("admin.apps.restricted.list", http_verb="GET", params=kwargs)
+
+    async def admin_apps_uninstall(
+        self,
+        *,
+        app_id: str,
+        enterprise_id: Optional[str] = None,
+        team_ids: Optional[Union[str, Sequence[str]]] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Uninstall an app from one or many workspaces, or an entire enterprise organization.
+        With an org-level token, enterprise_id or team_ids is required.
+        https://docs.slack.dev/reference/methods/admin.apps.uninstall
+        """
+        kwargs.update({"app_id": app_id})
+        if enterprise_id is not None:
+            kwargs.update({"enterprise_id": enterprise_id})
+        if team_ids is not None:
+            if isinstance(team_ids, (list, tuple)):
+                kwargs.update({"team_ids": ",".join(team_ids)})
+            else:
+                kwargs.update({"team_ids": team_ids})
+        return await self.api_call("admin.apps.uninstall", http_verb="POST", params=kwargs)
+
+    async def admin_apps_activities_list(
+        self,
+        *,
+        app_id: Optional[str] = None,
+        component_id: Optional[str] = None,
+        component_type: Optional[str] = None,
+        log_event_type: Optional[str] = None,
+        max_date_created: Optional[int] = None,
+        min_date_created: Optional[int] = None,
+        min_log_level: Optional[str] = None,
+        sort_direction: Optional[str] = None,
+        source: Optional[str] = None,
+        team_id: Optional[str] = None,
+        trace_id: Optional[str] = None,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Get logs for a specified team/org
+        https://docs.slack.dev/reference/methods/admin.apps.activities.list
+        """
+        kwargs.update(
+            {
+                "app_id": app_id,
+                "component_id": component_id,
+                "component_type": component_type,
+                "log_event_type": log_event_type,
+                "max_date_created": max_date_created,
+                "min_date_created": min_date_created,
+                "min_log_level": min_log_level,
+                "sort_direction": sort_direction,
+                "source": source,
+                "team_id": team_id,
+                "trace_id": trace_id,
+                "cursor": cursor,
+                "limit": limit,
+            }
+        )
+        return await self.api_call("admin.apps.activities.list", params=kwargs)
+
+    async def admin_apps_config_lookup(
+        self,
+        *,
+        app_ids: Union[str, Sequence[str]],
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Look up the app config for connectors by their IDs
+        https://docs.slack.dev/reference/methods/admin.apps.config.lookup
+        """
+        if isinstance(app_ids, (list, tuple)):
+            kwargs.update({"app_ids": ",".join(app_ids)})
+        else:
+            kwargs.update({"app_ids": app_ids})
+        return await self.api_call("admin.apps.config.lookup", params=kwargs)
+
+    async def admin_apps_config_set(
+        self,
+        *,
+        app_id: str,
+        domain_restrictions: Optional[Dict[str, Any]] = None,
+        workflow_auth_strategy: Optional[str] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Set the app config for a connector
+        https://docs.slack.dev/reference/methods/admin.apps.config.set
+        """
+        kwargs.update(
+            {
+                "app_id": app_id,
+                "workflow_auth_strategy": workflow_auth_strategy,
+            }
+        )
+        if domain_restrictions is not None:
+            kwargs.update({"domain_restrictions": json.dumps(domain_restrictions)})
+        return await self.api_call("admin.apps.config.set", params=kwargs)
+
+    async def admin_auth_policy_getEntities(
+        self,
+        *,
+        policy_name: str,
+        cursor: Optional[str] = None,
+        entity_type: Optional[str] = None,
+        limit: Optional[int] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Fetch all the entities assigned to a particular authentication policy by name.
+        https://docs.slack.dev/reference/methods/admin.auth.policy.getEntities
+        """
+        kwargs.update({"policy_name": policy_name})
+        if cursor is not None:
+            kwargs.update({"cursor": cursor})
+        if entity_type is not None:
+            kwargs.update({"entity_type": entity_type})
+        if limit is not None:
+            kwargs.update({"limit": limit})
+        return await self.api_call("admin.auth.policy.getEntities", http_verb="POST", params=kwargs)
+
+    async def admin_auth_policy_assignEntities(
+        self,
+        *,
+        entity_ids: Union[str, Sequence[str]],
+        policy_name: str,
+        entity_type: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Assign entities to a particular authentication policy.
+        https://docs.slack.dev/reference/methods/admin.auth.policy.assignEntities
+        """
+        if isinstance(entity_ids, (list, tuple)):
+            kwargs.update({"entity_ids": ",".join(entity_ids)})
+        else:
+            kwargs.update({"entity_ids": entity_ids})
+        kwargs.update({"policy_name": policy_name})
+        kwargs.update({"entity_type": entity_type})
+        return await self.api_call("admin.auth.policy.assignEntities", http_verb="POST", params=kwargs)
+
+    async def admin_auth_policy_removeEntities(
+        self,
+        *,
+        entity_ids: Union[str, Sequence[str]],
+        policy_name: str,
+        entity_type: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Remove specified entities from a specified authentication policy.
+        https://docs.slack.dev/reference/methods/admin.auth.policy.removeEntities
+        """
+        if isinstance(entity_ids, (list, tuple)):
+            kwargs.update({"entity_ids": ",".join(entity_ids)})
+        else:
+            kwargs.update({"entity_ids": entity_ids})
+        kwargs.update({"policy_name": policy_name})
+        kwargs.update({"entity_type": entity_type})
+        return await self.api_call("admin.auth.policy.removeEntities", http_verb="POST", params=kwargs)
+
+    async def admin_conversations_createForObjects(
+        self,
+        *,
+        object_id: str,
+        salesforce_org_id: str,
+        invite_object_team: Optional[bool] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Create a Salesforce channel for the corresponding object provided.
+        https://docs.slack.dev/reference/methods/admin.conversations.createForObjects
+        """
+        kwargs.update(
+            {"object_id": object_id, "salesforce_org_id": salesforce_org_id, "invite_object_team": invite_object_team}
+        )
+        return await self.api_call("admin.conversations.createForObjects", params=kwargs)
+
+    async def admin_conversations_linkObjects(
+        self,
+        *,
+        channel: str,
+        record_id: str,
+        salesforce_org_id: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Link a Salesforce record to a channel.
+        https://docs.slack.dev/reference/methods/admin.conversations.linkObjects
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "record_id": record_id,
+                "salesforce_org_id": salesforce_org_id,
+            }
+        )
+        return await self.api_call("admin.conversations.linkObjects", params=kwargs)
+
+    async def admin_conversations_unlinkObjects(
+        self,
+        *,
+        channel: str,
+        new_name: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Unlink a Salesforce record from a channel.
+        https://docs.slack.dev/reference/methods/admin.conversations.unlinkObjects
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "new_name": new_name,
+            }
+        )
+        return await self.api_call("admin.conversations.unlinkObjects", params=kwargs)
+
+    async def admin_barriers_create(
+        self,
+        *,
+        barriered_from_usergroup_ids: Union[str, Sequence[str]],
+        primary_usergroup_id: str,
+        restricted_subjects: Union[str, Sequence[str]],
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Create an Information Barrier
+        https://docs.slack.dev/reference/methods/admin.barriers.create
+        """
+        kwargs.update({"primary_usergroup_id": primary_usergroup_id})
+        if isinstance(barriered_from_usergroup_ids, (list, tuple)):
+            kwargs.update({"barriered_from_usergroup_ids": ",".join(barriered_from_usergroup_ids)})
+        else:
+            kwargs.update({"barriered_from_usergroup_ids": barriered_from_usergroup_ids})
+        if isinstance(restricted_subjects, (list, tuple)):
+            kwargs.update({"restricted_subjects": ",".join(restricted_subjects)})
+        else:
+            kwargs.update({"restricted_subjects": restricted_subjects})
+        return await self.api_call("admin.barriers.create", http_verb="POST", params=kwargs)
+
+    async def admin_barriers_delete(
+        self,
+        *,
+        barrier_id: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Delete an existing Information Barrier
+        https://docs.slack.dev/reference/methods/admin.barriers.delete
+        """
+        kwargs.update({"barrier_id": barrier_id})
+        return await self.api_call("admin.barriers.delete", http_verb="POST", params=kwargs)
+
+    async def admin_barriers_update(
+        self,
+        *,
+        barrier_id: str,
+        barriered_from_usergroup_ids: Union[str, Sequence[str]],
+        primary_usergroup_id: str,
+        restricted_subjects: Union[str, Sequence[str]],
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Update an existing Information Barrier
+        https://docs.slack.dev/reference/methods/admin.barriers.update
+        """
+        kwargs.update({"barrier_id": barrier_id, "primary_usergroup_id": primary_usergroup_id})
+        if isinstance(barriered_from_usergroup_ids, (list, tuple)):
+            kwargs.update({"barriered_from_usergroup_ids": ",".join(barriered_from_usergroup_ids)})
+        else:
+            kwargs.update({"barriered_from_usergroup_ids": barriered_from_usergroup_ids})
+        if isinstance(restricted_subjects, (list, tuple)):
+            kwargs.update({"restricted_subjects": ",".join(restricted_subjects)})
+        else:
+            kwargs.update({"restricted_subjects": restricted_subjects})
+        return await self.api_call("admin.barriers.update", http_verb="POST", params=kwargs)
+
+    async def admin_barriers_list(
+        self,
+        *,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Get all Information Barriers for your organization
+        https://docs.slack.dev/reference/methods/admin.barriers.list"""
+        kwargs.update(
+            {
+                "cursor": cursor,
+                "limit": limit,
+            }
+        )
+        return await self.api_call("admin.barriers.list", http_verb="GET", params=kwargs)
+
+    async def admin_conversations_create(
+        self,
+        *,
+        is_private: bool,
+        name: str,
+        description: Optional[str] = None,
+        org_wide: Optional[bool] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Create a public or private channel-based conversation.
+        https://docs.slack.dev/reference/methods/admin.conversations.create
+        """
+        kwargs.update(
+            {
+                "is_private": is_private,
+                "name": name,
+                "description": description,
+                "org_wide": org_wide,
+                "team_id": team_id,
+            }
+        )
+        return await self.api_call("admin.conversations.create", params=kwargs)
+
+    async def admin_conversations_delete(
+        self,
+        *,
+        channel_id: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Delete a public or private channel.
+        https://docs.slack.dev/reference/methods/admin.conversations.delete
+        """
+        kwargs.update({"channel_id": channel_id})
+        return await self.api_call("admin.conversations.delete", params=kwargs)
+
+    async def admin_conversations_invite(
+        self,
+        *,
+        channel_id: str,
+        user_ids: Union[str, Sequence[str]],
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Invite a user to a public or private channel.
+        https://docs.slack.dev/reference/methods/admin.conversations.invite
+        """
+        kwargs.update({"channel_id": channel_id})
+        if isinstance(user_ids, (list, tuple)):
+            kwargs.update({"user_ids": ",".join(user_ids)})
+        else:
+            kwargs.update({"user_ids": user_ids})
+        # NOTE: the endpoint is unable to handle Content-Type: application/json as of Sep 3, 2020.
+        return await self.api_call("admin.conversations.invite", params=kwargs)
+
+    async def admin_conversations_archive(
+        self,
+        *,
+        channel_id: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Archive a public or private channel.
+        https://docs.slack.dev/reference/methods/admin.conversations.archive
+        """
+        kwargs.update({"channel_id": channel_id})
+        return await self.api_call("admin.conversations.archive", params=kwargs)
+
+    async def admin_conversations_unarchive(
+        self,
+        *,
+        channel_id: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Unarchive a public or private channel.
+        https://docs.slack.dev/reference/methods/admin.conversations.archive
+        """
+        kwargs.update({"channel_id": channel_id})
+        return await self.api_call("admin.conversations.unarchive", params=kwargs)
+
+    async def admin_conversations_rename(
+        self,
+        *,
+        channel_id: str,
+        name: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Rename a public or private channel.
+        https://docs.slack.dev/reference/methods/admin.conversations.rename
+        """
+        kwargs.update({"channel_id": channel_id, "name": name})
+        return await self.api_call("admin.conversations.rename", params=kwargs)
+
+    async def admin_conversations_search(
+        self,
+        *,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        query: Optional[str] = None,
+        search_channel_types: Optional[Union[str, Sequence[str]]] = None,
+        sort: Optional[str] = None,
+        sort_dir: Optional[str] = None,
+        team_ids: Optional[Union[str, Sequence[str]]] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Search for public or private channels in an Enterprise organization.
+        https://docs.slack.dev/reference/methods/admin.conversations.search
+        """
+        kwargs.update(
+            {
+                "cursor": cursor,
+                "limit": limit,
+                "query": query,
+                "sort": sort,
+                "sort_dir": sort_dir,
+            }
+        )
+
+        if isinstance(search_channel_types, (list, tuple)):
+            kwargs.update({"search_channel_types": ",".join(search_channel_types)})
+        else:
+            kwargs.update({"search_channel_types": search_channel_types})
+
+        if isinstance(team_ids, (list, tuple)):
+            kwargs.update({"team_ids": ",".join(team_ids)})
+        else:
+            kwargs.update({"team_ids": team_ids})
+
+        return await self.api_call("admin.conversations.search", params=kwargs)
+
+    async def admin_conversations_convertToPrivate(
+        self,
+        *,
+        channel_id: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Convert a public channel to a private channel.
+        https://docs.slack.dev/reference/methods/admin.conversations.convertToPrivate
+        """
+        kwargs.update({"channel_id": channel_id})
+        return await self.api_call("admin.conversations.convertToPrivate", params=kwargs)
+
+    async def admin_conversations_convertToPublic(
+        self,
+        *,
+        channel_id: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Convert a privte channel to a public channel.
+        https://docs.slack.dev/reference/methods/admin.conversations.convertToPublic
+        """
+        kwargs.update({"channel_id": channel_id})
+        return await self.api_call("admin.conversations.convertToPublic", params=kwargs)
+
+    async def admin_conversations_setConversationPrefs(
+        self,
+        *,
+        channel_id: str,
+        prefs: Union[str, Dict[str, str]],
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Set the posting permissions for a public or private channel.
+        https://docs.slack.dev/reference/methods/admin.conversations.setConversationPrefs
+        """
+        kwargs.update({"channel_id": channel_id})
+        if isinstance(prefs, dict):
+            kwargs.update({"prefs": json.dumps(prefs)})
+        else:
+            kwargs.update({"prefs": prefs})
+        return await self.api_call("admin.conversations.setConversationPrefs", params=kwargs)
+
+    async def admin_conversations_getConversationPrefs(
+        self,
+        *,
+        channel_id: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Get conversation preferences for a public or private channel.
+        https://docs.slack.dev/reference/methods/admin.conversations.getConversationPrefs
+        """
+        kwargs.update({"channel_id": channel_id})
+        return await self.api_call("admin.conversations.getConversationPrefs", params=kwargs)
+
+    async def admin_conversations_disconnectShared(
+        self,
+        *,
+        channel_id: str,
+        leaving_team_ids: Optional[Union[str, Sequence[str]]] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Disconnect a connected channel from one or more workspaces.
+        https://docs.slack.dev/reference/methods/admin.conversations.disconnectShared
+        """
+        kwargs.update({"channel_id": channel_id})
+        if isinstance(leaving_team_ids, (list, tuple)):
+            kwargs.update({"leaving_team_ids": ",".join(leaving_team_ids)})
+        else:
+            kwargs.update({"leaving_team_ids": leaving_team_ids})
+        return await self.api_call("admin.conversations.disconnectShared", params=kwargs)
+
+    async def admin_conversations_lookup(
+        self,
+        *,
+        last_message_activity_before: int,
+        team_ids: Union[str, Sequence[str]],
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        max_member_count: Optional[int] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Returns channels on the given team using the filters.
+        https://docs.slack.dev/reference/methods/admin.conversations.lookup
+        """
+        kwargs.update(
+            {
+                "last_message_activity_before": last_message_activity_before,
+                "cursor": cursor,
+                "limit": limit,
+                "max_member_count": max_member_count,
+            }
+        )
+        if isinstance(team_ids, (list, tuple)):
+            kwargs.update({"team_ids": ",".join(team_ids)})
+        else:
+            kwargs.update({"team_ids": team_ids})
+        return await self.api_call("admin.conversations.lookup", params=kwargs)
+
+    async def admin_conversations_ekm_listOriginalConnectedChannelInfo(
+        self,
+        *,
+        channel_ids: Optional[Union[str, Sequence[str]]] = None,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        team_ids: Optional[Union[str, Sequence[str]]] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """List all disconnected channels—i.e.,
+        channels that were once connected to other workspaces and then disconnected—and
+        the corresponding original channel IDs for key revocation with EKM.
+        https://docs.slack.dev/reference/methods/admin.conversations.ekm.listOriginalConnectedChannelInfo
+        """
+        kwargs.update(
+            {
+                "cursor": cursor,
+                "limit": limit,
+            }
+        )
+        if isinstance(channel_ids, (list, tuple)):
+            kwargs.update({"channel_ids": ",".join(channel_ids)})
+        else:
+            kwargs.update({"channel_ids": channel_ids})
+        if isinstance(team_ids, (list, tuple)):
+            kwargs.update({"team_ids": ",".join(team_ids)})
+        else:
+            kwargs.update({"team_ids": team_ids})
+        return await self.api_call("admin.conversations.ekm.listOriginalConnectedChannelInfo", params=kwargs)
+
+    async def admin_conversations_restrictAccess_addGroup(
+        self,
+        *,
+        channel_id: str,
+        group_id: str,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Add an allowlist of IDP groups for accessing a channel.
+        https://docs.slack.dev/reference/methods/admin.conversations.restrictAccess.addGroup
+        """
+        kwargs.update(
+            {
+                "channel_id": channel_id,
+                "group_id": group_id,
+                "team_id": team_id,
+            }
+        )
+        return await self.api_call(
+            "admin.conversations.restrictAccess.addGroup",
+            http_verb="GET",
+            params=kwargs,
+        )
+
+    async def admin_conversations_restrictAccess_listGroups(
+        self,
+        *,
+        channel_id: str,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """List all IDP Groups linked to a channel.
+        https://docs.slack.dev/reference/methods/admin.conversations.restrictAccess.listGroups
+        """
+        kwargs.update(
+            {
+                "channel_id": channel_id,
+                "team_id": team_id,
+            }
+        )
+        return await self.api_call(
+            "admin.conversations.restrictAccess.listGroups",
+            http_verb="GET",
+            params=kwargs,
+        )
+
+    async def admin_conversations_restrictAccess_removeGroup(
+        self,
+        *,
+        channel_id: str,
+        group_id: str,
+        team_id: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Remove a linked IDP group linked from a private channel.
+        https://docs.slack.dev/reference/methods/admin.conversations.restrictAccess.removeGroup
+        """
+        kwargs.update(
+            {
+                "channel_id": channel_id,
+                "group_id": group_id,
+                "team_id": team_id,
+            }
+        )
+        return await self.api_call(
+            "admin.conversations.restrictAccess.removeGroup",
+            http_verb="GET",
+            params=kwargs,
+        )
+
+    async def admin_conversations_setTeams(
+        self,
+        *,
+        channel_id: str,
+        org_channel: Optional[bool] = None,
+        target_team_ids: Optional[Union[str, Sequence[str]]] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Set the workspaces in an Enterprise grid org that connect to a public or private channel.
+        https://docs.slack.dev/reference/methods/admin.conversations.setTeams
+        """
+        kwargs.update(
+            {
+                "channel_id": channel_id,
+                "org_channel": org_channel,
+                "team_id": team_id,
+            }
+        )
+        if isinstance(target_team_ids, (list, tuple)):
+            kwargs.update({"target_team_ids": ",".join(target_team_ids)})
+        else:
+            kwargs.update({"target_team_ids": target_team_ids})
+        return await self.api_call("admin.conversations.setTeams", params=kwargs)
+
+    async def admin_conversations_getTeams(
+        self,
+        *,
+        channel_id: str,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Set the workspaces in an Enterprise grid org that connect to a channel.
+        https://docs.slack.dev/reference/methods/admin.conversations.getTeams
+        """
+        kwargs.update(
+            {
+                "channel_id": channel_id,
+                "cursor": cursor,
+                "limit": limit,
+            }
+        )
+        return await self.api_call("admin.conversations.getTeams", params=kwargs)
+
+    async def admin_conversations_getCustomRetention(
+        self,
+        *,
+        channel_id: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Get a channel's retention policy
+        https://docs.slack.dev/reference/methods/admin.conversations.getCustomRetention
+        """
+        kwargs.update({"channel_id": channel_id})
+        return await self.api_call("admin.conversations.getCustomRetention", params=kwargs)
+
+    async def admin_conversations_removeCustomRetention(
+        self,
+        *,
+        channel_id: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Remove a channel's retention policy
+        https://docs.slack.dev/reference/methods/admin.conversations.removeCustomRetention
+        """
+        kwargs.update({"channel_id": channel_id})
+        return await self.api_call("admin.conversations.removeCustomRetention", params=kwargs)
+
+    async def admin_conversations_setCustomRetention(
+        self,
+        *,
+        channel_id: str,
+        duration_days: int,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Set a channel's retention policy
+        https://docs.slack.dev/reference/methods/admin.conversations.setCustomRetention
+        """
+        kwargs.update({"channel_id": channel_id, "duration_days": duration_days})
+        return await self.api_call("admin.conversations.setCustomRetention", params=kwargs)
+
+    async def admin_conversations_bulkArchive(
+        self,
+        *,
+        channel_ids: Union[Sequence[str], str],
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Archive public or private channels in bulk.
+        https://docs.slack.dev/reference/methods/admin.conversations.bulkArchive
+        """
+        kwargs.update({"channel_ids": ",".join(channel_ids) if isinstance(channel_ids, (list, tuple)) else channel_ids})
+        return await self.api_call("admin.conversations.bulkArchive", params=kwargs)
+
+    async def admin_conversations_bulkDelete(
+        self,
+        *,
+        channel_ids: Union[Sequence[str], str],
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Delete public or private channels in bulk.
+        https://slack.com/api/admin.conversations.bulkDelete
+        """
+        kwargs.update({"channel_ids": ",".join(channel_ids) if isinstance(channel_ids, (list, tuple)) else channel_ids})
+        return await self.api_call("admin.conversations.bulkDelete", params=kwargs)
+
+    async def admin_conversations_bulkMove(
+        self,
+        *,
+        channel_ids: Union[Sequence[str], str],
+        target_team_id: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Move public or private channels in bulk.
+        https://docs.slack.dev/reference/methods/admin.conversations.bulkMove
+        """
+        kwargs.update(
+            {
+                "target_team_id": target_team_id,
+                "channel_ids": ",".join(channel_ids) if isinstance(channel_ids, (list, tuple)) else channel_ids,
+            }
+        )
+        return await self.api_call("admin.conversations.bulkMove", params=kwargs)
+
+    async def admin_emoji_add(
+        self,
+        *,
+        name: str,
+        url: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Add an emoji.
+        https://docs.slack.dev/reference/methods/admin.emoji.add
+        """
+        kwargs.update({"name": name, "url": url})
+        return await self.api_call("admin.emoji.add", http_verb="GET", params=kwargs)
+
+    async def admin_emoji_addAlias(
+        self,
+        *,
+        alias_for: str,
+        name: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Add an emoji alias.
+        https://docs.slack.dev/reference/methods/admin.emoji.addAlias
+        """
+        kwargs.update({"alias_for": alias_for, "name": name})
+        return await self.api_call("admin.emoji.addAlias", http_verb="GET", params=kwargs)
+
+    async def admin_emoji_list(
+        self,
+        *,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """List emoji for an Enterprise Grid organization.
+        https://docs.slack.dev/reference/methods/admin.emoji.list
+        """
+        kwargs.update({"cursor": cursor, "limit": limit})
+        return await self.api_call("admin.emoji.list", http_verb="GET", params=kwargs)
+
+    async def admin_emoji_remove(
+        self,
+        *,
+        name: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Remove an emoji across an Enterprise Grid organization.
+        https://docs.slack.dev/reference/methods/admin.emoji.remove
+        """
+        kwargs.update({"name": name})
+        return await self.api_call("admin.emoji.remove", http_verb="GET", params=kwargs)
+
+    async def admin_emoji_rename(
+        self,
+        *,
+        name: str,
+        new_name: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Rename an emoji.
+        https://docs.slack.dev/reference/methods/admin.emoji.rename
+        """
+        kwargs.update({"name": name, "new_name": new_name})
+        return await self.api_call("admin.emoji.rename", http_verb="GET", params=kwargs)
+
+    async def admin_functions_list(
+        self,
+        *,
+        app_ids: Union[str, Sequence[str]],
+        team_id: Optional[str] = None,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Look up functions by a set of apps
+        https://docs.slack.dev/reference/methods/admin.functions.list
+        """
+        if isinstance(app_ids, (list, tuple)):
+            kwargs.update({"app_ids": ",".join(app_ids)})
+        else:
+            kwargs.update({"app_ids": app_ids})
+        kwargs.update(
+            {
+                "team_id": team_id,
+                "cursor": cursor,
+                "limit": limit,
+            }
+        )
+        return await self.api_call("admin.functions.list", params=kwargs)
+
+    async def admin_functions_permissions_lookup(
+        self,
+        *,
+        function_ids: Union[str, Sequence[str]],
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Lookup the visibility of multiple Slack functions
+        and include the users if it is limited to particular named entities.
+        https://docs.slack.dev/reference/methods/admin.functions.permissions.lookup
+        """
+        if isinstance(function_ids, (list, tuple)):
+            kwargs.update({"function_ids": ",".join(function_ids)})
+        else:
+            kwargs.update({"function_ids": function_ids})
+        return await self.api_call("admin.functions.permissions.lookup", params=kwargs)
+
+    async def admin_functions_permissions_set(
+        self,
+        *,
+        function_id: str,
+        visibility: str,
+        user_ids: Optional[Union[str, Sequence[str]]] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Set the visibility of a Slack function
+        and define the users or workspaces if it is set to named_entities
+        https://docs.slack.dev/reference/methods/admin.functions.permissions.set
+        """
+        kwargs.update(
+            {
+                "function_id": function_id,
+                "visibility": visibility,
+            }
+        )
+        if user_ids is not None:
+            if isinstance(user_ids, (list, tuple)):
+                kwargs.update({"user_ids": ",".join(user_ids)})
+            else:
+                kwargs.update({"user_ids": user_ids})
+        return await self.api_call("admin.functions.permissions.set", params=kwargs)
+
+    async def admin_roles_addAssignments(
+        self,
+        *,
+        role_id: str,
+        entity_ids: Union[str, Sequence[str]],
+        user_ids: Union[str, Sequence[str]],
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Adds members to the specified role with the specified scopes
+        https://docs.slack.dev/reference/methods/admin.roles.addAssignments
+        """
+        kwargs.update({"role_id": role_id})
+        if isinstance(entity_ids, (list, tuple)):
+            kwargs.update({"entity_ids": ",".join(entity_ids)})
+        else:
+            kwargs.update({"entity_ids": entity_ids})
+        if isinstance(user_ids, (list, tuple)):
+            kwargs.update({"user_ids": ",".join(user_ids)})
+        else:
+            kwargs.update({"user_ids": user_ids})
+        return await self.api_call("admin.roles.addAssignments", params=kwargs)
+
+    async def admin_roles_listAssignments(
+        self,
+        *,
+        role_ids: Optional[Union[str, Sequence[str]]] = None,
+        entity_ids: Optional[Union[str, Sequence[str]]] = None,
+        cursor: Optional[str] = None,
+        limit: Optional[Union[str, int]] = None,
+        sort_dir: Optional[str] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Lists assignments for all roles across entities.
+            Options to scope results by any combination of roles or entities
+        https://docs.slack.dev/reference/methods/admin.roles.listAssignments
+        """
+        kwargs.update({"cursor": cursor, "limit": limit, "sort_dir": sort_dir})
+        if isinstance(entity_ids, (list, tuple)):
+            kwargs.update({"entity_ids": ",".join(entity_ids)})
+        else:
+            kwargs.update({"entity_ids": entity_ids})
+        if isinstance(role_ids, (list, tuple)):
+            kwargs.update({"role_ids": ",".join(role_ids)})
+        else:
+            kwargs.update({"role_ids": role_ids})
+        return await self.api_call("admin.roles.listAssignments", params=kwargs)
+
+    async def admin_roles_removeAssignments(
+        self,
+        *,
+        role_id: str,
+        entity_ids: Union[str, Sequence[str]],
+        user_ids: Union[str, Sequence[str]],
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Removes a set of users from a role for the given scopes and entities
+        https://docs.slack.dev/reference/methods/admin.roles.removeAssignments
+        """
+        kwargs.update({"role_id": role_id})
+        if isinstance(entity_ids, (list, tuple)):
+            kwargs.update({"entity_ids": ",".join(entity_ids)})
+        else:
+            kwargs.update({"entity_ids": entity_ids})
+        if isinstance(user_ids, (list, tuple)):
+            kwargs.update({"user_ids": ",".join(user_ids)})
+        else:
+            kwargs.update({"user_ids": user_ids})
+        return await self.api_call("admin.roles.removeAssignments", params=kwargs)
+
+    async def admin_users_session_reset(
+        self,
+        *,
+        user_id: str,
+        mobile_only: Optional[bool] = None,
+        web_only: Optional[bool] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Wipes all valid sessions on all devices for a given user.
+        https://docs.slack.dev/reference/methods/admin.users.session.reset
+        """
+        kwargs.update(
+            {
+                "user_id": user_id,
+                "mobile_only": mobile_only,
+                "web_only": web_only,
+            }
+        )
+        return await self.api_call("admin.users.session.reset", params=kwargs)
+
+    async def admin_users_session_resetBulk(
+        self,
+        *,
+        user_ids: Union[str, Sequence[str]],
+        mobile_only: Optional[bool] = None,
+        web_only: Optional[bool] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Enqueues an asynchronous job to wipe all valid sessions on all devices for a given list of users
+        https://docs.slack.dev/reference/methods/admin.users.session.resetBulk
+        """
+        if isinstance(user_ids, (list, tuple)):
+            kwargs.update({"user_ids": ",".join(user_ids)})
+        else:
+            kwargs.update({"user_ids": user_ids})
+        kwargs.update(
+            {
+                "mobile_only": mobile_only,
+                "web_only": web_only,
+            }
+        )
+        return await self.api_call("admin.users.session.resetBulk", params=kwargs)
+
+    async def admin_users_session_invalidate(
+        self,
+        *,
+        session_id: str,
+        team_id: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Invalidate a single session for a user by session_id.
+        https://docs.slack.dev/reference/methods/admin.users.session.invalidate
+        """
+        kwargs.update({"session_id": session_id, "team_id": team_id})
+        return await self.api_call("admin.users.session.invalidate", params=kwargs)
+
+    async def admin_users_session_list(
+        self,
+        *,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        team_id: Optional[str] = None,
+        user_id: Optional[str] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Lists all active user sessions for an organization
+        https://docs.slack.dev/reference/methods/admin.users.session.list
+        """
+        kwargs.update(
+            {
+                "cursor": cursor,
+                "limit": limit,
+                "team_id": team_id,
+                "user_id": user_id,
+            }
+        )
+        return await self.api_call("admin.users.session.list", params=kwargs)
+
+    async def admin_teams_settings_setDefaultChannels(
+        self,
+        *,
+        team_id: str,
+        channel_ids: Union[str, Sequence[str]],
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Set the default channels of a workspace.
+        https://docs.slack.dev/reference/methods/admin.teams.settings.setDefaultChannels
+        """
+        kwargs.update({"team_id": team_id})
+        if isinstance(channel_ids, (list, tuple)):
+            kwargs.update({"channel_ids": ",".join(channel_ids)})
+        else:
+            kwargs.update({"channel_ids": channel_ids})
+        return await self.api_call("admin.teams.settings.setDefaultChannels", http_verb="GET", params=kwargs)
+
+    async def admin_users_session_getSettings(
+        self,
+        *,
+        user_ids: Union[str, Sequence[str]],
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Get user-specific session settings—the session duration
+        and what happens when the client closes—given a list of users.
+        https://docs.slack.dev/reference/methods/admin.users.session.getSettings
+        """
+        if isinstance(user_ids, (list, tuple)):
+            kwargs.update({"user_ids": ",".join(user_ids)})
+        else:
+            kwargs.update({"user_ids": user_ids})
+        return await self.api_call("admin.users.session.getSettings", params=kwargs)
+
+    async def admin_users_session_setSettings(
+        self,
+        *,
+        user_ids: Union[str, Sequence[str]],
+        desktop_app_browser_quit: Optional[bool] = None,
+        duration: Optional[int] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Configure the user-level session settings—the session duration
+        and what happens when the client closes—for one or more users.
+        https://docs.slack.dev/reference/methods/admin.users.session.setSettings
+        """
+        if isinstance(user_ids, (list, tuple)):
+            kwargs.update({"user_ids": ",".join(user_ids)})
+        else:
+            kwargs.update({"user_ids": user_ids})
+        kwargs.update(
+            {
+                "desktop_app_browser_quit": desktop_app_browser_quit,
+                "duration": duration,
+            }
+        )
+        return await self.api_call("admin.users.session.setSettings", params=kwargs)
+
+    async def admin_users_session_clearSettings(
+        self,
+        *,
+        user_ids: Union[str, Sequence[str]],
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Clear user-specific session settings—the session duration
+        and what happens when the client closes—for a list of users.
+        https://docs.slack.dev/reference/methods/admin.users.session.clearSettings
+        """
+        if isinstance(user_ids, (list, tuple)):
+            kwargs.update({"user_ids": ",".join(user_ids)})
+        else:
+            kwargs.update({"user_ids": user_ids})
+        return await self.api_call("admin.users.session.clearSettings", params=kwargs)
+
+    async def admin_users_unsupportedVersions_export(
+        self,
+        *,
+        date_end_of_support: Optional[Union[str, int]] = None,
+        date_sessions_started: Optional[Union[str, int]] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Ask Slackbot to send you an export listing all workspace members using unsupported software,
+        presented as a zipped CSV file.
+        https://docs.slack.dev/reference/methods/admin.users.unsupportedVersions.export
+        """
+        kwargs.update(
+            {
+                "date_end_of_support": date_end_of_support,
+                "date_sessions_started": date_sessions_started,
+            }
+        )
+        return await self.api_call("admin.users.unsupportedVersions.export", params=kwargs)
+
+    async def admin_inviteRequests_approve(
+        self,
+        *,
+        invite_request_id: str,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Approve a workspace invite request.
+        https://docs.slack.dev/reference/methods/admin.inviteRequests.approve
+        """
+        kwargs.update({"invite_request_id": invite_request_id, "team_id": team_id})
+        return await self.api_call("admin.inviteRequests.approve", params=kwargs)
+
+    async def admin_inviteRequests_approved_list(
+        self,
+        *,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """List all approved workspace invite requests.
+        https://docs.slack.dev/reference/methods/admin.inviteRequests.approved.list
+        """
+        kwargs.update(
+            {
+                "cursor": cursor,
+                "limit": limit,
+                "team_id": team_id,
+            }
+        )
+        return await self.api_call("admin.inviteRequests.approved.list", params=kwargs)
+
+    async def admin_inviteRequests_denied_list(
+        self,
+        *,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """List all denied workspace invite requests.
+        https://docs.slack.dev/reference/methods/admin.inviteRequests.denied.list
+        """
+        kwargs.update(
+            {
+                "cursor": cursor,
+                "limit": limit,
+                "team_id": team_id,
+            }
+        )
+        return await self.api_call("admin.inviteRequests.denied.list", params=kwargs)
+
+    async def admin_inviteRequests_deny(
+        self,
+        *,
+        invite_request_id: str,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Deny a workspace invite request.
+        https://docs.slack.dev/reference/methods/admin.inviteRequests.deny
+        """
+        kwargs.update({"invite_request_id": invite_request_id, "team_id": team_id})
+        return await self.api_call("admin.inviteRequests.deny", params=kwargs)
+
+    async def admin_inviteRequests_list(
+        self,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """List all pending workspace invite requests."""
+        return await self.api_call("admin.inviteRequests.list", params=kwargs)
+
+    async def admin_teams_admins_list(
+        self,
+        *,
+        team_id: str,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """List all of the admins on a given workspace.
+        https://docs.slack.dev/reference/methods/admin.inviteRequests.list
+        """
+        kwargs.update(
+            {
+                "cursor": cursor,
+                "limit": limit,
+                "team_id": team_id,
+            }
+        )
+        return await self.api_call("admin.teams.admins.list", http_verb="GET", params=kwargs)
+
+    async def admin_teams_create(
+        self,
+        *,
+        team_domain: str,
+        team_name: str,
+        team_description: Optional[str] = None,
+        team_discoverability: Optional[str] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Create an Enterprise team.
+        https://docs.slack.dev/reference/methods/admin.teams.create
+        """
+        kwargs.update(
+            {
+                "team_domain": team_domain,
+                "team_name": team_name,
+                "team_description": team_description,
+                "team_discoverability": team_discoverability,
+            }
+        )
+        return await self.api_call("admin.teams.create", params=kwargs)
+
+    async def admin_teams_list(
+        self,
+        *,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """List all teams on an Enterprise organization.
+        https://docs.slack.dev/reference/methods/admin.teams.list
+        """
+        kwargs.update({"cursor": cursor, "limit": limit})
+        return await self.api_call("admin.teams.list", params=kwargs)
+
+    async def admin_teams_owners_list(
+        self,
+        *,
+        team_id: str,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """List all of the admins on a given workspace.
+        https://docs.slack.dev/reference/methods/admin.teams.owners.list
+        """
+        kwargs.update({"team_id": team_id, "cursor": cursor, "limit": limit})
+        return await self.api_call("admin.teams.owners.list", http_verb="GET", params=kwargs)
+
+    async def admin_teams_settings_info(
+        self,
+        *,
+        team_id: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Fetch information about settings in a workspace
+        https://docs.slack.dev/reference/methods/admin.teams.settings.info
+        """
+        kwargs.update({"team_id": team_id})
+        return await self.api_call("admin.teams.settings.info", params=kwargs)
+
+    async def admin_teams_settings_setDescription(
+        self,
+        *,
+        team_id: str,
+        description: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Set the description of a given workspace.
+        https://docs.slack.dev/reference/methods/admin.teams.settings.setDescription
+        """
+        kwargs.update({"team_id": team_id, "description": description})
+        return await self.api_call("admin.teams.settings.setDescription", params=kwargs)
+
+    async def admin_teams_settings_setDiscoverability(
+        self,
+        *,
+        team_id: str,
+        discoverability: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Sets the icon of a workspace.
+        https://docs.slack.dev/reference/methods/admin.teams.settings.setDiscoverability
+        """
+        kwargs.update({"team_id": team_id, "discoverability": discoverability})
+        return await self.api_call("admin.teams.settings.setDiscoverability", params=kwargs)
+
+    async def admin_teams_settings_setIcon(
+        self,
+        *,
+        team_id: str,
+        image_url: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Sets the icon of a workspace.
+        https://docs.slack.dev/reference/methods/admin.teams.settings.setIcon
+        """
+        kwargs.update({"team_id": team_id, "image_url": image_url})
+        return await self.api_call("admin.teams.settings.setIcon", http_verb="GET", params=kwargs)
+
+    async def admin_teams_settings_setName(
+        self,
+        *,
+        team_id: str,
+        name: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Sets the icon of a workspace.
+        https://docs.slack.dev/reference/methods/admin.teams.settings.setName
+        """
+        kwargs.update({"team_id": team_id, "name": name})
+        return await self.api_call("admin.teams.settings.setName", params=kwargs)
+
+    async def admin_usergroups_addChannels(
+        self,
+        *,
+        channel_ids: Union[str, Sequence[str]],
+        usergroup_id: str,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Add one or more default channels to an IDP group.
+        https://docs.slack.dev/reference/methods/admin.usergroups.addChannels
+        """
+        kwargs.update({"team_id": team_id, "usergroup_id": usergroup_id})
+        if isinstance(channel_ids, (list, tuple)):
+            kwargs.update({"channel_ids": ",".join(channel_ids)})
+        else:
+            kwargs.update({"channel_ids": channel_ids})
+        return await self.api_call("admin.usergroups.addChannels", params=kwargs)
+
+    async def admin_usergroups_addTeams(
+        self,
+        *,
+        usergroup_id: str,
+        team_ids: Union[str, Sequence[str]],
+        auto_provision: Optional[bool] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Associate one or more default workspaces with an organization-wide IDP group.
+        https://docs.slack.dev/reference/methods/admin.usergroups.addTeams
+        """
+        kwargs.update({"usergroup_id": usergroup_id, "auto_provision": auto_provision})
+        if isinstance(team_ids, (list, tuple)):
+            kwargs.update({"team_ids": ",".join(team_ids)})
+        else:
+            kwargs.update({"team_ids": team_ids})
+        return await self.api_call("admin.usergroups.addTeams", params=kwargs)
+
+    async def admin_usergroups_listChannels(
+        self,
+        *,
+        usergroup_id: str,
+        include_num_members: Optional[bool] = None,
+        team_id: Optional[bool] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Add one or more default channels to an IDP group.
+        https://docs.slack.dev/reference/methods/admin.usergroups.listChannels
+        """
+        kwargs.update(
+            {
+                "usergroup_id": usergroup_id,
+                "include_num_members": include_num_members,
+                "team_id": team_id,
+            }
+        )
+        return await self.api_call("admin.usergroups.listChannels", params=kwargs)
+
+    async def admin_usergroups_removeChannels(
+        self,
+        *,
+        usergroup_id: str,
+        channel_ids: Union[str, Sequence[str]],
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Add one or more default channels to an IDP group.
+        https://docs.slack.dev/reference/methods/admin.usergroups.removeChannels
+        """
+        kwargs.update({"usergroup_id": usergroup_id})
+        if isinstance(channel_ids, (list, tuple)):
+            kwargs.update({"channel_ids": ",".join(channel_ids)})
+        else:
+            kwargs.update({"channel_ids": channel_ids})
+        return await self.api_call("admin.usergroups.removeChannels", params=kwargs)
+
+    async def admin_users_assign(
+        self,
+        *,
+        team_id: str,
+        user_id: str,
+        channel_ids: Optional[Union[str, Sequence[str]]] = None,
+        is_restricted: Optional[bool] = None,
+        is_ultra_restricted: Optional[bool] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Add an Enterprise user to a workspace.
+        https://docs.slack.dev/reference/methods/admin.users.assign
+        """
+        kwargs.update(
+            {
+                "team_id": team_id,
+                "user_id": user_id,
+                "is_restricted": is_restricted,
+                "is_ultra_restricted": is_ultra_restricted,
+            }
+        )
+        if isinstance(channel_ids, (list, tuple)):
+            kwargs.update({"channel_ids": ",".join(channel_ids)})
+        else:
+            kwargs.update({"channel_ids": channel_ids})
+        return await self.api_call("admin.users.assign", params=kwargs)
+
+    async def admin_users_invite(
+        self,
+        *,
+        team_id: str,
+        email: str,
+        channel_ids: Union[str, Sequence[str]],
+        custom_message: Optional[str] = None,
+        email_password_policy_enabled: Optional[bool] = None,
+        guest_expiration_ts: Optional[Union[str, float]] = None,
+        is_restricted: Optional[bool] = None,
+        is_ultra_restricted: Optional[bool] = None,
+        real_name: Optional[str] = None,
+        resend: Optional[bool] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Invite a user to a workspace.
+        https://docs.slack.dev/reference/methods/admin.users.invite
+        """
+        kwargs.update(
+            {
+                "team_id": team_id,
+                "email": email,
+                "custom_message": custom_message,
+                "email_password_policy_enabled": email_password_policy_enabled,
+                "guest_expiration_ts": str(guest_expiration_ts) if guest_expiration_ts is not None else None,
+                "is_restricted": is_restricted,
+                "is_ultra_restricted": is_ultra_restricted,
+                "real_name": real_name,
+                "resend": resend,
+            }
+        )
+        if isinstance(channel_ids, (list, tuple)):
+            kwargs.update({"channel_ids": ",".join(channel_ids)})
+        else:
+            kwargs.update({"channel_ids": channel_ids})
+        return await self.api_call("admin.users.invite", params=kwargs)
+
+    async def admin_users_list(
+        self,
+        *,
+        team_id: Optional[str] = None,
+        include_deactivated_user_workspaces: Optional[bool] = None,
+        is_active: Optional[bool] = None,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """List users on a workspace
+        https://docs.slack.dev/reference/methods/admin.users.list
+        """
+        kwargs.update(
+            {
+                "team_id": team_id,
+                "include_deactivated_user_workspaces": include_deactivated_user_workspaces,
+                "is_active": is_active,
+                "cursor": cursor,
+                "limit": limit,
+            }
+        )
+        return await self.api_call("admin.users.list", params=kwargs)
+
+    async def admin_users_remove(
+        self,
+        *,
+        team_id: str,
+        user_id: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Remove a user from a workspace.
+        https://docs.slack.dev/reference/methods/admin.users.remove
+        """
+        kwargs.update({"team_id": team_id, "user_id": user_id})
+        return await self.api_call("admin.users.remove", params=kwargs)
+
+    async def admin_users_setAdmin(
+        self,
+        *,
+        team_id: str,
+        user_id: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Set an existing guest, regular user, or owner to be an admin user.
+        https://docs.slack.dev/reference/methods/admin.users.setAdmin
+        """
+        kwargs.update({"team_id": team_id, "user_id": user_id})
+        return await self.api_call("admin.users.setAdmin", params=kwargs)
+
+    async def admin_users_setExpiration(
+        self,
+        *,
+        expiration_ts: int,
+        user_id: str,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Set an expiration for a guest user.
+        https://docs.slack.dev/reference/methods/admin.users.setExpiration
+        """
+        kwargs.update({"expiration_ts": expiration_ts, "team_id": team_id, "user_id": user_id})
+        return await self.api_call("admin.users.setExpiration", params=kwargs)
+
+    async def admin_users_setOwner(
+        self,
+        *,
+        team_id: str,
+        user_id: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Set an existing guest, regular user, or admin user to be a workspace owner.
+        https://docs.slack.dev/reference/methods/admin.users.setOwner
+        """
+        kwargs.update({"team_id": team_id, "user_id": user_id})
+        return await self.api_call("admin.users.setOwner", params=kwargs)
+
+    async def admin_users_setRegular(
+        self,
+        *,
+        team_id: str,
+        user_id: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Set an existing guest user, admin user, or owner to be a regular user.
+        https://docs.slack.dev/reference/methods/admin.users.setRegular
+        """
+        kwargs.update({"team_id": team_id, "user_id": user_id})
+        return await self.api_call("admin.users.setRegular", params=kwargs)
+
+    async def admin_workflows_search(
+        self,
+        *,
+        app_id: Optional[str] = None,
+        collaborator_ids: Optional[Union[str, Sequence[str]]] = None,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        no_collaborators: Optional[bool] = None,
+        num_trigger_ids: Optional[int] = None,
+        query: Optional[str] = None,
+        sort: Optional[str] = None,
+        sort_dir: Optional[str] = None,
+        source: Optional[str] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Search workflows within the team or enterprise
+        https://docs.slack.dev/reference/methods/admin.workflows.search
+        """
+        if collaborator_ids is not None:
+            if isinstance(collaborator_ids, (list, tuple)):
+                kwargs.update({"collaborator_ids": ",".join(collaborator_ids)})
+            else:
+                kwargs.update({"collaborator_ids": collaborator_ids})
+        kwargs.update(
+            {
+                "app_id": app_id,
+                "cursor": cursor,
+                "limit": limit,
+                "no_collaborators": no_collaborators,
+                "num_trigger_ids": num_trigger_ids,
+                "query": query,
+                "sort": sort,
+                "sort_dir": sort_dir,
+                "source": source,
+            }
+        )
+        return await self.api_call("admin.workflows.search", params=kwargs)
+
+    async def admin_workflows_permissions_lookup(
+        self,
+        *,
+        workflow_ids: Union[str, Sequence[str]],
+        max_workflow_triggers: Optional[int] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Look up the permissions for a set of workflows
+        https://docs.slack.dev/reference/methods/admin.workflows.permissions.lookup
+        """
+        if isinstance(workflow_ids, (list, tuple)):
+            kwargs.update({"workflow_ids": ",".join(workflow_ids)})
+        else:
+            kwargs.update({"workflow_ids": workflow_ids})
+        kwargs.update(
+            {
+                "max_workflow_triggers": max_workflow_triggers,
+            }
+        )
+        return await self.api_call("admin.workflows.permissions.lookup", params=kwargs)
+
+    async def admin_workflows_collaborators_add(
+        self,
+        *,
+        collaborator_ids: Union[str, Sequence[str]],
+        workflow_ids: Union[str, Sequence[str]],
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Add collaborators to workflows within the team or enterprise
+        https://docs.slack.dev/reference/methods/admin.workflows.collaborators.add
+        """
+        if isinstance(collaborator_ids, (list, tuple)):
+            kwargs.update({"collaborator_ids": ",".join(collaborator_ids)})
+        else:
+            kwargs.update({"collaborator_ids": collaborator_ids})
+        if isinstance(workflow_ids, (list, tuple)):
+            kwargs.update({"workflow_ids": ",".join(workflow_ids)})
+        else:
+            kwargs.update({"workflow_ids": workflow_ids})
+        return await self.api_call("admin.workflows.collaborators.add", params=kwargs)
+
+    async def admin_workflows_collaborators_remove(
+        self,
+        *,
+        collaborator_ids: Union[str, Sequence[str]],
+        workflow_ids: Union[str, Sequence[str]],
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Remove collaborators from workflows within the team or enterprise
+        https://docs.slack.dev/reference/methods/admin.workflows.collaborators.remove
+        """
+        if isinstance(collaborator_ids, (list, tuple)):
+            kwargs.update({"collaborator_ids": ",".join(collaborator_ids)})
+        else:
+            kwargs.update({"collaborator_ids": collaborator_ids})
+        if isinstance(workflow_ids, (list, tuple)):
+            kwargs.update({"workflow_ids": ",".join(workflow_ids)})
+        else:
+            kwargs.update({"workflow_ids": workflow_ids})
+        return await self.api_call("admin.workflows.collaborators.remove", params=kwargs)
+
+    async def admin_workflows_unpublish(
+        self,
+        *,
+        workflow_ids: Union[str, Sequence[str]],
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Unpublish workflows within the team or enterprise
+        https://docs.slack.dev/reference/methods/admin.workflows.unpublish
+        """
+        if isinstance(workflow_ids, (list, tuple)):
+            kwargs.update({"workflow_ids": ",".join(workflow_ids)})
+        else:
+            kwargs.update({"workflow_ids": workflow_ids})
+        return await self.api_call("admin.workflows.unpublish", params=kwargs)
+
+    async def api_test(
+        self,
+        *,
+        error: Optional[str] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Checks API calling code.
+        https://docs.slack.dev/reference/methods/api.test
+        """
+        kwargs.update({"error": error})
+        return await self.api_call("api.test", params=kwargs)
+
+    async def apps_connections_open(
+        self,
+        *,
+        app_token: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Generate a temporary Socket Mode WebSocket URL that your app can connect to
+        in order to receive events and interactive payloads
+        https://docs.slack.dev/reference/methods/apps.connections.open
+        """
+        kwargs.update({"token": app_token})
+        return await self.api_call("apps.connections.open", http_verb="POST", params=kwargs)
+
+    async def apps_event_authorizations_list(
+        self,
+        *,
+        event_context: str,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Get a list of authorizations for the given event context.
+        Each authorization represents an app installation that the event is visible to.
+        https://docs.slack.dev/reference/methods/apps.event.authorizations.list
+        """
+        kwargs.update({"event_context": event_context, "cursor": cursor, "limit": limit})
+        return await self.api_call("apps.event.authorizations.list", params=kwargs)
+
+    async def apps_uninstall(
+        self,
+        *,
+        client_id: str,
+        client_secret: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Uninstalls your app from a workspace.
+        https://docs.slack.dev/reference/methods/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://docs.slack.dev/reference/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://docs.slack.dev/reference/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://docs.slack.dev/reference/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://docs.slack.dev/reference/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://docs.slack.dev/reference/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://docs.slack.dev/reference/methods/tooling.tokens.rotate
+        """
+        kwargs.update({"refresh_token": refresh_token})
+        return await self.api_call("tooling.tokens.rotate", params=kwargs)
+
+    async def assistant_threads_setStatus(
+        self,
+        *,
+        channel_id: str,
+        thread_ts: str,
+        status: str,
+        loading_messages: Optional[List[str]] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Set the status for an AI assistant thread.
+        https://docs.slack.dev/reference/methods/assistant.threads.setStatus
+        """
+        kwargs.update(
+            {"channel_id": channel_id, "thread_ts": thread_ts, "status": status, "loading_messages": loading_messages}
+        )
+        kwargs = _remove_none_values(kwargs)
+        return await self.api_call("assistant.threads.setStatus", json=kwargs)
+
+    async def assistant_threads_setTitle(
+        self,
+        *,
+        channel_id: str,
+        thread_ts: str,
+        title: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Set the title for the given assistant thread.
+        https://docs.slack.dev/reference/methods/assistant.threads.setTitle
+        """
+        kwargs.update({"channel_id": channel_id, "thread_ts": thread_ts, "title": title})
+        return await self.api_call("assistant.threads.setTitle", params=kwargs)
+
+    async def assistant_threads_setSuggestedPrompts(
+        self,
+        *,
+        channel_id: str,
+        thread_ts: str,
+        title: Optional[str] = None,
+        prompts: List[Dict[str, str]],
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Set suggested prompts for the given assistant thread.
+        https://docs.slack.dev/reference/methods/assistant.threads.setSuggestedPrompts
+        """
+        kwargs.update({"channel_id": channel_id, "thread_ts": thread_ts, "prompts": prompts})
+        if title is not None:
+            kwargs.update({"title": title})
+        return await self.api_call("assistant.threads.setSuggestedPrompts", json=kwargs)
+
+    async def auth_revoke(
+        self,
+        *,
+        test: Optional[bool] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Revokes a token.
+        https://docs.slack.dev/reference/methods/auth.revoke
+        """
+        kwargs.update({"test": test})
+        return await self.api_call("auth.revoke", http_verb="GET", params=kwargs)
+
+    async def auth_test(
+        self,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Checks authentication & identity.
+        https://docs.slack.dev/reference/methods/auth.test
+        """
+        return await self.api_call("auth.test", params=kwargs)
+
+    async def auth_teams_list(
+        self,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        include_icon: Optional[bool] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """List the workspaces a token can access.
+        https://docs.slack.dev/reference/methods/auth.teams.list
+        """
+        kwargs.update({"cursor": cursor, "limit": limit, "include_icon": include_icon})
+        return await self.api_call("auth.teams.list", params=kwargs)
+
+    async def bookmarks_add(
+        self,
+        *,
+        channel_id: str,
+        title: str,
+        type: str,
+        emoji: Optional[str] = None,
+        entity_id: Optional[str] = None,
+        link: Optional[str] = None,  # include when type is 'link'
+        parent_id: Optional[str] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Add bookmark to a channel.
+        https://docs.slack.dev/reference/methods/bookmarks.add
+        """
+        kwargs.update(
+            {
+                "channel_id": channel_id,
+                "title": title,
+                "type": type,
+                "emoji": emoji,
+                "entity_id": entity_id,
+                "link": link,
+                "parent_id": parent_id,
+            }
+        )
+        return await self.api_call("bookmarks.add", http_verb="POST", params=kwargs)
+
+    async def bookmarks_edit(
+        self,
+        *,
+        bookmark_id: str,
+        channel_id: str,
+        emoji: Optional[str] = None,
+        link: Optional[str] = None,
+        title: Optional[str] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Edit bookmark.
+        https://docs.slack.dev/reference/methods/bookmarks.edit
+        """
+        kwargs.update(
+            {
+                "bookmark_id": bookmark_id,
+                "channel_id": channel_id,
+                "emoji": emoji,
+                "link": link,
+                "title": title,
+            }
+        )
+        return await self.api_call("bookmarks.edit", http_verb="POST", params=kwargs)
+
+    async def bookmarks_list(
+        self,
+        *,
+        channel_id: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """List bookmark for the channel.
+        https://docs.slack.dev/reference/methods/bookmarks.list
+        """
+        kwargs.update({"channel_id": channel_id})
+        return await self.api_call("bookmarks.list", http_verb="POST", params=kwargs)
+
+    async def bookmarks_remove(
+        self,
+        *,
+        bookmark_id: str,
+        channel_id: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Remove bookmark from the channel.
+        https://docs.slack.dev/reference/methods/bookmarks.remove
+        """
+        kwargs.update({"bookmark_id": bookmark_id, "channel_id": channel_id})
+        return await self.api_call("bookmarks.remove", http_verb="POST", params=kwargs)
+
+    async def bots_info(
+        self,
+        *,
+        bot: Optional[str] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Gets information about a bot user.
+        https://docs.slack.dev/reference/methods/bots.info
+        """
+        kwargs.update({"bot": bot, "team_id": team_id})
+        return await self.api_call("bots.info", http_verb="GET", params=kwargs)
+
+    async def calls_add(
+        self,
+        *,
+        external_unique_id: str,
+        join_url: str,
+        created_by: Optional[str] = None,
+        date_start: Optional[int] = None,
+        desktop_app_join_url: Optional[str] = None,
+        external_display_id: Optional[str] = None,
+        title: Optional[str] = None,
+        users: Optional[Union[str, Sequence[Dict[str, str]]]] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Registers a new Call.
+        https://docs.slack.dev/reference/methods/calls.add
+        """
+        kwargs.update(
+            {
+                "external_unique_id": external_unique_id,
+                "join_url": join_url,
+                "created_by": created_by,
+                "date_start": date_start,
+                "desktop_app_join_url": desktop_app_join_url,
+                "external_display_id": external_display_id,
+                "title": title,
+            }
+        )
+        _update_call_participants(
+            kwargs,
+            users if users is not None else kwargs.get("users"),  # type: ignore[arg-type]
+        )
+        return await self.api_call("calls.add", http_verb="POST", params=kwargs)
+
+    async def calls_end(
+        self,
+        *,
+        id: str,
+        duration: Optional[int] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Ends a Call.
+        https://docs.slack.dev/reference/methods/calls.end
+        """
+        kwargs.update({"id": id, "duration": duration})
+        return await self.api_call("calls.end", http_verb="POST", params=kwargs)
+
+    async def calls_info(
+        self,
+        *,
+        id: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Returns information about a Call.
+        https://docs.slack.dev/reference/methods/calls.info
+        """
+        kwargs.update({"id": id})
+        return await self.api_call("calls.info", http_verb="POST", params=kwargs)
+
+    async def calls_participants_add(
+        self,
+        *,
+        id: str,
+        users: Union[str, Sequence[Dict[str, str]]],
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Registers new participants added to a Call.
+        https://docs.slack.dev/reference/methods/calls.participants.add
+        """
+        kwargs.update({"id": id})
+        _update_call_participants(kwargs, users)
+        return await self.api_call("calls.participants.add", http_verb="POST", params=kwargs)
+
+    async def calls_participants_remove(
+        self,
+        *,
+        id: str,
+        users: Union[str, Sequence[Dict[str, str]]],
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Registers participants removed from a Call.
+        https://docs.slack.dev/reference/methods/calls.participants.remove
+        """
+        kwargs.update({"id": id})
+        _update_call_participants(kwargs, users)
+        return await self.api_call("calls.participants.remove", http_verb="POST", params=kwargs)
+
+    async def calls_update(
+        self,
+        *,
+        id: str,
+        desktop_app_join_url: Optional[str] = None,
+        join_url: Optional[str] = None,
+        title: Optional[str] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Updates information about a Call.
+        https://docs.slack.dev/reference/methods/calls.update
+        """
+        kwargs.update(
+            {
+                "id": id,
+                "desktop_app_join_url": desktop_app_join_url,
+                "join_url": join_url,
+                "title": title,
+            }
+        )
+        return await self.api_call("calls.update", http_verb="POST", params=kwargs)
+
+    async def canvases_create(
+        self,
+        *,
+        title: Optional[str] = None,
+        document_content: Dict[str, str],
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Create Canvas for a user
+        https://docs.slack.dev/reference/methods/canvases.create
+        """
+        kwargs.update({"title": title, "document_content": document_content})
+        return await self.api_call("canvases.create", json=kwargs)
+
+    async def canvases_edit(
+        self,
+        *,
+        canvas_id: str,
+        changes: Sequence[Dict[str, Any]],
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Update an existing canvas
+        https://docs.slack.dev/reference/methods/canvases.edit
+        """
+        kwargs.update({"canvas_id": canvas_id, "changes": changes})
+        return await self.api_call("canvases.edit", json=kwargs)
+
+    async def canvases_delete(
+        self,
+        *,
+        canvas_id: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Deletes a canvas
+        https://docs.slack.dev/reference/methods/canvases.delete
+        """
+        kwargs.update({"canvas_id": canvas_id})
+        return await self.api_call("canvases.delete", params=kwargs)
+
+    async def canvases_access_set(
+        self,
+        *,
+        canvas_id: str,
+        access_level: str,
+        channel_ids: Optional[Union[Sequence[str], str]] = None,
+        user_ids: Optional[Union[Sequence[str], str]] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Sets the access level to a canvas for specified entities
+        https://docs.slack.dev/reference/methods/canvases.access.set
+        """
+        kwargs.update({"canvas_id": canvas_id, "access_level": access_level})
+        if channel_ids is not None:
+            if isinstance(channel_ids, (list, tuple)):
+                kwargs.update({"channel_ids": ",".join(channel_ids)})
+            else:
+                kwargs.update({"channel_ids": channel_ids})
+        if user_ids is not None:
+            if isinstance(user_ids, (list, tuple)):
+                kwargs.update({"user_ids": ",".join(user_ids)})
+            else:
+                kwargs.update({"user_ids": user_ids})
+
+        return await self.api_call("canvases.access.set", params=kwargs)
+
+    async def canvases_access_delete(
+        self,
+        *,
+        canvas_id: str,
+        channel_ids: Optional[Union[Sequence[str], str]] = None,
+        user_ids: Optional[Union[Sequence[str], str]] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Create a Channel Canvas for a channel
+        https://docs.slack.dev/reference/methods/canvases.access.delete
+        """
+        kwargs.update({"canvas_id": canvas_id})
+        if channel_ids is not None:
+            if isinstance(channel_ids, (list, tuple)):
+                kwargs.update({"channel_ids": ",".join(channel_ids)})
+            else:
+                kwargs.update({"channel_ids": channel_ids})
+        if user_ids is not None:
+            if isinstance(user_ids, (list, tuple)):
+                kwargs.update({"user_ids": ",".join(user_ids)})
+            else:
+                kwargs.update({"user_ids": user_ids})
+        return await self.api_call("canvases.access.delete", params=kwargs)
+
+    async def canvases_sections_lookup(
+        self,
+        *,
+        canvas_id: str,
+        criteria: Dict[str, Any],
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Find sections matching the provided criteria
+        https://docs.slack.dev/reference/methods/canvases.sections.lookup
+        """
+        kwargs.update({"canvas_id": canvas_id, "criteria": json.dumps(criteria)})
+        return await self.api_call("canvases.sections.lookup", params=kwargs)
+
+    # --------------------------
+    # Deprecated: channels.*
+    # You can use conversations.* APIs instead.
+    # https://docs.slack.dev/changelog/2020-01-deprecating-antecedents-to-the-conversations-api/
+    # --------------------------
+
+    async def channels_archive(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Archives a channel."""
+        kwargs.update({"channel": channel})
+        kwargs = _remove_none_values(kwargs)
+        return await self.api_call("channels.archive", json=kwargs)
+
+    async def channels_create(
+        self,
+        *,
+        name: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Creates a channel."""
+        kwargs.update({"name": name})
+        kwargs = _remove_none_values(kwargs)
+        return await self.api_call("channels.create", json=kwargs)
+
+    async def channels_history(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Fetches history of messages and events from a channel."""
+        kwargs.update({"channel": channel})
+        return await self.api_call("channels.history", http_verb="GET", params=kwargs)
+
+    async def channels_info(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Gets information about a channel."""
+        kwargs.update({"channel": channel})
+        return await self.api_call("channels.info", http_verb="GET", params=kwargs)
+
+    async def channels_invite(
+        self,
+        *,
+        channel: str,
+        user: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Invites a user to a channel."""
+        kwargs.update({"channel": channel, "user": user})
+        kwargs = _remove_none_values(kwargs)
+        return await self.api_call("channels.invite", json=kwargs)
+
+    async def channels_join(
+        self,
+        *,
+        name: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Joins a channel, creating it if needed."""
+        kwargs.update({"name": name})
+        kwargs = _remove_none_values(kwargs)
+        return await self.api_call("channels.join", json=kwargs)
+
+    async def channels_kick(
+        self,
+        *,
+        channel: str,
+        user: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Removes a user from a channel."""
+        kwargs.update({"channel": channel, "user": user})
+        kwargs = _remove_none_values(kwargs)
+        return await self.api_call("channels.kick", json=kwargs)
+
+    async def channels_leave(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Leaves a channel."""
+        kwargs.update({"channel": channel})
+        kwargs = _remove_none_values(kwargs)
+        return await self.api_call("channels.leave", json=kwargs)
+
+    async def channels_list(
+        self,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Lists all channels in a Slack team."""
+        return await self.api_call("channels.list", http_verb="GET", params=kwargs)
+
+    async def channels_mark(
+        self,
+        *,
+        channel: str,
+        ts: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Sets the read cursor in a channel."""
+        kwargs.update({"channel": channel, "ts": ts})
+        kwargs = _remove_none_values(kwargs)
+        return await self.api_call("channels.mark", json=kwargs)
+
+    async def channels_rename(
+        self,
+        *,
+        channel: str,
+        name: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Renames a channel."""
+        kwargs.update({"channel": channel, "name": name})
+        kwargs = _remove_none_values(kwargs)
+        return await self.api_call("channels.rename", json=kwargs)
+
+    async def channels_replies(
+        self,
+        *,
+        channel: str,
+        thread_ts: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Retrieve a thread of messages posted to a channel"""
+        kwargs.update({"channel": channel, "thread_ts": thread_ts})
+        return await self.api_call("channels.replies", http_verb="GET", params=kwargs)
+
+    async def channels_setPurpose(
+        self,
+        *,
+        channel: str,
+        purpose: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Sets the purpose for a channel."""
+        kwargs.update({"channel": channel, "purpose": purpose})
+        kwargs = _remove_none_values(kwargs)
+        return await self.api_call("channels.setPurpose", json=kwargs)
+
+    async def channels_setTopic(
+        self,
+        *,
+        channel: str,
+        topic: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Sets the topic for a channel."""
+        kwargs.update({"channel": channel, "topic": topic})
+        kwargs = _remove_none_values(kwargs)
+        return await self.api_call("channels.setTopic", json=kwargs)
+
+    async def channels_unarchive(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Unarchives a channel."""
+        kwargs.update({"channel": channel})
+        kwargs = _remove_none_values(kwargs)
+        return await self.api_call("channels.unarchive", json=kwargs)
+
+    # --------------------------
+
+    async def chat_appendStream(
+        self,
+        *,
+        channel: str,
+        ts: str,
+        markdown_text: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Appends text to an existing streaming conversation.
+        https://docs.slack.dev/reference/methods/chat.appendStream
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "ts": ts,
+                "markdown_text": markdown_text,
+            }
+        )
+        kwargs = _remove_none_values(kwargs)
+        return await self.api_call("chat.appendStream", json=kwargs)
+
+    async def chat_delete(
+        self,
+        *,
+        channel: str,
+        ts: str,
+        as_user: Optional[bool] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Deletes a message.
+        https://docs.slack.dev/reference/methods/chat.delete
+        """
+        kwargs.update({"channel": channel, "ts": ts, "as_user": as_user})
+        return await self.api_call("chat.delete", params=kwargs)
+
+    async def chat_deleteScheduledMessage(
+        self,
+        *,
+        channel: str,
+        scheduled_message_id: str,
+        as_user: Optional[bool] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Deletes a scheduled message.
+        https://docs.slack.dev/reference/methods/chat.deleteScheduledMessage
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "scheduled_message_id": scheduled_message_id,
+                "as_user": as_user,
+            }
+        )
+        return await self.api_call("chat.deleteScheduledMessage", params=kwargs)
+
+    async def chat_getPermalink(
+        self,
+        *,
+        channel: str,
+        message_ts: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Retrieve a permalink URL for a specific extant message
+        https://docs.slack.dev/reference/methods/chat.getPermalink
+        """
+        kwargs.update({"channel": channel, "message_ts": message_ts})
+        return await self.api_call("chat.getPermalink", http_verb="GET", params=kwargs)
+
+    async def chat_meMessage(
+        self,
+        *,
+        channel: str,
+        text: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Share a me message into a channel.
+        https://docs.slack.dev/reference/methods/chat.meMessage
+        """
+        kwargs.update({"channel": channel, "text": text})
+        return await self.api_call("chat.meMessage", params=kwargs)
+
+    async def chat_postEphemeral(
+        self,
+        *,
+        channel: str,
+        user: str,
+        text: Optional[str] = None,
+        as_user: Optional[bool] = None,
+        attachments: Optional[Union[str, Sequence[Union[Dict, Attachment]]]] = None,
+        blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None,
+        thread_ts: Optional[str] = None,
+        icon_emoji: Optional[str] = None,
+        icon_url: Optional[str] = None,
+        link_names: Optional[bool] = None,
+        username: Optional[str] = None,
+        parse: Optional[str] = None,
+        markdown_text: Optional[str] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Sends an ephemeral message to a user in a channel.
+        https://docs.slack.dev/reference/methods/chat.postEphemeral
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "user": user,
+                "text": text,
+                "as_user": as_user,
+                "attachments": attachments,
+                "blocks": blocks,
+                "thread_ts": thread_ts,
+                "icon_emoji": icon_emoji,
+                "icon_url": icon_url,
+                "link_names": link_names,
+                "username": username,
+                "parse": parse,
+                "markdown_text": markdown_text,
+            }
+        )
+        _parse_web_class_objects(kwargs)
+        kwargs = _remove_none_values(kwargs)
+        _warn_if_message_text_content_is_missing("chat.postEphemeral", kwargs)
+        # NOTE: intentionally using json over params for the API methods using blocks/attachments
+        return await self.api_call("chat.postEphemeral", json=kwargs)
+
+    async def chat_postMessage(
+        self,
+        *,
+        channel: str,
+        text: Optional[str] = None,
+        as_user: Optional[bool] = None,
+        attachments: Optional[Union[str, Sequence[Union[Dict, Attachment]]]] = None,
+        blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None,
+        thread_ts: Optional[str] = None,
+        reply_broadcast: Optional[bool] = None,
+        unfurl_links: Optional[bool] = None,
+        unfurl_media: Optional[bool] = None,
+        container_id: Optional[str] = None,
+        icon_emoji: Optional[str] = None,
+        icon_url: Optional[str] = None,
+        mrkdwn: Optional[bool] = None,
+        link_names: Optional[bool] = None,
+        username: Optional[str] = None,
+        parse: Optional[str] = None,  # none, full
+        metadata: Optional[Union[Dict, Metadata, EventAndEntityMetadata]] = None,
+        markdown_text: Optional[str] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Sends a message to a channel.
+        https://docs.slack.dev/reference/methods/chat.postMessage
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "text": text,
+                "as_user": as_user,
+                "attachments": attachments,
+                "blocks": blocks,
+                "thread_ts": thread_ts,
+                "reply_broadcast": reply_broadcast,
+                "unfurl_links": unfurl_links,
+                "unfurl_media": unfurl_media,
+                "container_id": container_id,
+                "icon_emoji": icon_emoji,
+                "icon_url": icon_url,
+                "mrkdwn": mrkdwn,
+                "link_names": link_names,
+                "username": username,
+                "parse": parse,
+                "metadata": metadata,
+                "markdown_text": markdown_text,
+            }
+        )
+        _parse_web_class_objects(kwargs)
+        kwargs = _remove_none_values(kwargs)
+        _warn_if_message_text_content_is_missing("chat.postMessage", kwargs)
+        # NOTE: intentionally using json over params for the API methods using blocks/attachments
+        return await self.api_call("chat.postMessage", json=kwargs)
+
+    async def chat_scheduleMessage(
+        self,
+        *,
+        channel: str,
+        post_at: Union[str, int],
+        text: Optional[str] = None,
+        as_user: Optional[bool] = None,
+        attachments: Optional[Union[str, Sequence[Union[Dict, Attachment]]]] = None,
+        blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None,
+        thread_ts: Optional[str] = None,
+        parse: Optional[str] = None,
+        reply_broadcast: Optional[bool] = None,
+        unfurl_links: Optional[bool] = None,
+        unfurl_media: Optional[bool] = None,
+        link_names: Optional[bool] = None,
+        metadata: Optional[Union[Dict, Metadata]] = None,
+        markdown_text: Optional[str] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Schedules a message.
+        https://docs.slack.dev/reference/methods/chat.scheduleMessage
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "post_at": post_at,
+                "text": text,
+                "as_user": as_user,
+                "attachments": attachments,
+                "blocks": blocks,
+                "thread_ts": thread_ts,
+                "reply_broadcast": reply_broadcast,
+                "parse": parse,
+                "unfurl_links": unfurl_links,
+                "unfurl_media": unfurl_media,
+                "link_names": link_names,
+                "metadata": metadata,
+                "markdown_text": markdown_text,
+            }
+        )
+        _parse_web_class_objects(kwargs)
+        kwargs = _remove_none_values(kwargs)
+        _warn_if_message_text_content_is_missing("chat.scheduleMessage", kwargs)
+        # NOTE: intentionally using json over params for the API methods using blocks/attachments
+        return await self.api_call("chat.scheduleMessage", json=kwargs)
+
+    async def chat_scheduledMessages_list(
+        self,
+        *,
+        channel: Optional[str] = None,
+        cursor: Optional[str] = None,
+        latest: Optional[str] = None,
+        limit: Optional[int] = None,
+        oldest: Optional[str] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Lists all scheduled messages.
+        https://docs.slack.dev/reference/methods/chat.scheduledMessages.list
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "cursor": cursor,
+                "latest": latest,
+                "limit": limit,
+                "oldest": oldest,
+                "team_id": team_id,
+            }
+        )
+        return await self.api_call("chat.scheduledMessages.list", params=kwargs)
+
+    async def chat_startStream(
+        self,
+        *,
+        channel: str,
+        thread_ts: str,
+        markdown_text: Optional[str] = None,
+        recipient_team_id: Optional[str] = None,
+        recipient_user_id: Optional[str] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Starts a new streaming conversation.
+        https://docs.slack.dev/reference/methods/chat.startStream
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "thread_ts": thread_ts,
+                "markdown_text": markdown_text,
+                "recipient_team_id": recipient_team_id,
+                "recipient_user_id": recipient_user_id,
+            }
+        )
+        kwargs = _remove_none_values(kwargs)
+        return await self.api_call("chat.startStream", json=kwargs)
+
+    async def chat_stopStream(
+        self,
+        *,
+        channel: str,
+        ts: str,
+        markdown_text: Optional[str] = None,
+        blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None,
+        metadata: Optional[Union[Dict, Metadata]] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Stops a streaming conversation.
+        https://docs.slack.dev/reference/methods/chat.stopStream
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "ts": ts,
+                "markdown_text": markdown_text,
+                "blocks": blocks,
+                "metadata": metadata,
+            }
+        )
+        _parse_web_class_objects(kwargs)
+        kwargs = _remove_none_values(kwargs)
+        return await self.api_call("chat.stopStream", json=kwargs)
+
+    async def chat_stream(
+        self,
+        *,
+        buffer_size: int = 256,
+        channel: str,
+        thread_ts: str,
+        recipient_team_id: Optional[str] = None,
+        recipient_user_id: Optional[str] = None,
+        **kwargs,
+    ) -> AsyncChatStream:
+        """Stream markdown text into a conversation.
+
+        This method starts a new chat stream in a conversation that can be appended to. After appending an entire message,
+        the stream can be stopped with concluding arguments such as "blocks" for gathering feedback.
+
+        The following methods are used:
+
+        - chat.startStream: Starts a new streaming conversation.
+          [Reference](https://docs.slack.dev/reference/methods/chat.startStream).
+        - chat.appendStream: Appends text to an existing streaming conversation.
+          [Reference](https://docs.slack.dev/reference/methods/chat.appendStream).
+        - chat.stopStream: Stops a streaming conversation.
+          [Reference](https://docs.slack.dev/reference/methods/chat.stopStream).
+
+        Args:
+            buffer_size: The length of markdown_text to buffer in-memory before calling a stream method. Increasing this
+              value decreases the number of method calls made for the same amount of text, which is useful to avoid rate
+              limits. Default: 256.
+            channel: An encoded ID that represents a channel, private group, or DM.
+            thread_ts: Provide another message's ts value to reply to. Streamed messages should always be replies to a user
+              request.
+            recipient_team_id: The encoded ID of the team the user receiving the streaming text belongs to. Required when
+              streaming to channels.
+            recipient_user_id: The encoded ID of the user to receive the streaming text. Required when streaming to channels.
+            **kwargs: Additional arguments passed to the underlying API calls.
+
+        Returns:
+            ChatStream instance for managing the stream
+
+        Example:
+            ```python
+            streamer = await client.chat_stream(
+                channel="C0123456789",
+                thread_ts="1700000001.123456",
+                recipient_team_id="T0123456789",
+                recipient_user_id="U0123456789",
+            )
+            await streamer.append(markdown_text="**hello wo")
+            await streamer.append(markdown_text="rld!**")
+            await streamer.stop()
+            ```
+        """
+        return AsyncChatStream(
+            self,
+            logger=self._logger,
+            channel=channel,
+            thread_ts=thread_ts,
+            recipient_team_id=recipient_team_id,
+            recipient_user_id=recipient_user_id,
+            buffer_size=buffer_size,
+            **kwargs,
+        )
+
+    async def chat_unfurl(
+        self,
+        *,
+        channel: Optional[str] = None,
+        ts: Optional[str] = None,
+        source: Optional[str] = None,
+        unfurl_id: Optional[str] = None,
+        unfurls: Optional[Dict[str, Dict]] = None,  # or user_auth_*
+        metadata: Optional[Union[Dict, EventAndEntityMetadata]] = None,
+        user_auth_blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None,
+        user_auth_message: Optional[str] = None,
+        user_auth_required: Optional[bool] = None,
+        user_auth_url: Optional[str] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Provide custom unfurl behavior for user-posted URLs.
+        https://docs.slack.dev/reference/methods/chat.unfurl
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "ts": ts,
+                "source": source,
+                "unfurl_id": unfurl_id,
+                "unfurls": unfurls,
+                "metadata": metadata,
+                "user_auth_blocks": user_auth_blocks,
+                "user_auth_message": user_auth_message,
+                "user_auth_required": user_auth_required,
+                "user_auth_url": user_auth_url,
+            }
+        )
+        _parse_web_class_objects(kwargs)  # for user_auth_blocks
+        kwargs = _remove_none_values(kwargs)
+        # NOTE: intentionally using json over params for API methods using blocks/attachments
+        return await self.api_call("chat.unfurl", json=kwargs)
+
+    async def chat_update(
+        self,
+        *,
+        channel: str,
+        ts: str,
+        text: Optional[str] = None,
+        attachments: Optional[Union[str, Sequence[Union[Dict, Attachment]]]] = None,
+        blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None,
+        as_user: Optional[bool] = None,
+        file_ids: Optional[Union[str, Sequence[str]]] = None,
+        link_names: Optional[bool] = None,
+        parse: Optional[str] = None,  # none, full
+        reply_broadcast: Optional[bool] = None,
+        metadata: Optional[Union[Dict, Metadata]] = None,
+        markdown_text: Optional[str] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Updates a message in a channel.
+        https://docs.slack.dev/reference/methods/chat.update
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "ts": ts,
+                "text": text,
+                "attachments": attachments,
+                "blocks": blocks,
+                "as_user": as_user,
+                "link_names": link_names,
+                "parse": parse,
+                "reply_broadcast": reply_broadcast,
+                "metadata": metadata,
+                "markdown_text": markdown_text,
+            }
+        )
+        if isinstance(file_ids, (list, tuple)):
+            kwargs.update({"file_ids": ",".join(file_ids)})
+        else:
+            kwargs.update({"file_ids": file_ids})
+        _parse_web_class_objects(kwargs)
+        kwargs = _remove_none_values(kwargs)
+        _warn_if_message_text_content_is_missing("chat.update", kwargs)
+        # NOTE: intentionally using json over params for API methods using blocks/attachments
+        return await self.api_call("chat.update", json=kwargs)
+
+    async def conversations_acceptSharedInvite(
+        self,
+        *,
+        channel_name: str,
+        channel_id: Optional[str] = None,
+        invite_id: Optional[str] = None,
+        free_trial_accepted: Optional[bool] = None,
+        is_private: Optional[bool] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Accepts an invitation to a Slack Connect channel.
+        https://docs.slack.dev/reference/methods/conversations.acceptSharedInvite
+        """
+        if channel_id is None and invite_id is None:
+            raise e.SlackRequestError("Either channel_id or invite_id must be provided.")
+        kwargs.update(
+            {
+                "channel_name": channel_name,
+                "channel_id": channel_id,
+                "invite_id": invite_id,
+                "free_trial_accepted": free_trial_accepted,
+                "is_private": is_private,
+                "team_id": team_id,
+            }
+        )
+        return await self.api_call("conversations.acceptSharedInvite", http_verb="POST", params=kwargs)
+
+    async def conversations_approveSharedInvite(
+        self,
+        *,
+        invite_id: str,
+        target_team: Optional[str] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Approves an invitation to a Slack Connect channel.
+        https://docs.slack.dev/reference/methods/conversations.approveSharedInvite
+        """
+        kwargs.update({"invite_id": invite_id, "target_team": target_team})
+        return await self.api_call("conversations.approveSharedInvite", http_verb="POST", params=kwargs)
+
+    async def conversations_archive(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Archives a conversation.
+        https://docs.slack.dev/reference/methods/conversations.archive
+        """
+        kwargs.update({"channel": channel})
+        return await self.api_call("conversations.archive", params=kwargs)
+
+    async def conversations_close(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Closes a direct message or multi-person direct message.
+        https://docs.slack.dev/reference/methods/conversations.close
+        """
+        kwargs.update({"channel": channel})
+        return await self.api_call("conversations.close", params=kwargs)
+
+    async def conversations_create(
+        self,
+        *,
+        name: str,
+        is_private: Optional[bool] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Initiates a public or private channel-based conversation
+        https://docs.slack.dev/reference/methods/conversations.create
+        """
+        kwargs.update({"name": name, "is_private": is_private, "team_id": team_id})
+        return await self.api_call("conversations.create", params=kwargs)
+
+    async def conversations_declineSharedInvite(
+        self,
+        *,
+        invite_id: str,
+        target_team: Optional[str] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Declines a Slack Connect channel invite.
+        https://docs.slack.dev/reference/methods/conversations.declineSharedInvite
+        """
+        kwargs.update({"invite_id": invite_id, "target_team": target_team})
+        return await self.api_call("conversations.declineSharedInvite", http_verb="GET", params=kwargs)
+
+    async def conversations_externalInvitePermissions_set(
+        self, *, action: str, channel: str, target_team: str, **kwargs
+    ) -> AsyncSlackResponse:
+        """Sets a team in a shared External Limited channel to a shared Slack Connect channel or vice versa.
+        https://docs.slack.dev/reference/methods/conversations.externalInvitePermissions.set
+        """
+        kwargs.update(
+            {
+                "action": action,
+                "channel": channel,
+                "target_team": target_team,
+            }
+        )
+        return await self.api_call("conversations.externalInvitePermissions.set", params=kwargs)
+
+    async def conversations_history(
+        self,
+        *,
+        channel: str,
+        cursor: Optional[str] = None,
+        inclusive: Optional[bool] = None,
+        include_all_metadata: Optional[bool] = None,
+        latest: Optional[str] = None,
+        limit: Optional[int] = None,
+        oldest: Optional[str] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Fetches a conversation's history of messages and events.
+        https://docs.slack.dev/reference/methods/conversations.history
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "cursor": cursor,
+                "inclusive": inclusive,
+                "include_all_metadata": include_all_metadata,
+                "limit": limit,
+                "latest": latest,
+                "oldest": oldest,
+            }
+        )
+        return await self.api_call("conversations.history", http_verb="GET", params=kwargs)
+
+    async def conversations_info(
+        self,
+        *,
+        channel: str,
+        include_locale: Optional[bool] = None,
+        include_num_members: Optional[bool] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Retrieve information about a conversation.
+        https://docs.slack.dev/reference/methods/conversations.info
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "include_locale": include_locale,
+                "include_num_members": include_num_members,
+            }
+        )
+        return await self.api_call("conversations.info", http_verb="GET", params=kwargs)
+
+    async def conversations_invite(
+        self,
+        *,
+        channel: str,
+        users: Union[str, Sequence[str]],
+        force: Optional[bool] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Invites users to a channel.
+        https://docs.slack.dev/reference/methods/conversations.invite
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "force": force,
+            }
+        )
+        if isinstance(users, (list, tuple)):
+            kwargs.update({"users": ",".join(users)})
+        else:
+            kwargs.update({"users": users})
+        return await self.api_call("conversations.invite", params=kwargs)
+
+    async def conversations_inviteShared(
+        self,
+        *,
+        channel: str,
+        emails: Optional[Union[str, Sequence[str]]] = None,
+        user_ids: Optional[Union[str, Sequence[str]]] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Sends an invitation to a Slack Connect channel.
+        https://docs.slack.dev/reference/methods/conversations.inviteShared
+        """
+        if emails is None and user_ids is None:
+            raise e.SlackRequestError("Either emails or user ids must be provided.")
+        kwargs.update({"channel": channel})
+        if isinstance(emails, (list, tuple)):
+            kwargs.update({"emails": ",".join(emails)})
+        else:
+            kwargs.update({"emails": emails})
+        if isinstance(user_ids, (list, tuple)):
+            kwargs.update({"user_ids": ",".join(user_ids)})
+        else:
+            kwargs.update({"user_ids": user_ids})
+        return await self.api_call("conversations.inviteShared", http_verb="GET", params=kwargs)
+
+    async def conversations_join(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Joins an existing conversation.
+        https://docs.slack.dev/reference/methods/conversations.join
+        """
+        kwargs.update({"channel": channel})
+        return await self.api_call("conversations.join", params=kwargs)
+
+    async def conversations_kick(
+        self,
+        *,
+        channel: str,
+        user: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Removes a user from a conversation.
+        https://docs.slack.dev/reference/methods/conversations.kick
+        """
+        kwargs.update({"channel": channel, "user": user})
+        return await self.api_call("conversations.kick", params=kwargs)
+
+    async def conversations_leave(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Leaves a conversation.
+        https://docs.slack.dev/reference/methods/conversations.leave
+        """
+        kwargs.update({"channel": channel})
+        return await self.api_call("conversations.leave", params=kwargs)
+
+    async def conversations_list(
+        self,
+        *,
+        cursor: Optional[str] = None,
+        exclude_archived: Optional[bool] = None,
+        limit: Optional[int] = None,
+        team_id: Optional[str] = None,
+        types: Optional[Union[str, Sequence[str]]] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Lists all channels in a Slack team.
+        https://docs.slack.dev/reference/methods/conversations.list
+        """
+        kwargs.update(
+            {
+                "cursor": cursor,
+                "exclude_archived": exclude_archived,
+                "limit": limit,
+                "team_id": team_id,
+            }
+        )
+        if isinstance(types, (list, tuple)):
+            kwargs.update({"types": ",".join(types)})
+        else:
+            kwargs.update({"types": types})
+        return await self.api_call("conversations.list", http_verb="GET", params=kwargs)
+
+    async def conversations_listConnectInvites(
+        self,
+        *,
+        count: Optional[int] = None,
+        cursor: Optional[str] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """List shared channel invites that have been generated
+        or received but have not yet been approved by all parties.
+        https://docs.slack.dev/reference/methods/conversations.listConnectInvites
+        """
+        kwargs.update({"count": count, "cursor": cursor, "team_id": team_id})
+        return await self.api_call("conversations.listConnectInvites", params=kwargs)
+
+    async def conversations_mark(
+        self,
+        *,
+        channel: str,
+        ts: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Sets the read cursor in a channel.
+        https://docs.slack.dev/reference/methods/conversations.mark
+        """
+        kwargs.update({"channel": channel, "ts": ts})
+        return await self.api_call("conversations.mark", params=kwargs)
+
+    async def conversations_members(
+        self,
+        *,
+        channel: str,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Retrieve members of a conversation.
+        https://docs.slack.dev/reference/methods/conversations.members
+        """
+        kwargs.update({"channel": channel, "cursor": cursor, "limit": limit})
+        return await self.api_call("conversations.members", http_verb="GET", params=kwargs)
+
+    async def conversations_open(
+        self,
+        *,
+        channel: Optional[str] = None,
+        return_im: Optional[bool] = None,
+        users: Optional[Union[str, Sequence[str]]] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Opens or resumes a direct message or multi-person direct message.
+        https://docs.slack.dev/reference/methods/conversations.open
+        """
+        if channel is None and users is None:
+            raise e.SlackRequestError("Either channel or users must be provided.")
+        kwargs.update({"channel": channel, "return_im": return_im})
+        if isinstance(users, (list, tuple)):
+            kwargs.update({"users": ",".join(users)})
+        else:
+            kwargs.update({"users": users})
+        return await self.api_call("conversations.open", params=kwargs)
+
+    async def conversations_rename(
+        self,
+        *,
+        channel: str,
+        name: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Renames a conversation.
+        https://docs.slack.dev/reference/methods/conversations.rename
+        """
+        kwargs.update({"channel": channel, "name": name})
+        return await self.api_call("conversations.rename", params=kwargs)
+
+    async def conversations_replies(
+        self,
+        *,
+        channel: str,
+        ts: str,
+        cursor: Optional[str] = None,
+        inclusive: Optional[bool] = None,
+        include_all_metadata: Optional[bool] = None,
+        latest: Optional[str] = None,
+        limit: Optional[int] = None,
+        oldest: Optional[str] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Retrieve a thread of messages posted to a conversation
+        https://docs.slack.dev/reference/methods/conversations.replies
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "ts": ts,
+                "cursor": cursor,
+                "inclusive": inclusive,
+                "include_all_metadata": include_all_metadata,
+                "limit": limit,
+                "latest": latest,
+                "oldest": oldest,
+            }
+        )
+        return await self.api_call("conversations.replies", http_verb="GET", params=kwargs)
+
+    async def conversations_requestSharedInvite_approve(
+        self,
+        *,
+        invite_id: str,
+        channel_id: Optional[str] = None,
+        is_external_limited: Optional[str] = None,
+        message: Optional[Dict[str, Any]] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Approve a request to add an external user to a channel. This also sends them a Slack Connect invite.
+        https://docs.slack.dev/reference/methods/conversations.requestSharedInvite.approve
+        """
+        kwargs.update(
+            {
+                "invite_id": invite_id,
+                "channel_id": channel_id,
+                "is_external_limited": is_external_limited,
+            }
+        )
+        if message is not None:
+            kwargs.update({"message": json.dumps(message)})
+        return await self.api_call("conversations.requestSharedInvite.approve", params=kwargs)
+
+    async def conversations_requestSharedInvite_deny(
+        self,
+        *,
+        invite_id: str,
+        message: Optional[str] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Deny a request to invite an external user to a channel.
+        https://docs.slack.dev/reference/methods/conversations.requestSharedInvite.deny
+        """
+        kwargs.update({"invite_id": invite_id, "message": message})
+        return await self.api_call("conversations.requestSharedInvite.deny", params=kwargs)
+
+    async def conversations_requestSharedInvite_list(
+        self,
+        *,
+        cursor: Optional[str] = None,
+        include_approved: Optional[bool] = None,
+        include_denied: Optional[bool] = None,
+        include_expired: Optional[bool] = None,
+        invite_ids: Optional[Union[str, Sequence[str]]] = None,
+        limit: Optional[int] = None,
+        user_id: Optional[str] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Lists requests to add external users to channels with ability to filter.
+        https://docs.slack.dev/reference/methods/conversations.requestSharedInvite.list
+        """
+        kwargs.update(
+            {
+                "cursor": cursor,
+                "include_approved": include_approved,
+                "include_denied": include_denied,
+                "include_expired": include_expired,
+                "limit": limit,
+                "user_id": user_id,
+            }
+        )
+        if invite_ids is not None:
+            if isinstance(invite_ids, (list, tuple)):
+                kwargs.update({"invite_ids": ",".join(invite_ids)})
+            else:
+                kwargs.update({"invite_ids": invite_ids})
+        return await self.api_call("conversations.requestSharedInvite.list", params=kwargs)
+
+    async def conversations_setPurpose(
+        self,
+        *,
+        channel: str,
+        purpose: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Sets the purpose for a conversation.
+        https://docs.slack.dev/reference/methods/conversations.setPurpose
+        """
+        kwargs.update({"channel": channel, "purpose": purpose})
+        return await self.api_call("conversations.setPurpose", params=kwargs)
+
+    async def conversations_setTopic(
+        self,
+        *,
+        channel: str,
+        topic: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Sets the topic for a conversation.
+        https://docs.slack.dev/reference/methods/conversations.setTopic
+        """
+        kwargs.update({"channel": channel, "topic": topic})
+        return await self.api_call("conversations.setTopic", params=kwargs)
+
+    async def conversations_unarchive(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Reverses conversation archival.
+        https://docs.slack.dev/reference/methods/conversations.unarchive
+        """
+        kwargs.update({"channel": channel})
+        return await self.api_call("conversations.unarchive", params=kwargs)
+
+    async def conversations_canvases_create(
+        self,
+        *,
+        channel_id: str,
+        document_content: Dict[str, str],
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Create a Channel Canvas for a channel
+        https://docs.slack.dev/reference/methods/conversations.canvases.create
+        """
+        kwargs.update({"channel_id": channel_id, "document_content": document_content})
+        return await self.api_call("conversations.canvases.create", json=kwargs)
+
+    async def dialog_open(
+        self,
+        *,
+        dialog: Dict[str, Any],
+        trigger_id: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Open a dialog with a user.
+        https://docs.slack.dev/reference/methods/dialog.open
+        """
+        kwargs.update({"dialog": dialog, "trigger_id": trigger_id})
+        kwargs = _remove_none_values(kwargs)
+        # NOTE: As the dialog can be a dict, this API call works only with json format.
+        return await self.api_call("dialog.open", json=kwargs)
+
+    async def dnd_endDnd(
+        self,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Ends the current user's Do Not Disturb session immediately.
+        https://docs.slack.dev/reference/methods/dnd.endDnd
+        """
+        return await self.api_call("dnd.endDnd", params=kwargs)
+
+    async def dnd_endSnooze(
+        self,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Ends the current user's snooze mode immediately.
+        https://docs.slack.dev/reference/methods/dnd.endSnooze
+        """
+        return await self.api_call("dnd.endSnooze", params=kwargs)
+
+    async def dnd_info(
+        self,
+        *,
+        team_id: Optional[str] = None,
+        user: Optional[str] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Retrieves a user's current Do Not Disturb status.
+        https://docs.slack.dev/reference/methods/dnd.info
+        """
+        kwargs.update({"team_id": team_id, "user": user})
+        return await self.api_call("dnd.info", http_verb="GET", params=kwargs)
+
+    async def dnd_setSnooze(
+        self,
+        *,
+        num_minutes: Union[int, str],
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Turns on Do Not Disturb mode for the current user, or changes its duration.
+        https://docs.slack.dev/reference/methods/dnd.setSnooze
+        """
+        kwargs.update({"num_minutes": num_minutes})
+        return await self.api_call("dnd.setSnooze", http_verb="GET", params=kwargs)
+
+    async def dnd_teamInfo(
+        self,
+        users: Union[str, Sequence[str]],
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Retrieves the Do Not Disturb status for users on a team.
+        https://docs.slack.dev/reference/methods/dnd.teamInfo
+        """
+        if isinstance(users, (list, tuple)):
+            kwargs.update({"users": ",".join(users)})
+        else:
+            kwargs.update({"users": users})
+        kwargs.update({"team_id": team_id})
+        return await self.api_call("dnd.teamInfo", http_verb="GET", params=kwargs)
+
+    async def emoji_list(
+        self,
+        include_categories: Optional[bool] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Lists custom emoji for a team.
+        https://docs.slack.dev/reference/methods/emoji.list
+        """
+        kwargs.update({"include_categories": include_categories})
+        return await self.api_call("emoji.list", http_verb="GET", params=kwargs)
+
+    async def entity_presentDetails(
+        self,
+        trigger_id: str,
+        metadata: Optional[Union[Dict, EntityMetadata]] = None,
+        user_auth_required: Optional[bool] = None,
+        user_auth_url: Optional[str] = None,
+        error: Optional[Dict[str, Any]] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Provides entity details for the flexpane.
+        https://docs.slack.dev/reference/methods/entity.presentDetails/
+        """
+        kwargs.update({"trigger_id": trigger_id})
+        if metadata is not None:
+            kwargs.update({"metadata": metadata})
+        if user_auth_required is not None:
+            kwargs.update({"user_auth_required": user_auth_required})
+        if user_auth_url is not None:
+            kwargs.update({"user_auth_url": user_auth_url})
+        if error is not None:
+            kwargs.update({"error": error})
+        _parse_web_class_objects(kwargs)
+        return await self.api_call("entity.presentDetails", json=kwargs)
+
+    async def files_comments_delete(
+        self,
+        *,
+        file: str,
+        id: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Deletes an existing comment on a file.
+        https://docs.slack.dev/reference/methods/files.comments.delete
+        """
+        kwargs.update({"file": file, "id": id})
+        return await self.api_call("files.comments.delete", params=kwargs)
+
+    async def files_delete(
+        self,
+        *,
+        file: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Deletes a file.
+        https://docs.slack.dev/reference/methods/files.delete
+        """
+        kwargs.update({"file": file})
+        return await self.api_call("files.delete", params=kwargs)
+
+    async def files_info(
+        self,
+        *,
+        file: str,
+        count: Optional[int] = None,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        page: Optional[int] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Gets information about a team file.
+        https://docs.slack.dev/reference/methods/files.info
+        """
+        kwargs.update(
+            {
+                "file": file,
+                "count": count,
+                "cursor": cursor,
+                "limit": limit,
+                "page": page,
+            }
+        )
+        return await self.api_call("files.info", http_verb="GET", params=kwargs)
+
+    async def files_list(
+        self,
+        *,
+        channel: Optional[str] = None,
+        count: Optional[int] = None,
+        page: Optional[int] = None,
+        show_files_hidden_by_limit: Optional[bool] = None,
+        team_id: Optional[str] = None,
+        ts_from: Optional[str] = None,
+        ts_to: Optional[str] = None,
+        types: Optional[Union[str, Sequence[str]]] = None,
+        user: Optional[str] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Lists & filters team files.
+        https://docs.slack.dev/reference/methods/files.list
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "count": count,
+                "page": page,
+                "show_files_hidden_by_limit": show_files_hidden_by_limit,
+                "team_id": team_id,
+                "ts_from": ts_from,
+                "ts_to": ts_to,
+                "user": user,
+            }
+        )
+        if isinstance(types, (list, tuple)):
+            kwargs.update({"types": ",".join(types)})
+        else:
+            kwargs.update({"types": types})
+        return await self.api_call("files.list", http_verb="GET", params=kwargs)
+
+    async def files_remote_info(
+        self,
+        *,
+        external_id: Optional[str] = None,
+        file: Optional[str] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Retrieve information about a remote file added to Slack.
+        https://docs.slack.dev/reference/methods/files.remote.info
+        """
+        kwargs.update({"external_id": external_id, "file": file})
+        return await self.api_call("files.remote.info", http_verb="GET", params=kwargs)
+
+    async def files_remote_list(
+        self,
+        *,
+        channel: Optional[str] = None,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        ts_from: Optional[str] = None,
+        ts_to: Optional[str] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Retrieve information about a remote file added to Slack.
+        https://docs.slack.dev/reference/methods/files.remote.list
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "cursor": cursor,
+                "limit": limit,
+                "ts_from": ts_from,
+                "ts_to": ts_to,
+            }
+        )
+        return await self.api_call("files.remote.list", http_verb="GET", params=kwargs)
+
+    async def files_remote_add(
+        self,
+        *,
+        external_id: str,
+        external_url: str,
+        title: str,
+        filetype: Optional[str] = None,
+        indexable_file_contents: Optional[Union[str, bytes, IOBase]] = None,
+        preview_image: Optional[Union[str, bytes, IOBase]] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Adds a file from a remote service.
+        https://docs.slack.dev/reference/methods/files.remote.add
+        """
+        kwargs.update(
+            {
+                "external_id": external_id,
+                "external_url": external_url,
+                "title": title,
+                "filetype": filetype,
+            }
+        )
+        files = None
+        # preview_image (file): Preview of the document via multipart/form-data.
+        if preview_image is not None or indexable_file_contents is not None:
+            files = {
+                "preview_image": preview_image,
+                "indexable_file_contents": indexable_file_contents,
+            }
+
+        return await self.api_call(
+            # Intentionally using "POST" method over "GET" here
+            "files.remote.add",
+            http_verb="POST",
+            data=kwargs,
+            files=files,
+        )
+
+    async def files_remote_update(
+        self,
+        *,
+        external_id: Optional[str] = None,
+        external_url: Optional[str] = None,
+        file: Optional[str] = None,
+        title: Optional[str] = None,
+        filetype: Optional[str] = None,
+        indexable_file_contents: Optional[str] = None,
+        preview_image: Optional[str] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Updates an existing remote file.
+        https://docs.slack.dev/reference/methods/files.remote.update
+        """
+        kwargs.update(
+            {
+                "external_id": external_id,
+                "external_url": external_url,
+                "file": file,
+                "title": title,
+                "filetype": filetype,
+            }
+        )
+        files = None
+        # preview_image (file): Preview of the document via multipart/form-data.
+        if preview_image is not None or indexable_file_contents is not None:
+            files = {
+                "preview_image": preview_image,
+                "indexable_file_contents": indexable_file_contents,
+            }
+
+        return await self.api_call(
+            # Intentionally using "POST" method over "GET" here
+            "files.remote.update",
+            http_verb="POST",
+            data=kwargs,
+            files=files,
+        )
+
+    async def files_remote_remove(
+        self,
+        *,
+        external_id: Optional[str] = None,
+        file: Optional[str] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Remove a remote file.
+        https://docs.slack.dev/reference/methods/files.remote.remove
+        """
+        kwargs.update({"external_id": external_id, "file": file})
+        return await self.api_call("files.remote.remove", http_verb="POST", params=kwargs)
+
+    async def files_remote_share(
+        self,
+        *,
+        channels: Union[str, Sequence[str]],
+        external_id: Optional[str] = None,
+        file: Optional[str] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Share a remote file into a channel.
+        https://docs.slack.dev/reference/methods/files.remote.share
+        """
+        if external_id is None and file is None:
+            raise e.SlackRequestError("Either external_id or file must be provided.")
+        if isinstance(channels, (list, tuple)):
+            kwargs.update({"channels": ",".join(channels)})
+        else:
+            kwargs.update({"channels": channels})
+        kwargs.update({"external_id": external_id, "file": file})
+        return await self.api_call("files.remote.share", http_verb="GET", params=kwargs)
+
+    async def files_revokePublicURL(
+        self,
+        *,
+        file: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Revokes public/external sharing access for a file
+        https://docs.slack.dev/reference/methods/files.revokePublicURL
+        """
+        kwargs.update({"file": file})
+        return await self.api_call("files.revokePublicURL", params=kwargs)
+
+    async def files_sharedPublicURL(
+        self,
+        *,
+        file: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Enables a file for public/external sharing.
+        https://docs.slack.dev/reference/methods/files.sharedPublicURL
+        """
+        kwargs.update({"file": file})
+        return await self.api_call("files.sharedPublicURL", params=kwargs)
+
+    async def files_upload(
+        self,
+        *,
+        file: Optional[Union[str, bytes, IOBase]] = None,
+        content: Optional[Union[str, bytes]] = None,
+        filename: Optional[str] = None,
+        filetype: Optional[str] = None,
+        initial_comment: Optional[str] = None,
+        thread_ts: Optional[str] = None,
+        title: Optional[str] = None,
+        channels: Optional[Union[str, Sequence[str]]] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Uploads or creates a file.
+        https://docs.slack.dev/reference/methods/files.upload
+        """
+        _print_files_upload_v2_suggestion()
+
+        if file is None and content is None:
+            raise e.SlackRequestError("The file or content argument must be specified.")
+        if file is not None and content is not None:
+            raise e.SlackRequestError("You cannot specify both the file and the content argument.")
+
+        if isinstance(channels, (list, tuple)):
+            kwargs.update({"channels": ",".join(channels)})
+        else:
+            kwargs.update({"channels": channels})
+        kwargs.update(
+            {
+                "filename": filename,
+                "filetype": filetype,
+                "initial_comment": initial_comment,
+                "thread_ts": thread_ts,
+                "title": title,
+            }
+        )
+        if file:
+            if kwargs.get("filename") is None and isinstance(file, str):
+                # use the local filename if filename is missing
+                if kwargs.get("filename") is None:
+                    kwargs["filename"] = file.split(os.path.sep)[-1]
+            return await self.api_call("files.upload", files={"file": file}, data=kwargs)
+        else:
+            kwargs["content"] = content
+            return await self.api_call("files.upload", data=kwargs)
+
+    async def files_upload_v2(
+        self,
+        *,
+        # for sending a single file
+        filename: Optional[str] = None,  # you can skip this only when sending along with content parameter
+        file: Optional[Union[str, bytes, IOBase, os.PathLike]] = None,
+        content: Optional[Union[str, bytes]] = None,
+        title: Optional[str] = None,
+        alt_txt: Optional[str] = None,
+        snippet_type: Optional[str] = None,
+        # To upload multiple files at a time
+        file_uploads: Optional[List[Dict[str, Any]]] = None,
+        channel: Optional[str] = None,
+        channels: Optional[List[str]] = None,
+        initial_comment: Optional[str] = None,
+        thread_ts: Optional[str] = None,
+        request_file_info: bool = True,  # since v3.23, this flag is no longer necessary
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """This wrapper method provides an easy way to upload files using the following endpoints:
+
+        - step1: https://docs.slack.dev/reference/methods/files.getUploadURLExternal
+
+        - step2: "https://files.slack.com/upload/v1/..." URLs returned from files.getUploadURLExternal API
+
+        - step3: https://docs.slack.dev/reference/methods/files.completeUploadExternal
+            and https://docs.slack.dev/reference/methods/files.info
+
+        """
+        if file is None and content is None and file_uploads is None:
+            raise e.SlackRequestError("Any of file, content, and file_uploads must be specified.")
+        if file is not None and content is not None:
+            raise e.SlackRequestError("You cannot specify both the file and the content argument.")
+
+        # deprecated arguments:
+        filetype = kwargs.get("filetype")
+
+        if filetype is not None:
+            warnings.warn("The filetype parameter is no longer supported. Please remove it from the arguments.")
+
+        # step1: files.getUploadURLExternal per file
+        files: List[Dict[str, Any]] = []
+        if file_uploads is not None:
+            for f in file_uploads:
+                files.append(_to_v2_file_upload_item(f))
+        else:
+            f = _to_v2_file_upload_item(
+                {
+                    "filename": filename,
+                    "file": file,
+                    "content": content,
+                    "title": title,
+                    "alt_txt": alt_txt,
+                    "snippet_type": snippet_type,
+                }
+            )
+            files.append(f)
+
+        for f in files:
+            url_response = await self.files_getUploadURLExternal(
+                filename=f.get("filename"),  # type: ignore[arg-type]
+                length=f.get("length"),  # type: ignore[arg-type]
+                alt_txt=f.get("alt_txt"),
+                snippet_type=f.get("snippet_type"),
+                token=kwargs.get("token"),
+            )
+            _validate_for_legacy_client(url_response)
+            f["file_id"] = url_response.get("file_id")  # type: ignore[union-attr, unused-ignore]
+            f["upload_url"] = url_response.get("upload_url")  # type: ignore[union-attr, unused-ignore]
+
+        # step2: "https://files.slack.com/upload/v1/..." per file
+        for f in files:
+            upload_result = await self._upload_file(
+                url=f["upload_url"],
+                data=f["data"],
+                logger=self._logger,
+                timeout=self.timeout,
+                proxy=self.proxy,
+                ssl=self.ssl,
+            )
+            if upload_result.status != 200:
+                status = upload_result.status
+                body = upload_result.body
+                message = (
+                    "Failed to upload a file "
+                    f"(status: {status}, body: {body}, filename: {f.get('filename')}, title: {f.get('title')})"
+                )
+                raise e.SlackRequestError(message)
+
+        # step3: files.completeUploadExternal with all the sets of (file_id + title)
+        completion = await self.files_completeUploadExternal(
+            files=[{"id": f["file_id"], "title": f["title"]} for f in files],
+            channel_id=channel,
+            channels=channels,
+            initial_comment=initial_comment,
+            thread_ts=thread_ts,
+            **kwargs,
+        )
+        if len(completion.get("files")) == 1:  # type: ignore[arg-type, union-attr, unused-ignore]
+            completion.data["file"] = completion.get("files")[0]  # type: ignore[index, union-attr, unused-ignore]
+        return completion
+
+    async def files_getUploadURLExternal(
+        self,
+        *,
+        filename: str,
+        length: int,
+        alt_txt: Optional[str] = None,
+        snippet_type: Optional[str] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Gets a URL for an edge external upload.
+        https://docs.slack.dev/reference/methods/files.getUploadURLExternal
+        """
+        kwargs.update(
+            {
+                "filename": filename,
+                "length": length,
+                "alt_txt": alt_txt,
+                "snippet_type": snippet_type,
+            }
+        )
+        return await self.api_call("files.getUploadURLExternal", params=kwargs)
+
+    async def files_completeUploadExternal(
+        self,
+        *,
+        files: List[Dict[str, str]],
+        channel_id: Optional[str] = None,
+        channels: Optional[List[str]] = None,
+        initial_comment: Optional[str] = None,
+        thread_ts: Optional[str] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Finishes an upload started with files.getUploadURLExternal.
+        https://docs.slack.dev/reference/methods/files.completeUploadExternal
+        """
+        _files = [{k: v for k, v in f.items() if v is not None} for f in files]
+        kwargs.update(
+            {
+                "files": json.dumps(_files),
+                "channel_id": channel_id,
+                "initial_comment": initial_comment,
+                "thread_ts": thread_ts,
+            }
+        )
+        if channels:
+            kwargs["channels"] = ",".join(channels)
+        return await self.api_call("files.completeUploadExternal", params=kwargs)
+
+    async def functions_completeSuccess(
+        self,
+        *,
+        function_execution_id: str,
+        outputs: Dict[str, Any],
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Signal the successful completion of a function
+        https://docs.slack.dev/reference/methods/functions.completeSuccess
+        """
+        kwargs.update({"function_execution_id": function_execution_id, "outputs": json.dumps(outputs)})
+        return await self.api_call("functions.completeSuccess", params=kwargs)
+
+    async def functions_completeError(
+        self,
+        *,
+        function_execution_id: str,
+        error: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Signal the failure to execute a function
+        https://docs.slack.dev/reference/methods/functions.completeError
+        """
+        kwargs.update({"function_execution_id": function_execution_id, "error": error})
+        return await self.api_call("functions.completeError", params=kwargs)
+
+    # --------------------------
+    # Deprecated: groups.*
+    # You can use conversations.* APIs instead.
+    # https://docs.slack.dev/changelog/2020-01-deprecating-antecedents-to-the-conversations-api/
+    # --------------------------
+
+    async def groups_archive(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Archives a private channel."""
+        kwargs.update({"channel": channel})
+        kwargs = _remove_none_values(kwargs)
+        return await self.api_call("groups.archive", json=kwargs)
+
+    async def groups_create(
+        self,
+        *,
+        name: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Creates a private channel."""
+        kwargs.update({"name": name})
+        kwargs = _remove_none_values(kwargs)
+        return await self.api_call("groups.create", json=kwargs)
+
+    async def groups_createChild(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Clones and archives a private channel."""
+        kwargs.update({"channel": channel})
+        return await self.api_call("groups.createChild", http_verb="GET", params=kwargs)
+
+    async def groups_history(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Fetches history of messages and events from a private channel."""
+        kwargs.update({"channel": channel})
+        return await self.api_call("groups.history", http_verb="GET", params=kwargs)
+
+    async def groups_info(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Gets information about a private channel."""
+        kwargs.update({"channel": channel})
+        return await self.api_call("groups.info", http_verb="GET", params=kwargs)
+
+    async def groups_invite(
+        self,
+        *,
+        channel: str,
+        user: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Invites a user to a private channel."""
+        kwargs.update({"channel": channel, "user": user})
+        kwargs = _remove_none_values(kwargs)
+        return await self.api_call("groups.invite", json=kwargs)
+
+    async def groups_kick(
+        self,
+        *,
+        channel: str,
+        user: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Removes a user from a private channel."""
+        kwargs.update({"channel": channel, "user": user})
+        kwargs = _remove_none_values(kwargs)
+        return await self.api_call("groups.kick", json=kwargs)
+
+    async def groups_leave(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Leaves a private channel."""
+        kwargs.update({"channel": channel})
+        kwargs = _remove_none_values(kwargs)
+        return await self.api_call("groups.leave", json=kwargs)
+
+    async def groups_list(
+        self,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Lists private channels that the calling user has access to."""
+        return await self.api_call("groups.list", http_verb="GET", params=kwargs)
+
+    async def groups_mark(
+        self,
+        *,
+        channel: str,
+        ts: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Sets the read cursor in a private channel."""
+        kwargs.update({"channel": channel, "ts": ts})
+        kwargs = _remove_none_values(kwargs)
+        return await self.api_call("groups.mark", json=kwargs)
+
+    async def groups_open(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Opens a private channel."""
+        kwargs.update({"channel": channel})
+        kwargs = _remove_none_values(kwargs)
+        return await self.api_call("groups.open", json=kwargs)
+
+    async def groups_rename(
+        self,
+        *,
+        channel: str,
+        name: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Renames a private channel."""
+        kwargs.update({"channel": channel, "name": name})
+        kwargs = _remove_none_values(kwargs)
+        return await self.api_call("groups.rename", json=kwargs)
+
+    async def groups_replies(
+        self,
+        *,
+        channel: str,
+        thread_ts: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Retrieve a thread of messages posted to a private channel"""
+        kwargs.update({"channel": channel, "thread_ts": thread_ts})
+        return await self.api_call("groups.replies", http_verb="GET", params=kwargs)
+
+    async def groups_setPurpose(
+        self,
+        *,
+        channel: str,
+        purpose: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Sets the purpose for a private channel."""
+        kwargs.update({"channel": channel, "purpose": purpose})
+        kwargs = _remove_none_values(kwargs)
+        return await self.api_call("groups.setPurpose", json=kwargs)
+
+    async def groups_setTopic(
+        self,
+        *,
+        channel: str,
+        topic: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Sets the topic for a private channel."""
+        kwargs.update({"channel": channel, "topic": topic})
+        kwargs = _remove_none_values(kwargs)
+        return await self.api_call("groups.setTopic", json=kwargs)
+
+    async def groups_unarchive(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Unarchives a private channel."""
+        kwargs.update({"channel": channel})
+        kwargs = _remove_none_values(kwargs)
+        return await self.api_call("groups.unarchive", json=kwargs)
+
+    # --------------------------
+    # Deprecated: im.*
+    # You can use conversations.* APIs instead.
+    # https://docs.slack.dev/changelog/2020-01-deprecating-antecedents-to-the-conversations-api/
+    # --------------------------
+
+    async def im_close(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Close a direct message channel."""
+        kwargs.update({"channel": channel})
+        kwargs = _remove_none_values(kwargs)
+        return await self.api_call("im.close", json=kwargs)
+
+    async def im_history(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Fetches history of messages and events from direct message channel."""
+        kwargs.update({"channel": channel})
+        return await self.api_call("im.history", http_verb="GET", params=kwargs)
+
+    async def im_list(
+        self,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Lists direct message channels for the calling user."""
+        return await self.api_call("im.list", http_verb="GET", params=kwargs)
+
+    async def im_mark(
+        self,
+        *,
+        channel: str,
+        ts: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Sets the read cursor in a direct message channel."""
+        kwargs.update({"channel": channel, "ts": ts})
+        kwargs = _remove_none_values(kwargs)
+        return await self.api_call("im.mark", json=kwargs)
+
+    async def im_open(
+        self,
+        *,
+        user: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Opens a direct message channel."""
+        kwargs.update({"user": user})
+        kwargs = _remove_none_values(kwargs)
+        return await self.api_call("im.open", json=kwargs)
+
+    async def im_replies(
+        self,
+        *,
+        channel: str,
+        thread_ts: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Retrieve a thread of messages posted to a direct message conversation"""
+        kwargs.update({"channel": channel, "thread_ts": thread_ts})
+        return await self.api_call("im.replies", http_verb="GET", params=kwargs)
+
+    # --------------------------
+
+    async def migration_exchange(
+        self,
+        *,
+        users: Union[str, Sequence[str]],
+        team_id: Optional[str] = None,
+        to_old: Optional[bool] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """For Enterprise Grid workspaces, map local user IDs to global user IDs
+        https://docs.slack.dev/reference/methods/migration.exchange
+        """
+        if isinstance(users, (list, tuple)):
+            kwargs.update({"users": ",".join(users)})
+        else:
+            kwargs.update({"users": users})
+        kwargs.update({"team_id": team_id, "to_old": to_old})
+        return await self.api_call("migration.exchange", http_verb="GET", params=kwargs)
+
+    # --------------------------
+    # Deprecated: mpim.*
+    # You can use conversations.* APIs instead.
+    # https://docs.slack.dev/changelog/2020-01-deprecating-antecedents-to-the-conversations-api/
+    # --------------------------
+
+    async def mpim_close(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Closes a multiparty direct message channel."""
+        kwargs.update({"channel": channel})
+        kwargs = _remove_none_values(kwargs)
+        return await self.api_call("mpim.close", json=kwargs)
+
+    async def mpim_history(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Fetches history of messages and events from a multiparty direct message."""
+        kwargs.update({"channel": channel})
+        return await self.api_call("mpim.history", http_verb="GET", params=kwargs)
+
+    async def mpim_list(
+        self,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Lists multiparty direct message channels for the calling user."""
+        return await self.api_call("mpim.list", http_verb="GET", params=kwargs)
+
+    async def mpim_mark(
+        self,
+        *,
+        channel: str,
+        ts: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Sets the read cursor in a multiparty direct message channel."""
+        kwargs.update({"channel": channel, "ts": ts})
+        kwargs = _remove_none_values(kwargs)
+        return await self.api_call("mpim.mark", json=kwargs)
+
+    async def mpim_open(
+        self,
+        *,
+        users: Union[str, Sequence[str]],
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """This method opens a multiparty direct message."""
+        if isinstance(users, (list, tuple)):
+            kwargs.update({"users": ",".join(users)})
+        else:
+            kwargs.update({"users": users})
+        return await self.api_call("mpim.open", params=kwargs)
+
+    async def mpim_replies(
+        self,
+        *,
+        channel: str,
+        thread_ts: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Retrieve a thread of messages posted to a direct message conversation from a
+        multiparty direct message.
+        """
+        kwargs.update({"channel": channel, "thread_ts": thread_ts})
+        return await self.api_call("mpim.replies", http_verb="GET", params=kwargs)
+
+    # --------------------------
+
+    async def oauth_v2_access(
+        self,
+        *,
+        client_id: str,
+        client_secret: str,
+        # This field is required when processing the OAuth redirect URL requests
+        # while it's absent for token rotation
+        code: Optional[str] = None,
+        redirect_uri: Optional[str] = None,
+        # This field is required for token rotation
+        grant_type: Optional[str] = None,
+        # This field is required for token rotation
+        refresh_token: Optional[str] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Exchanges a temporary OAuth verifier code for an access token.
+        https://docs.slack.dev/reference/methods/oauth.v2.access
+        """
+        if redirect_uri is not None:
+            kwargs.update({"redirect_uri": redirect_uri})
+        if code is not None:
+            kwargs.update({"code": code})
+        if grant_type is not None:
+            kwargs.update({"grant_type": grant_type})
+        if refresh_token is not None:
+            kwargs.update({"refresh_token": refresh_token})
+        return await self.api_call(
+            "oauth.v2.access",
+            data=kwargs,
+            auth={"client_id": client_id, "client_secret": client_secret},
+        )
+
+    async def oauth_access(
+        self,
+        *,
+        client_id: str,
+        client_secret: str,
+        code: str,
+        redirect_uri: Optional[str] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Exchanges a temporary OAuth verifier code for an access token.
+        https://docs.slack.dev/reference/methods/oauth.access
+        """
+        if redirect_uri is not None:
+            kwargs.update({"redirect_uri": redirect_uri})
+        kwargs.update({"code": code})
+        return await self.api_call(
+            "oauth.access",
+            data=kwargs,
+            auth={"client_id": client_id, "client_secret": client_secret},
+        )
+
+    async def oauth_v2_exchange(
+        self,
+        *,
+        token: str,
+        client_id: str,
+        client_secret: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Exchanges a legacy access token for a new expiring access token and refresh token
+        https://docs.slack.dev/reference/methods/oauth.v2.exchange
+        """
+        kwargs.update({"client_id": client_id, "client_secret": client_secret, "token": token})
+        return await self.api_call("oauth.v2.exchange", params=kwargs)
+
+    async def openid_connect_token(
+        self,
+        client_id: str,
+        client_secret: str,
+        code: Optional[str] = None,
+        redirect_uri: Optional[str] = None,
+        grant_type: Optional[str] = None,
+        refresh_token: Optional[str] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Exchanges a temporary OAuth verifier code for an access token for Sign in with Slack.
+        https://docs.slack.dev/reference/methods/openid.connect.token
+        """
+        if redirect_uri is not None:
+            kwargs.update({"redirect_uri": redirect_uri})
+        if code is not None:
+            kwargs.update({"code": code})
+        if grant_type is not None:
+            kwargs.update({"grant_type": grant_type})
+        if refresh_token is not None:
+            kwargs.update({"refresh_token": refresh_token})
+        return await self.api_call(
+            "openid.connect.token",
+            data=kwargs,
+            auth={"client_id": client_id, "client_secret": client_secret},
+        )
+
+    async def openid_connect_userInfo(
+        self,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Get the identity of a user who has authorized Sign in with Slack.
+        https://docs.slack.dev/reference/methods/openid.connect.userInfo
+        """
+        return await self.api_call("openid.connect.userInfo", params=kwargs)
+
+    async def pins_add(
+        self,
+        *,
+        channel: str,
+        timestamp: Optional[str] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Pins an item to a channel.
+        https://docs.slack.dev/reference/methods/pins.add
+        """
+        kwargs.update({"channel": channel, "timestamp": timestamp})
+        return await self.api_call("pins.add", params=kwargs)
+
+    async def pins_list(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Lists items pinned to a channel.
+        https://docs.slack.dev/reference/methods/pins.list
+        """
+        kwargs.update({"channel": channel})
+        return await self.api_call("pins.list", http_verb="GET", params=kwargs)
+
+    async def pins_remove(
+        self,
+        *,
+        channel: str,
+        timestamp: Optional[str] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Un-pins an item from a channel.
+        https://docs.slack.dev/reference/methods/pins.remove
+        """
+        kwargs.update({"channel": channel, "timestamp": timestamp})
+        return await self.api_call("pins.remove", params=kwargs)
+
+    async def reactions_add(
+        self,
+        *,
+        channel: str,
+        name: str,
+        timestamp: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Adds a reaction to an item.
+        https://docs.slack.dev/reference/methods/reactions.add
+        """
+        kwargs.update({"channel": channel, "name": name, "timestamp": timestamp})
+        return await self.api_call("reactions.add", params=kwargs)
+
+    async def reactions_get(
+        self,
+        *,
+        channel: Optional[str] = None,
+        file: Optional[str] = None,
+        file_comment: Optional[str] = None,
+        full: Optional[bool] = None,
+        timestamp: Optional[str] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Gets reactions for an item.
+        https://docs.slack.dev/reference/methods/reactions.get
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "file": file,
+                "file_comment": file_comment,
+                "full": full,
+                "timestamp": timestamp,
+            }
+        )
+        return await self.api_call("reactions.get", http_verb="GET", params=kwargs)
+
+    async def reactions_list(
+        self,
+        *,
+        count: Optional[int] = None,
+        cursor: Optional[str] = None,
+        full: Optional[bool] = None,
+        limit: Optional[int] = None,
+        page: Optional[int] = None,
+        team_id: Optional[str] = None,
+        user: Optional[str] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Lists reactions made by a user.
+        https://docs.slack.dev/reference/methods/reactions.list
+        """
+        kwargs.update(
+            {
+                "count": count,
+                "cursor": cursor,
+                "full": full,
+                "limit": limit,
+                "page": page,
+                "team_id": team_id,
+                "user": user,
+            }
+        )
+        return await self.api_call("reactions.list", http_verb="GET", params=kwargs)
+
+    async def reactions_remove(
+        self,
+        *,
+        name: str,
+        channel: Optional[str] = None,
+        file: Optional[str] = None,
+        file_comment: Optional[str] = None,
+        timestamp: Optional[str] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Removes a reaction from an item.
+        https://docs.slack.dev/reference/methods/reactions.remove
+        """
+        kwargs.update(
+            {
+                "name": name,
+                "channel": channel,
+                "file": file,
+                "file_comment": file_comment,
+                "timestamp": timestamp,
+            }
+        )
+        return await self.api_call("reactions.remove", params=kwargs)
+
+    async def reminders_add(
+        self,
+        *,
+        text: str,
+        time: str,
+        team_id: Optional[str] = None,
+        user: Optional[str] = None,
+        recurrence: Optional[str] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Creates a reminder.
+        https://docs.slack.dev/reference/methods/reminders.add
+        """
+        kwargs.update(
+            {
+                "text": text,
+                "time": time,
+                "team_id": team_id,
+                "user": user,
+                "recurrence": recurrence,
+            }
+        )
+        return await self.api_call("reminders.add", params=kwargs)
+
+    async def reminders_complete(
+        self,
+        *,
+        reminder: str,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Marks a reminder as complete.
+        https://docs.slack.dev/reference/methods/reminders.complete
+        """
+        kwargs.update({"reminder": reminder, "team_id": team_id})
+        return await self.api_call("reminders.complete", params=kwargs)
+
+    async def reminders_delete(
+        self,
+        *,
+        reminder: str,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Deletes a reminder.
+        https://docs.slack.dev/reference/methods/reminders.delete
+        """
+        kwargs.update({"reminder": reminder, "team_id": team_id})
+        return await self.api_call("reminders.delete", params=kwargs)
+
+    async def reminders_info(
+        self,
+        *,
+        reminder: str,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Gets information about a reminder.
+        https://docs.slack.dev/reference/methods/reminders.info
+        """
+        kwargs.update({"reminder": reminder, "team_id": team_id})
+        return await self.api_call("reminders.info", http_verb="GET", params=kwargs)
+
+    async def reminders_list(
+        self,
+        *,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Lists all reminders created by or for a given user.
+        https://docs.slack.dev/reference/methods/reminders.list
+        """
+        kwargs.update({"team_id": team_id})
+        return await self.api_call("reminders.list", http_verb="GET", params=kwargs)
+
+    async def rtm_connect(
+        self,
+        *,
+        batch_presence_aware: Optional[bool] = None,
+        presence_sub: Optional[bool] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Starts a Real Time Messaging session.
+        https://docs.slack.dev/reference/methods/rtm.connect
+        """
+        kwargs.update({"batch_presence_aware": batch_presence_aware, "presence_sub": presence_sub})
+        return await self.api_call("rtm.connect", http_verb="GET", params=kwargs)
+
+    async def rtm_start(
+        self,
+        *,
+        batch_presence_aware: Optional[bool] = None,
+        include_locale: Optional[bool] = None,
+        mpim_aware: Optional[bool] = None,
+        no_latest: Optional[bool] = None,
+        no_unreads: Optional[bool] = None,
+        presence_sub: Optional[bool] = None,
+        simple_latest: Optional[bool] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Starts a Real Time Messaging session.
+        https://docs.slack.dev/reference/methods/rtm.start
+        """
+        kwargs.update(
+            {
+                "batch_presence_aware": batch_presence_aware,
+                "include_locale": include_locale,
+                "mpim_aware": mpim_aware,
+                "no_latest": no_latest,
+                "no_unreads": no_unreads,
+                "presence_sub": presence_sub,
+                "simple_latest": simple_latest,
+            }
+        )
+        return await self.api_call("rtm.start", http_verb="GET", params=kwargs)
+
+    async def search_all(
+        self,
+        *,
+        query: str,
+        count: Optional[int] = None,
+        highlight: Optional[bool] = None,
+        page: Optional[int] = None,
+        sort: Optional[str] = None,
+        sort_dir: Optional[str] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Searches for messages and files matching a query.
+        https://docs.slack.dev/reference/methods/search.all
+        """
+        kwargs.update(
+            {
+                "query": query,
+                "count": count,
+                "highlight": highlight,
+                "page": page,
+                "sort": sort,
+                "sort_dir": sort_dir,
+                "team_id": team_id,
+            }
+        )
+        return await self.api_call("search.all", http_verb="GET", params=kwargs)
+
+    async def search_files(
+        self,
+        *,
+        query: str,
+        count: Optional[int] = None,
+        highlight: Optional[bool] = None,
+        page: Optional[int] = None,
+        sort: Optional[str] = None,
+        sort_dir: Optional[str] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Searches for files matching a query.
+        https://docs.slack.dev/reference/methods/search.files
+        """
+        kwargs.update(
+            {
+                "query": query,
+                "count": count,
+                "highlight": highlight,
+                "page": page,
+                "sort": sort,
+                "sort_dir": sort_dir,
+                "team_id": team_id,
+            }
+        )
+        return await self.api_call("search.files", http_verb="GET", params=kwargs)
+
+    async def search_messages(
+        self,
+        *,
+        query: str,
+        count: Optional[int] = None,
+        cursor: Optional[str] = None,
+        highlight: Optional[bool] = None,
+        page: Optional[int] = None,
+        sort: Optional[str] = None,
+        sort_dir: Optional[str] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Searches for messages matching a query.
+        https://docs.slack.dev/reference/methods/search.messages
+        """
+        kwargs.update(
+            {
+                "query": query,
+                "count": count,
+                "cursor": cursor,
+                "highlight": highlight,
+                "page": page,
+                "sort": sort,
+                "sort_dir": sort_dir,
+                "team_id": team_id,
+            }
+        )
+        return await self.api_call("search.messages", http_verb="GET", params=kwargs)
+
+    async def slackLists_access_delete(
+        self,
+        *,
+        list_id: str,
+        channel_ids: Optional[List[str]] = None,
+        user_ids: Optional[List[str]] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Revoke access to a List for specified entities.
+        https://docs.slack.dev/reference/methods/slackLists.access.delete
+        """
+        kwargs.update({"list_id": list_id, "channel_ids": channel_ids, "user_ids": user_ids})
+        kwargs = _remove_none_values(kwargs)
+        return await self.api_call("slackLists.access.delete", json=kwargs)
+
+    async def slackLists_access_set(
+        self,
+        *,
+        list_id: str,
+        access_level: str,
+        channel_ids: Optional[List[str]] = None,
+        user_ids: Optional[List[str]] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Set the access level to a List for specified entities.
+        https://docs.slack.dev/reference/methods/slackLists.access.set
+        """
+        kwargs.update({"list_id": list_id, "access_level": access_level, "channel_ids": channel_ids, "user_ids": user_ids})
+        kwargs = _remove_none_values(kwargs)
+        return await self.api_call("slackLists.access.set", json=kwargs)
+
+    async def slackLists_create(
+        self,
+        *,
+        name: str,
+        description_blocks: Optional[Union[str, Sequence[Union[Dict, RichTextBlock]]]] = None,
+        schema: Optional[List[Dict[str, Any]]] = None,
+        copy_from_list_id: Optional[str] = None,
+        include_copied_list_records: Optional[bool] = None,
+        todo_mode: Optional[bool] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Creates a List.
+        https://docs.slack.dev/reference/methods/slackLists.create
+        """
+        kwargs.update(
+            {
+                "name": name,
+                "description_blocks": description_blocks,
+                "schema": schema,
+                "copy_from_list_id": copy_from_list_id,
+                "include_copied_list_records": include_copied_list_records,
+                "todo_mode": todo_mode,
+            }
+        )
+        kwargs = _remove_none_values(kwargs)
+        return await self.api_call("slackLists.create", json=kwargs)
+
+    async def slackLists_download_get(
+        self,
+        *,
+        list_id: str,
+        job_id: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Retrieve List download URL from an export job to download List contents.
+        https://docs.slack.dev/reference/methods/slackLists.download.get
+        """
+        kwargs.update(
+            {
+                "list_id": list_id,
+                "job_id": job_id,
+            }
+        )
+        kwargs = _remove_none_values(kwargs)
+        return await self.api_call("slackLists.download.get", json=kwargs)
+
+    async def slackLists_download_start(
+        self,
+        *,
+        list_id: str,
+        include_archived: Optional[bool] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Initiate a job to export List contents.
+        https://docs.slack.dev/reference/methods/slackLists.download.start
+        """
+        kwargs.update(
+            {
+                "list_id": list_id,
+                "include_archived": include_archived,
+            }
+        )
+        kwargs = _remove_none_values(kwargs)
+        return await self.api_call("slackLists.download.start", json=kwargs)
+
+    async def slackLists_items_create(
+        self,
+        *,
+        list_id: str,
+        duplicated_item_id: Optional[str] = None,
+        parent_item_id: Optional[str] = None,
+        initial_fields: Optional[List[Dict[str, Any]]] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Add a new item to an existing List.
+        https://docs.slack.dev/reference/methods/slackLists.items.create
+        """
+        kwargs.update(
+            {
+                "list_id": list_id,
+                "duplicated_item_id": duplicated_item_id,
+                "parent_item_id": parent_item_id,
+                "initial_fields": initial_fields,
+            }
+        )
+        kwargs = _remove_none_values(kwargs)
+        return await self.api_call("slackLists.items.create", json=kwargs)
+
+    async def slackLists_items_delete(
+        self,
+        *,
+        list_id: str,
+        id: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Deletes an item from an existing List.
+        https://docs.slack.dev/reference/methods/slackLists.items.delete
+        """
+        kwargs.update(
+            {
+                "list_id": list_id,
+                "id": id,
+            }
+        )
+        kwargs = _remove_none_values(kwargs)
+        return await self.api_call("slackLists.items.delete", json=kwargs)
+
+    async def slackLists_items_deleteMultiple(
+        self,
+        *,
+        list_id: str,
+        ids: List[str],
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Deletes multiple items from an existing List.
+        https://docs.slack.dev/reference/methods/slackLists.items.deleteMultiple
+        """
+        kwargs.update(
+            {
+                "list_id": list_id,
+                "ids": ids,
+            }
+        )
+        kwargs = _remove_none_values(kwargs)
+        return await self.api_call("slackLists.items.deleteMultiple", json=kwargs)
+
+    async def slackLists_items_info(
+        self,
+        *,
+        list_id: str,
+        id: str,
+        include_is_subscribed: Optional[bool] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Get a row from a List.
+        https://docs.slack.dev/reference/methods/slackLists.items.info
+        """
+        kwargs.update(
+            {
+                "list_id": list_id,
+                "id": id,
+                "include_is_subscribed": include_is_subscribed,
+            }
+        )
+        kwargs = _remove_none_values(kwargs)
+        return await self.api_call("slackLists.items.info", json=kwargs)
+
+    async def slackLists_items_list(
+        self,
+        *,
+        list_id: str,
+        limit: Optional[int] = None,
+        cursor: Optional[str] = None,
+        archived: Optional[bool] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Get records from a List.
+        https://docs.slack.dev/reference/methods/slackLists.items.list
+        """
+        kwargs.update(
+            {
+                "list_id": list_id,
+                "limit": limit,
+                "cursor": cursor,
+                "archived": archived,
+            }
+        )
+        kwargs = _remove_none_values(kwargs)
+        return await self.api_call("slackLists.items.list", json=kwargs)
+
+    async def slackLists_items_update(
+        self,
+        *,
+        list_id: str,
+        cells: List[Dict[str, Any]],
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Updates cells in a List.
+        https://docs.slack.dev/reference/methods/slackLists.items.update
+        """
+        kwargs.update(
+            {
+                "list_id": list_id,
+                "cells": cells,
+            }
+        )
+        kwargs = _remove_none_values(kwargs)
+        return await self.api_call("slackLists.items.update", json=kwargs)
+
+    async def slackLists_update(
+        self,
+        *,
+        id: str,
+        name: Optional[str] = None,
+        description_blocks: Optional[Union[str, Sequence[Union[Dict, RichTextBlock]]]] = None,
+        todo_mode: Optional[bool] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Update a List.
+        https://docs.slack.dev/reference/methods/slackLists.update
+        """
+        kwargs.update(
+            {
+                "id": id,
+                "name": name,
+                "description_blocks": description_blocks,
+                "todo_mode": todo_mode,
+            }
+        )
+        kwargs = _remove_none_values(kwargs)
+        return await self.api_call("slackLists.update", json=kwargs)
+
+    async def stars_add(
+        self,
+        *,
+        channel: Optional[str] = None,
+        file: Optional[str] = None,
+        file_comment: Optional[str] = None,
+        timestamp: Optional[str] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Adds a star to an item.
+        https://docs.slack.dev/reference/methods/stars.add
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "file": file,
+                "file_comment": file_comment,
+                "timestamp": timestamp,
+            }
+        )
+        return await self.api_call("stars.add", params=kwargs)
+
+    async def stars_list(
+        self,
+        *,
+        count: Optional[int] = None,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        page: Optional[int] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Lists stars for a user.
+        https://docs.slack.dev/reference/methods/stars.list
+        """
+        kwargs.update(
+            {
+                "count": count,
+                "cursor": cursor,
+                "limit": limit,
+                "page": page,
+                "team_id": team_id,
+            }
+        )
+        return await self.api_call("stars.list", http_verb="GET", params=kwargs)
+
+    async def stars_remove(
+        self,
+        *,
+        channel: Optional[str] = None,
+        file: Optional[str] = None,
+        file_comment: Optional[str] = None,
+        timestamp: Optional[str] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Removes a star from an item.
+        https://docs.slack.dev/reference/methods/stars.remove
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "file": file,
+                "file_comment": file_comment,
+                "timestamp": timestamp,
+            }
+        )
+        return await self.api_call("stars.remove", params=kwargs)
+
+    async def team_accessLogs(
+        self,
+        *,
+        before: Optional[Union[int, str]] = None,
+        count: Optional[Union[int, str]] = None,
+        page: Optional[Union[int, str]] = None,
+        team_id: Optional[str] = None,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Gets the access logs for the current team.
+        https://docs.slack.dev/reference/methods/team.accessLogs
+        """
+        kwargs.update(
+            {
+                "before": before,
+                "count": count,
+                "page": page,
+                "team_id": team_id,
+                "cursor": cursor,
+                "limit": limit,
+            }
+        )
+        return await self.api_call("team.accessLogs", http_verb="GET", params=kwargs)
+
+    async def team_billableInfo(
+        self,
+        *,
+        team_id: Optional[str] = None,
+        user: Optional[str] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Gets billable users information for the current team.
+        https://docs.slack.dev/reference/methods/team.billableInfo
+        """
+        kwargs.update({"team_id": team_id, "user": user})
+        return await self.api_call("team.billableInfo", http_verb="GET", params=kwargs)
+
+    async def team_billing_info(
+        self,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Reads a workspace's billing plan information.
+        https://docs.slack.dev/reference/methods/team.billing.info
+        """
+        return await self.api_call("team.billing.info", params=kwargs)
+
+    async def team_externalTeams_disconnect(
+        self,
+        *,
+        target_team: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Disconnects an external organization.
+        https://docs.slack.dev/reference/methods/team.externalTeams.disconnect
+        """
+        kwargs.update(
+            {
+                "target_team": target_team,
+            }
+        )
+        return await self.api_call("team.externalTeams.disconnect", params=kwargs)
+
+    async def team_externalTeams_list(
+        self,
+        *,
+        connection_status_filter: Optional[str] = None,
+        slack_connect_pref_filter: Optional[Sequence[str]] = None,
+        sort_direction: Optional[str] = None,
+        sort_field: Optional[str] = None,
+        workspace_filter: Optional[Sequence[str]] = None,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Returns a list of all the external teams connected and details about the connection.
+        https://docs.slack.dev/reference/methods/team.externalTeams.list
+        """
+        kwargs.update(
+            {
+                "connection_status_filter": connection_status_filter,
+                "sort_direction": sort_direction,
+                "sort_field": sort_field,
+                "cursor": cursor,
+                "limit": limit,
+            }
+        )
+        if slack_connect_pref_filter is not None:
+            if isinstance(slack_connect_pref_filter, (list, tuple)):
+                kwargs.update({"slack_connect_pref_filter": ",".join(slack_connect_pref_filter)})
+            else:
+                kwargs.update({"slack_connect_pref_filter": slack_connect_pref_filter})
+        if workspace_filter is not None:
+            if isinstance(workspace_filter, (list, tuple)):
+                kwargs.update({"workspace_filter": ",".join(workspace_filter)})
+            else:
+                kwargs.update({"workspace_filter": workspace_filter})
+        return await self.api_call("team.externalTeams.list", http_verb="GET", params=kwargs)
+
+    async def team_info(
+        self,
+        *,
+        team: Optional[str] = None,
+        domain: Optional[str] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Gets information about the current team.
+        https://docs.slack.dev/reference/methods/team.info
+        """
+        kwargs.update({"team": team, "domain": domain})
+        return await self.api_call("team.info", http_verb="GET", params=kwargs)
+
+    async def team_integrationLogs(
+        self,
+        *,
+        app_id: Optional[str] = None,
+        change_type: Optional[str] = None,
+        count: Optional[Union[int, str]] = None,
+        page: Optional[Union[int, str]] = None,
+        service_id: Optional[str] = None,
+        team_id: Optional[str] = None,
+        user: Optional[str] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Gets the integration logs for the current team.
+        https://docs.slack.dev/reference/methods/team.integrationLogs
+        """
+        kwargs.update(
+            {
+                "app_id": app_id,
+                "change_type": change_type,
+                "count": count,
+                "page": page,
+                "service_id": service_id,
+                "team_id": team_id,
+                "user": user,
+            }
+        )
+        return await self.api_call("team.integrationLogs", http_verb="GET", params=kwargs)
+
+    async def team_profile_get(
+        self,
+        *,
+        visibility: Optional[str] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Retrieve a team's profile.
+        https://docs.slack.dev/reference/methods/team.profile.get
+        """
+        kwargs.update({"visibility": visibility})
+        return await self.api_call("team.profile.get", http_verb="GET", params=kwargs)
+
+    async def team_preferences_list(
+        self,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Retrieve a list of a workspace's team preferences.
+        https://docs.slack.dev/reference/methods/team.preferences.list
+        """
+        return await self.api_call("team.preferences.list", params=kwargs)
+
+    async def usergroups_create(
+        self,
+        *,
+        name: str,
+        channels: Optional[Union[str, Sequence[str]]] = None,
+        description: Optional[str] = None,
+        handle: Optional[str] = None,
+        include_count: Optional[bool] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Create a User Group
+        https://docs.slack.dev/reference/methods/usergroups.create
+        """
+        kwargs.update(
+            {
+                "name": name,
+                "description": description,
+                "handle": handle,
+                "include_count": include_count,
+                "team_id": team_id,
+            }
+        )
+        if isinstance(channels, (list, tuple)):
+            kwargs.update({"channels": ",".join(channels)})
+        else:
+            kwargs.update({"channels": channels})
+        return await self.api_call("usergroups.create", params=kwargs)
+
+    async def usergroups_disable(
+        self,
+        *,
+        usergroup: str,
+        include_count: Optional[bool] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Disable an existing User Group
+        https://docs.slack.dev/reference/methods/usergroups.disable
+        """
+        kwargs.update({"usergroup": usergroup, "include_count": include_count, "team_id": team_id})
+        return await self.api_call("usergroups.disable", params=kwargs)
+
+    async def usergroups_enable(
+        self,
+        *,
+        usergroup: str,
+        include_count: Optional[bool] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Enable a User Group
+        https://docs.slack.dev/reference/methods/usergroups.enable
+        """
+        kwargs.update({"usergroup": usergroup, "include_count": include_count, "team_id": team_id})
+        return await self.api_call("usergroups.enable", params=kwargs)
+
+    async def usergroups_list(
+        self,
+        *,
+        include_count: Optional[bool] = None,
+        include_disabled: Optional[bool] = None,
+        include_users: Optional[bool] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """List all User Groups for a team
+        https://docs.slack.dev/reference/methods/usergroups.list
+        """
+        kwargs.update(
+            {
+                "include_count": include_count,
+                "include_disabled": include_disabled,
+                "include_users": include_users,
+                "team_id": team_id,
+            }
+        )
+        return await self.api_call("usergroups.list", http_verb="GET", params=kwargs)
+
+    async def usergroups_update(
+        self,
+        *,
+        usergroup: str,
+        channels: Optional[Union[str, Sequence[str]]] = None,
+        description: Optional[str] = None,
+        handle: Optional[str] = None,
+        include_count: Optional[bool] = None,
+        name: Optional[str] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Update an existing User Group
+        https://docs.slack.dev/reference/methods/usergroups.update
+        """
+        kwargs.update(
+            {
+                "usergroup": usergroup,
+                "description": description,
+                "handle": handle,
+                "include_count": include_count,
+                "name": name,
+                "team_id": team_id,
+            }
+        )
+        if isinstance(channels, (list, tuple)):
+            kwargs.update({"channels": ",".join(channels)})
+        else:
+            kwargs.update({"channels": channels})
+        return await self.api_call("usergroups.update", params=kwargs)
+
+    async def usergroups_users_list(
+        self,
+        *,
+        usergroup: str,
+        include_disabled: Optional[bool] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """List all users in a User Group
+        https://docs.slack.dev/reference/methods/usergroups.users.list
+        """
+        kwargs.update(
+            {
+                "usergroup": usergroup,
+                "include_disabled": include_disabled,
+                "team_id": team_id,
+            }
+        )
+        return await self.api_call("usergroups.users.list", http_verb="GET", params=kwargs)
+
+    async def usergroups_users_update(
+        self,
+        *,
+        usergroup: str,
+        users: Union[str, Sequence[str]],
+        include_count: Optional[bool] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Update the list of users for a User Group
+        https://docs.slack.dev/reference/methods/usergroups.users.update
+        """
+        kwargs.update(
+            {
+                "usergroup": usergroup,
+                "include_count": include_count,
+                "team_id": team_id,
+            }
+        )
+        if isinstance(users, (list, tuple)):
+            kwargs.update({"users": ",".join(users)})
+        else:
+            kwargs.update({"users": users})
+        return await self.api_call("usergroups.users.update", params=kwargs)
+
+    async def users_conversations(
+        self,
+        *,
+        cursor: Optional[str] = None,
+        exclude_archived: Optional[bool] = None,
+        limit: Optional[int] = None,
+        team_id: Optional[str] = None,
+        types: Optional[Union[str, Sequence[str]]] = None,
+        user: Optional[str] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """List conversations the calling user may access.
+        https://docs.slack.dev/reference/methods/users.conversations
+        """
+        kwargs.update(
+            {
+                "cursor": cursor,
+                "exclude_archived": exclude_archived,
+                "limit": limit,
+                "team_id": team_id,
+                "user": user,
+            }
+        )
+        if isinstance(types, (list, tuple)):
+            kwargs.update({"types": ",".join(types)})
+        else:
+            kwargs.update({"types": types})
+        return await self.api_call("users.conversations", http_verb="GET", params=kwargs)
+
+    async def users_deletePhoto(
+        self,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Delete the user profile photo
+        https://docs.slack.dev/reference/methods/users.deletePhoto
+        """
+        return await self.api_call("users.deletePhoto", http_verb="GET", params=kwargs)
+
+    async def users_getPresence(
+        self,
+        *,
+        user: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Gets user presence information.
+        https://docs.slack.dev/reference/methods/users.getPresence
+        """
+        kwargs.update({"user": user})
+        return await self.api_call("users.getPresence", http_verb="GET", params=kwargs)
+
+    async def users_identity(
+        self,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Get a user's identity.
+        https://docs.slack.dev/reference/methods/users.identity
+        """
+        return await self.api_call("users.identity", http_verb="GET", params=kwargs)
+
+    async def users_info(
+        self,
+        *,
+        user: str,
+        include_locale: Optional[bool] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Gets information about a user.
+        https://docs.slack.dev/reference/methods/users.info
+        """
+        kwargs.update({"user": user, "include_locale": include_locale})
+        return await self.api_call("users.info", http_verb="GET", params=kwargs)
+
+    async def users_list(
+        self,
+        *,
+        cursor: Optional[str] = None,
+        include_locale: Optional[bool] = None,
+        limit: Optional[int] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Lists all users in a Slack team.
+        https://docs.slack.dev/reference/methods/users.list
+        """
+        kwargs.update(
+            {
+                "cursor": cursor,
+                "include_locale": include_locale,
+                "limit": limit,
+                "team_id": team_id,
+            }
+        )
+        return await self.api_call("users.list", http_verb="GET", params=kwargs)
+
+    async def users_lookupByEmail(
+        self,
+        *,
+        email: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Find a user with an email address.
+        https://docs.slack.dev/reference/methods/users.lookupByEmail
+        """
+        kwargs.update({"email": email})
+        return await self.api_call("users.lookupByEmail", http_verb="GET", params=kwargs)
+
+    async def users_setPhoto(
+        self,
+        *,
+        image: Union[str, IOBase],
+        crop_w: Optional[Union[int, str]] = None,
+        crop_x: Optional[Union[int, str]] = None,
+        crop_y: Optional[Union[int, str]] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Set the user profile photo
+        https://docs.slack.dev/reference/methods/users.setPhoto
+        """
+        kwargs.update({"crop_w": crop_w, "crop_x": crop_x, "crop_y": crop_y})
+        return await self.api_call("users.setPhoto", files={"image": image}, data=kwargs)
+
+    async def users_setPresence(
+        self,
+        *,
+        presence: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Manually sets user presence.
+        https://docs.slack.dev/reference/methods/users.setPresence
+        """
+        kwargs.update({"presence": presence})
+        return await self.api_call("users.setPresence", params=kwargs)
+
+    async def users_discoverableContacts_lookup(
+        self,
+        email: str,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Lookup an email address to see if someone is on Slack
+        https://docs.slack.dev/reference/methods/users.discoverableContacts.lookup
+        """
+        kwargs.update({"email": email})
+        return await self.api_call("users.discoverableContacts.lookup", params=kwargs)
+
+    async def users_profile_get(
+        self,
+        *,
+        user: Optional[str] = None,
+        include_labels: Optional[bool] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Retrieves a user's profile information.
+        https://docs.slack.dev/reference/methods/users.profile.get
+        """
+        kwargs.update({"user": user, "include_labels": include_labels})
+        return await self.api_call("users.profile.get", http_verb="GET", params=kwargs)
+
+    async def users_profile_set(
+        self,
+        *,
+        name: Optional[str] = None,
+        value: Optional[str] = None,
+        user: Optional[str] = None,
+        profile: Optional[Dict] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Set the profile information for a user.
+        https://docs.slack.dev/reference/methods/users.profile.set
+        """
+        kwargs.update(
+            {
+                "name": name,
+                "profile": profile,
+                "user": user,
+                "value": value,
+            }
+        )
+        kwargs = _remove_none_values(kwargs)
+        # NOTE: Intentionally using json for the "profile" parameter
+        return await self.api_call("users.profile.set", json=kwargs)
+
+    async def views_open(
+        self,
+        *,
+        trigger_id: Optional[str] = None,
+        interactivity_pointer: Optional[str] = None,
+        view: Union[dict, View],
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Open a view for a user.
+        https://docs.slack.dev/reference/methods/views.open
+        See https://docs.slack.dev/surfaces/modals/ for details.
+        """
+        kwargs.update({"trigger_id": trigger_id, "interactivity_pointer": interactivity_pointer})
+        if isinstance(view, View):
+            kwargs.update({"view": view.to_dict()})
+        else:
+            kwargs.update({"view": view})
+        kwargs = _remove_none_values(kwargs)
+        # NOTE: Intentionally using json for the "view" parameter
+        return await self.api_call("views.open", json=kwargs)
+
+    async def views_push(
+        self,
+        *,
+        trigger_id: Optional[str] = None,
+        interactivity_pointer: Optional[str] = None,
+        view: Union[dict, View],
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Push a view onto the stack of a root view.
+        Push a new view onto the existing view stack by passing a view
+        payload and a valid trigger_id generated from an interaction
+        within the existing modal.
+        Read the modals documentation (https://docs.slack.dev/surfaces/modals/)
+        to learn more about the lifecycle and intricacies of views.
+        https://docs.slack.dev/reference/methods/views.push
+        """
+        kwargs.update({"trigger_id": trigger_id, "interactivity_pointer": interactivity_pointer})
+        if isinstance(view, View):
+            kwargs.update({"view": view.to_dict()})
+        else:
+            kwargs.update({"view": view})
+        kwargs = _remove_none_values(kwargs)
+        # NOTE: Intentionally using json for the "view" parameter
+        return await self.api_call("views.push", json=kwargs)
+
+    async def views_update(
+        self,
+        *,
+        view: Union[dict, View],
+        external_id: Optional[str] = None,
+        view_id: Optional[str] = None,
+        hash: Optional[str] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Update an existing view.
+        Update a view by passing a new view definition along with the
+        view_id returned in views.open or the external_id.
+        See the modals documentation (https://docs.slack.dev/surfaces/modals/#updating_views)
+        to learn more about updating views and avoiding race conditions with the hash argument.
+        https://docs.slack.dev/reference/methods/views.update
+        """
+        if isinstance(view, View):
+            kwargs.update({"view": view.to_dict()})
+        else:
+            kwargs.update({"view": view})
+        if external_id:
+            kwargs.update({"external_id": external_id})
+        elif view_id:
+            kwargs.update({"view_id": view_id})
+        else:
+            raise e.SlackRequestError("Either view_id or external_id is required.")
+        kwargs.update({"hash": hash})
+        kwargs = _remove_none_values(kwargs)
+        # NOTE: Intentionally using json for the "view" parameter
+        return await self.api_call("views.update", json=kwargs)
+
+    async def views_publish(
+        self,
+        *,
+        user_id: str,
+        view: Union[dict, View],
+        hash: Optional[str] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Publish a static view for a User.
+        Create or update the view that comprises an
+        app's Home tab (https://docs.slack.dev/surfaces/app-home/)
+        https://docs.slack.dev/reference/methods/views.publish
+        """
+        kwargs.update({"user_id": user_id, "hash": hash})
+        if isinstance(view, View):
+            kwargs.update({"view": view.to_dict()})
+        else:
+            kwargs.update({"view": view})
+        kwargs = _remove_none_values(kwargs)
+        # NOTE: Intentionally using json for the "view" parameter
+        return await self.api_call("views.publish", json=kwargs)
+
+    async def workflows_featured_add(
+        self,
+        *,
+        channel_id: str,
+        trigger_ids: Union[str, Sequence[str]],
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Add featured workflows to a channel.
+        https://docs.slack.dev/reference/methods/workflows.featured.add
+        """
+        kwargs.update({"channel_id": channel_id})
+        if isinstance(trigger_ids, (list, tuple)):
+            kwargs.update({"trigger_ids": ",".join(trigger_ids)})
+        else:
+            kwargs.update({"trigger_ids": trigger_ids})
+        return await self.api_call("workflows.featured.add", params=kwargs)
+
+    async def workflows_featured_list(
+        self,
+        *,
+        channel_ids: Union[str, Sequence[str]],
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """List the featured workflows for specified channels.
+        https://docs.slack.dev/reference/methods/workflows.featured.list
+        """
+        if isinstance(channel_ids, (list, tuple)):
+            kwargs.update({"channel_ids": ",".join(channel_ids)})
+        else:
+            kwargs.update({"channel_ids": channel_ids})
+        return await self.api_call("workflows.featured.list", params=kwargs)
+
+    async def workflows_featured_remove(
+        self,
+        *,
+        channel_id: str,
+        trigger_ids: Union[str, Sequence[str]],
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Remove featured workflows from a channel.
+        https://docs.slack.dev/reference/methods/workflows.featured.remove
+        """
+        kwargs.update({"channel_id": channel_id})
+        if isinstance(trigger_ids, (list, tuple)):
+            kwargs.update({"trigger_ids": ",".join(trigger_ids)})
+        else:
+            kwargs.update({"trigger_ids": trigger_ids})
+        return await self.api_call("workflows.featured.remove", params=kwargs)
+
+    async def workflows_featured_set(
+        self,
+        *,
+        channel_id: str,
+        trigger_ids: Union[str, Sequence[str]],
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Set featured workflows for a channel.
+        https://docs.slack.dev/reference/methods/workflows.featured.set
+        """
+        kwargs.update({"channel_id": channel_id})
+        if isinstance(trigger_ids, (list, tuple)):
+            kwargs.update({"trigger_ids": ",".join(trigger_ids)})
+        else:
+            kwargs.update({"trigger_ids": trigger_ids})
+        return await self.api_call("workflows.featured.set", params=kwargs)
+
+    async def workflows_stepCompleted(
+        self,
+        *,
+        workflow_step_execute_id: str,
+        outputs: Optional[dict] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Indicate a successful outcome of a workflow step's execution.
+        https://docs.slack.dev/reference/methods/workflows.stepCompleted
+        """
+        kwargs.update({"workflow_step_execute_id": workflow_step_execute_id})
+        if outputs is not None:
+            kwargs.update({"outputs": outputs})
+        kwargs = _remove_none_values(kwargs)
+        # NOTE: Intentionally using json for the "outputs" parameter
+        return await self.api_call("workflows.stepCompleted", json=kwargs)
+
+    async def workflows_stepFailed(
+        self,
+        *,
+        workflow_step_execute_id: str,
+        error: Dict[str, str],
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Indicate an unsuccessful outcome of a workflow step's execution.
+        https://docs.slack.dev/reference/methods/workflows.stepFailed
+        """
+        kwargs.update(
+            {
+                "workflow_step_execute_id": workflow_step_execute_id,
+                "error": error,
+            }
+        )
+        kwargs = _remove_none_values(kwargs)
+        # NOTE: Intentionally using json for the "error" parameter
+        return await self.api_call("workflows.stepFailed", json=kwargs)
+
+    async def workflows_updateStep(
+        self,
+        *,
+        workflow_step_edit_id: str,
+        inputs: Optional[Dict[str, Any]] = None,
+        outputs: Optional[List[Dict[str, str]]] = None,
+        **kwargs,
+    ) -> AsyncSlackResponse:
+        """Update the configuration for a workflow extension step.
+        https://docs.slack.dev/reference/methods/workflows.updateStep
+        """
+        kwargs.update({"workflow_step_edit_id": workflow_step_edit_id})
+        if inputs is not None:
+            kwargs.update({"inputs": inputs})
+        if outputs is not None:
+            kwargs.update({"outputs": outputs})
+        kwargs = _remove_none_values(kwargs)
+        # NOTE: Intentionally using json for the "inputs" / "outputs" parameters
+        return await self.api_call("workflows.updateStep", json=kwargs)
+
+

A WebClient allows apps to communicate with the Slack Platform's Web API.

+

https://docs.slack.dev/reference/methods

+

The Slack Web API is an interface for querying information from +and enacting change in a Slack workspace.

+

This client handles constructing and sending HTTP requests to Slack +as well as parsing any responses received into a SlackResponse.

+

Attributes

+
+
token : str
+
A string specifying an xoxp-* or xoxb-* token.
+
base_url : str
+
A string representing the Slack API base URL. +Default is 'https://slack.com/api/'
+
timeout : int
+
The maximum number of seconds the client will wait +to connect and receive a response from Slack. +Default is 30 seconds.
+
ssl : SSLContext
+
An ssl.SSLContext instance, helpful for specifying +your own custom certificate chain.
+
proxy : str
+
String representing a fully-qualified URL to a proxy through +which to route all requests to the Slack API. Even if this parameter +is not specified, if any of the following environment variables are +present, they will be loaded into this parameter: HTTPS_PROXY, +https_proxy, HTTP_PROXY or http_proxy.
+
headers : dict
+
Additional request headers to attach to all requests.
+
+

Methods

+

api_call: Constructs a request and executes the API call to Slack.

+

Example of recommended usage:

+
    import os
+    from slack_sdk.web.async_client import AsyncWebClient
+
+    client = AsyncWebClient(token=os.environ['SLACK_API_TOKEN'])
+    response = client.chat_postMessage(
+        channel='#random',
+        text="Hello world!")
+    assert response["ok"]
+    assert response["message"]["text"] == "Hello world!"
+
+

Example manually creating an API request:

+
    import os
+    from slack_sdk.web.async_client import AsyncWebClient
+
+    client = AsyncWebClient(token=os.environ['SLACK_API_TOKEN'])
+    response = client.api_call(
+        api_method='chat.postMessage',
+        json={'channel': '#random','text': "Hello world!"}
+    )
+    assert response["ok"]
+    assert response["message"]["text"] == "Hello world!"
+
+

Note

+

Any attributes or methods prefixed with _underscores are +intended to be "private" internal use only. They may be changed or +removed at anytime.

+

Ancestors

+ +

Methods

+
+
+async def admin_analytics_getFile(self,
*,
type: str,
date: str | None = None,
metadata_only: bool | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def admin_analytics_getFile(
+    self,
+    *,
+    type: str,
+    date: Optional[str] = None,
+    metadata_only: Optional[bool] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Retrieve analytics data for a given date, presented as a compressed JSON file
+    https://docs.slack.dev/reference/methods/admin.analytics.getFile
+    """
+    kwargs.update({"type": type})
+    if date is not None:
+        kwargs.update({"date": date})
+    if metadata_only is not None:
+        kwargs.update({"metadata_only": metadata_only})
+    return await self.api_call("admin.analytics.getFile", params=kwargs)
+
+

Retrieve analytics data for a given date, presented as a compressed JSON file +https://docs.slack.dev/reference/methods/admin.analytics.getFile

+
+
+async def admin_apps_activities_list(self,
*,
app_id: str | None = None,
component_id: str | None = None,
component_type: str | None = None,
log_event_type: str | None = None,
max_date_created: int | None = None,
min_date_created: int | None = None,
min_log_level: str | None = None,
sort_direction: str | None = None,
source: str | None = None,
team_id: str | None = None,
trace_id: str | None = None,
cursor: str | None = None,
limit: int | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def admin_apps_activities_list(
+    self,
+    *,
+    app_id: Optional[str] = None,
+    component_id: Optional[str] = None,
+    component_type: Optional[str] = None,
+    log_event_type: Optional[str] = None,
+    max_date_created: Optional[int] = None,
+    min_date_created: Optional[int] = None,
+    min_log_level: Optional[str] = None,
+    sort_direction: Optional[str] = None,
+    source: Optional[str] = None,
+    team_id: Optional[str] = None,
+    trace_id: Optional[str] = None,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Get logs for a specified team/org
+    https://docs.slack.dev/reference/methods/admin.apps.activities.list
+    """
+    kwargs.update(
+        {
+            "app_id": app_id,
+            "component_id": component_id,
+            "component_type": component_type,
+            "log_event_type": log_event_type,
+            "max_date_created": max_date_created,
+            "min_date_created": min_date_created,
+            "min_log_level": min_log_level,
+            "sort_direction": sort_direction,
+            "source": source,
+            "team_id": team_id,
+            "trace_id": trace_id,
+            "cursor": cursor,
+            "limit": limit,
+        }
+    )
+    return await self.api_call("admin.apps.activities.list", params=kwargs)
+
+ +
+
+async def admin_apps_approve(self,
*,
app_id: str | None = None,
request_id: str | None = None,
enterprise_id: str | None = None,
team_id: str | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def admin_apps_approve(
+    self,
+    *,
+    app_id: Optional[str] = None,
+    request_id: Optional[str] = None,
+    enterprise_id: Optional[str] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Approve an app for installation on a workspace.
+    Either app_id or request_id is required.
+    These IDs can be obtained either directly via the app_requested event,
+    or by the admin.apps.requests.list method.
+    https://docs.slack.dev/reference/methods/admin.apps.approve
+    """
+    if app_id:
+        kwargs.update({"app_id": app_id})
+    elif request_id:
+        kwargs.update({"request_id": request_id})
+    else:
+        raise e.SlackRequestError("The app_id or request_id argument must be specified.")
+
+    kwargs.update(
+        {
+            "enterprise_id": enterprise_id,
+            "team_id": team_id,
+        }
+    )
+    return await self.api_call("admin.apps.approve", params=kwargs)
+
+

Approve an app for installation on a workspace. +Either app_id or request_id is required. +These IDs can be obtained either directly via the app_requested event, +or by the admin.apps.requests.list method. +https://docs.slack.dev/reference/methods/admin.apps.approve

+
+
+async def admin_apps_approved_list(self,
*,
cursor: str | None = None,
limit: int | None = None,
enterprise_id: str | None = None,
team_id: str | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def admin_apps_approved_list(
+    self,
+    *,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    enterprise_id: Optional[str] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """List approved apps for an org or workspace.
+    https://docs.slack.dev/reference/methods/admin.apps.approved.list
+    """
+    kwargs.update(
+        {
+            "cursor": cursor,
+            "limit": limit,
+            "enterprise_id": enterprise_id,
+            "team_id": team_id,
+        }
+    )
+    return await self.api_call("admin.apps.approved.list", http_verb="GET", params=kwargs)
+
+

List approved apps for an org or workspace. +https://docs.slack.dev/reference/methods/admin.apps.approved.list

+
+
+async def admin_apps_clearResolution(self,
*,
app_id: str,
enterprise_id: str | None = None,
team_id: str | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def admin_apps_clearResolution(
+    self,
+    *,
+    app_id: str,
+    enterprise_id: Optional[str] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Clear an app resolution
+    https://docs.slack.dev/reference/methods/admin.apps.clearResolution
+    """
+    kwargs.update(
+        {
+            "app_id": app_id,
+            "enterprise_id": enterprise_id,
+            "team_id": team_id,
+        }
+    )
+    return await self.api_call("admin.apps.clearResolution", http_verb="POST", params=kwargs)
+
+ +
+
+async def admin_apps_config_lookup(self, *, app_ids: str | Sequence[str], **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def admin_apps_config_lookup(
+    self,
+    *,
+    app_ids: Union[str, Sequence[str]],
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Look up the app config for connectors by their IDs
+    https://docs.slack.dev/reference/methods/admin.apps.config.lookup
+    """
+    if isinstance(app_ids, (list, tuple)):
+        kwargs.update({"app_ids": ",".join(app_ids)})
+    else:
+        kwargs.update({"app_ids": app_ids})
+    return await self.api_call("admin.apps.config.lookup", params=kwargs)
+
+

Look up the app config for connectors by their IDs +https://docs.slack.dev/reference/methods/admin.apps.config.lookup

+
+
+async def admin_apps_config_set(self,
*,
app_id: str,
domain_restrictions: Dict[str, Any] | None = None,
workflow_auth_strategy: str | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def admin_apps_config_set(
+    self,
+    *,
+    app_id: str,
+    domain_restrictions: Optional[Dict[str, Any]] = None,
+    workflow_auth_strategy: Optional[str] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Set the app config for a connector
+    https://docs.slack.dev/reference/methods/admin.apps.config.set
+    """
+    kwargs.update(
+        {
+            "app_id": app_id,
+            "workflow_auth_strategy": workflow_auth_strategy,
+        }
+    )
+    if domain_restrictions is not None:
+        kwargs.update({"domain_restrictions": json.dumps(domain_restrictions)})
+    return await self.api_call("admin.apps.config.set", params=kwargs)
+
+ +
+
+async def admin_apps_requests_cancel(self,
*,
request_id: str,
enterprise_id: str | None = None,
team_id: str | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def admin_apps_requests_cancel(
+    self,
+    *,
+    request_id: str,
+    enterprise_id: Optional[str] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """List app requests for a team/workspace.
+    https://docs.slack.dev/reference/methods/admin.apps.requests.cancel
+    """
+    kwargs.update(
+        {
+            "request_id": request_id,
+            "enterprise_id": enterprise_id,
+            "team_id": team_id,
+        }
+    )
+    return await self.api_call("admin.apps.requests.cancel", http_verb="POST", params=kwargs)
+
+ +
+
+async def admin_apps_requests_list(self,
*,
cursor: str | None = None,
limit: int | None = None,
team_id: str | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def admin_apps_requests_list(
+    self,
+    *,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """List app requests for a team/workspace.
+    https://docs.slack.dev/reference/methods/admin.apps.requests.list
+    """
+    kwargs.update(
+        {
+            "cursor": cursor,
+            "limit": limit,
+            "team_id": team_id,
+        }
+    )
+    return await self.api_call("admin.apps.requests.list", http_verb="GET", params=kwargs)
+
+ +
+
+async def admin_apps_restrict(self,
*,
app_id: str | None = None,
request_id: str | None = None,
enterprise_id: str | None = None,
team_id: str | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def admin_apps_restrict(
+    self,
+    *,
+    app_id: Optional[str] = None,
+    request_id: Optional[str] = None,
+    enterprise_id: Optional[str] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Restrict an app for installation on a workspace.
+    Exactly one of the team_id or enterprise_id arguments is required, not both.
+    Either app_id or request_id is required. These IDs can be obtained either directly
+    via the app_requested event, or by the admin.apps.requests.list method.
+    https://docs.slack.dev/reference/methods/admin.apps.restrict
+    """
+    if app_id:
+        kwargs.update({"app_id": app_id})
+    elif request_id:
+        kwargs.update({"request_id": request_id})
+    else:
+        raise e.SlackRequestError("The app_id or request_id argument must be specified.")
+
+    kwargs.update(
+        {
+            "enterprise_id": enterprise_id,
+            "team_id": team_id,
+        }
+    )
+    return await self.api_call("admin.apps.restrict", params=kwargs)
+
+

Restrict an app for installation on a workspace. +Exactly one of the team_id or enterprise_id arguments is required, not both. +Either app_id or request_id is required. These IDs can be obtained either directly +via the app_requested event, or by the admin.apps.requests.list method. +https://docs.slack.dev/reference/methods/admin.apps.restrict

+
+
+async def admin_apps_restricted_list(self,
*,
cursor: str | None = None,
limit: int | None = None,
enterprise_id: str | None = None,
team_id: str | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def admin_apps_restricted_list(
+    self,
+    *,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    enterprise_id: Optional[str] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """List restricted apps for an org or workspace.
+    https://docs.slack.dev/reference/methods/admin.apps.restricted.list
+    """
+    kwargs.update(
+        {
+            "cursor": cursor,
+            "limit": limit,
+            "enterprise_id": enterprise_id,
+            "team_id": team_id,
+        }
+    )
+    return await self.api_call("admin.apps.restricted.list", http_verb="GET", params=kwargs)
+
+

List restricted apps for an org or workspace. +https://docs.slack.dev/reference/methods/admin.apps.restricted.list

+
+
+async def admin_apps_uninstall(self,
*,
app_id: str,
enterprise_id: str | None = None,
team_ids: str | Sequence[str] | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def admin_apps_uninstall(
+    self,
+    *,
+    app_id: str,
+    enterprise_id: Optional[str] = None,
+    team_ids: Optional[Union[str, Sequence[str]]] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Uninstall an app from one or many workspaces, or an entire enterprise organization.
+    With an org-level token, enterprise_id or team_ids is required.
+    https://docs.slack.dev/reference/methods/admin.apps.uninstall
+    """
+    kwargs.update({"app_id": app_id})
+    if enterprise_id is not None:
+        kwargs.update({"enterprise_id": enterprise_id})
+    if team_ids is not None:
+        if isinstance(team_ids, (list, tuple)):
+            kwargs.update({"team_ids": ",".join(team_ids)})
+        else:
+            kwargs.update({"team_ids": team_ids})
+    return await self.api_call("admin.apps.uninstall", http_verb="POST", params=kwargs)
+
+

Uninstall an app from one or many workspaces, or an entire enterprise organization. +With an org-level token, enterprise_id or team_ids is required. +https://docs.slack.dev/reference/methods/admin.apps.uninstall

+
+
+async def admin_auth_policy_assignEntities(self,
*,
entity_ids: str | Sequence[str],
policy_name: str,
entity_type: str,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def admin_auth_policy_assignEntities(
+    self,
+    *,
+    entity_ids: Union[str, Sequence[str]],
+    policy_name: str,
+    entity_type: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Assign entities to a particular authentication policy.
+    https://docs.slack.dev/reference/methods/admin.auth.policy.assignEntities
+    """
+    if isinstance(entity_ids, (list, tuple)):
+        kwargs.update({"entity_ids": ",".join(entity_ids)})
+    else:
+        kwargs.update({"entity_ids": entity_ids})
+    kwargs.update({"policy_name": policy_name})
+    kwargs.update({"entity_type": entity_type})
+    return await self.api_call("admin.auth.policy.assignEntities", http_verb="POST", params=kwargs)
+
+

Assign entities to a particular authentication policy. +https://docs.slack.dev/reference/methods/admin.auth.policy.assignEntities

+
+
+async def admin_auth_policy_getEntities(self,
*,
policy_name: str,
cursor: str | None = None,
entity_type: str | None = None,
limit: int | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def admin_auth_policy_getEntities(
+    self,
+    *,
+    policy_name: str,
+    cursor: Optional[str] = None,
+    entity_type: Optional[str] = None,
+    limit: Optional[int] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Fetch all the entities assigned to a particular authentication policy by name.
+    https://docs.slack.dev/reference/methods/admin.auth.policy.getEntities
+    """
+    kwargs.update({"policy_name": policy_name})
+    if cursor is not None:
+        kwargs.update({"cursor": cursor})
+    if entity_type is not None:
+        kwargs.update({"entity_type": entity_type})
+    if limit is not None:
+        kwargs.update({"limit": limit})
+    return await self.api_call("admin.auth.policy.getEntities", http_verb="POST", params=kwargs)
+
+

Fetch all the entities assigned to a particular authentication policy by name. +https://docs.slack.dev/reference/methods/admin.auth.policy.getEntities

+
+
+async def admin_auth_policy_removeEntities(self,
*,
entity_ids: str | Sequence[str],
policy_name: str,
entity_type: str,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def admin_auth_policy_removeEntities(
+    self,
+    *,
+    entity_ids: Union[str, Sequence[str]],
+    policy_name: str,
+    entity_type: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Remove specified entities from a specified authentication policy.
+    https://docs.slack.dev/reference/methods/admin.auth.policy.removeEntities
+    """
+    if isinstance(entity_ids, (list, tuple)):
+        kwargs.update({"entity_ids": ",".join(entity_ids)})
+    else:
+        kwargs.update({"entity_ids": entity_ids})
+    kwargs.update({"policy_name": policy_name})
+    kwargs.update({"entity_type": entity_type})
+    return await self.api_call("admin.auth.policy.removeEntities", http_verb="POST", params=kwargs)
+
+

Remove specified entities from a specified authentication policy. +https://docs.slack.dev/reference/methods/admin.auth.policy.removeEntities

+
+
+async def admin_barriers_create(self,
*,
barriered_from_usergroup_ids: str | Sequence[str],
primary_usergroup_id: str,
restricted_subjects: str | Sequence[str],
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def admin_barriers_create(
+    self,
+    *,
+    barriered_from_usergroup_ids: Union[str, Sequence[str]],
+    primary_usergroup_id: str,
+    restricted_subjects: Union[str, Sequence[str]],
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Create an Information Barrier
+    https://docs.slack.dev/reference/methods/admin.barriers.create
+    """
+    kwargs.update({"primary_usergroup_id": primary_usergroup_id})
+    if isinstance(barriered_from_usergroup_ids, (list, tuple)):
+        kwargs.update({"barriered_from_usergroup_ids": ",".join(barriered_from_usergroup_ids)})
+    else:
+        kwargs.update({"barriered_from_usergroup_ids": barriered_from_usergroup_ids})
+    if isinstance(restricted_subjects, (list, tuple)):
+        kwargs.update({"restricted_subjects": ",".join(restricted_subjects)})
+    else:
+        kwargs.update({"restricted_subjects": restricted_subjects})
+    return await self.api_call("admin.barriers.create", http_verb="POST", params=kwargs)
+
+ +
+
+async def admin_barriers_delete(self, *, barrier_id: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def admin_barriers_delete(
+    self,
+    *,
+    barrier_id: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Delete an existing Information Barrier
+    https://docs.slack.dev/reference/methods/admin.barriers.delete
+    """
+    kwargs.update({"barrier_id": barrier_id})
+    return await self.api_call("admin.barriers.delete", http_verb="POST", params=kwargs)
+
+ +
+
+async def admin_barriers_list(self, *, cursor: str | None = None, limit: int | None = None, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def admin_barriers_list(
+    self,
+    *,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Get all Information Barriers for your organization
+    https://docs.slack.dev/reference/methods/admin.barriers.list"""
+    kwargs.update(
+        {
+            "cursor": cursor,
+            "limit": limit,
+        }
+    )
+    return await self.api_call("admin.barriers.list", http_verb="GET", params=kwargs)
+
+

Get all Information Barriers for your organization +https://docs.slack.dev/reference/methods/admin.barriers.list

+
+
+async def admin_barriers_update(self,
*,
barrier_id: str,
barriered_from_usergroup_ids: str | Sequence[str],
primary_usergroup_id: str,
restricted_subjects: str | Sequence[str],
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def admin_barriers_update(
+    self,
+    *,
+    barrier_id: str,
+    barriered_from_usergroup_ids: Union[str, Sequence[str]],
+    primary_usergroup_id: str,
+    restricted_subjects: Union[str, Sequence[str]],
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Update an existing Information Barrier
+    https://docs.slack.dev/reference/methods/admin.barriers.update
+    """
+    kwargs.update({"barrier_id": barrier_id, "primary_usergroup_id": primary_usergroup_id})
+    if isinstance(barriered_from_usergroup_ids, (list, tuple)):
+        kwargs.update({"barriered_from_usergroup_ids": ",".join(barriered_from_usergroup_ids)})
+    else:
+        kwargs.update({"barriered_from_usergroup_ids": barriered_from_usergroup_ids})
+    if isinstance(restricted_subjects, (list, tuple)):
+        kwargs.update({"restricted_subjects": ",".join(restricted_subjects)})
+    else:
+        kwargs.update({"restricted_subjects": restricted_subjects})
+    return await self.api_call("admin.barriers.update", http_verb="POST", params=kwargs)
+
+ +
+
+async def admin_conversations_archive(self, *, channel_id: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def admin_conversations_archive(
+    self,
+    *,
+    channel_id: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Archive a public or private channel.
+    https://docs.slack.dev/reference/methods/admin.conversations.archive
+    """
+    kwargs.update({"channel_id": channel_id})
+    return await self.api_call("admin.conversations.archive", params=kwargs)
+
+ +
+
+async def admin_conversations_bulkArchive(self, *, channel_ids: str | Sequence[str], **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def admin_conversations_bulkArchive(
+    self,
+    *,
+    channel_ids: Union[Sequence[str], str],
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Archive public or private channels in bulk.
+    https://docs.slack.dev/reference/methods/admin.conversations.bulkArchive
+    """
+    kwargs.update({"channel_ids": ",".join(channel_ids) if isinstance(channel_ids, (list, tuple)) else channel_ids})
+    return await self.api_call("admin.conversations.bulkArchive", params=kwargs)
+
+ +
+
+async def admin_conversations_bulkDelete(self, *, channel_ids: str | Sequence[str], **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def admin_conversations_bulkDelete(
+    self,
+    *,
+    channel_ids: Union[Sequence[str], str],
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Delete public or private channels in bulk.
+    https://slack.com/api/admin.conversations.bulkDelete
+    """
+    kwargs.update({"channel_ids": ",".join(channel_ids) if isinstance(channel_ids, (list, tuple)) else channel_ids})
+    return await self.api_call("admin.conversations.bulkDelete", params=kwargs)
+
+

Delete public or private channels in bulk. +https://slack.com/api/admin.conversations.bulkDelete

+
+
+async def admin_conversations_bulkMove(self, *, channel_ids: str | Sequence[str], target_team_id: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def admin_conversations_bulkMove(
+    self,
+    *,
+    channel_ids: Union[Sequence[str], str],
+    target_team_id: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Move public or private channels in bulk.
+    https://docs.slack.dev/reference/methods/admin.conversations.bulkMove
+    """
+    kwargs.update(
+        {
+            "target_team_id": target_team_id,
+            "channel_ids": ",".join(channel_ids) if isinstance(channel_ids, (list, tuple)) else channel_ids,
+        }
+    )
+    return await self.api_call("admin.conversations.bulkMove", params=kwargs)
+
+ +
+
+async def admin_conversations_convertToPrivate(self, *, channel_id: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def admin_conversations_convertToPrivate(
+    self,
+    *,
+    channel_id: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Convert a public channel to a private channel.
+    https://docs.slack.dev/reference/methods/admin.conversations.convertToPrivate
+    """
+    kwargs.update({"channel_id": channel_id})
+    return await self.api_call("admin.conversations.convertToPrivate", params=kwargs)
+
+ +
+
+async def admin_conversations_convertToPublic(self, *, channel_id: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def admin_conversations_convertToPublic(
+    self,
+    *,
+    channel_id: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Convert a privte channel to a public channel.
+    https://docs.slack.dev/reference/methods/admin.conversations.convertToPublic
+    """
+    kwargs.update({"channel_id": channel_id})
+    return await self.api_call("admin.conversations.convertToPublic", params=kwargs)
+
+ +
+
+async def admin_conversations_create(self,
*,
is_private: bool,
name: str,
description: str | None = None,
org_wide: bool | None = None,
team_id: str | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def admin_conversations_create(
+    self,
+    *,
+    is_private: bool,
+    name: str,
+    description: Optional[str] = None,
+    org_wide: Optional[bool] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Create a public or private channel-based conversation.
+    https://docs.slack.dev/reference/methods/admin.conversations.create
+    """
+    kwargs.update(
+        {
+            "is_private": is_private,
+            "name": name,
+            "description": description,
+            "org_wide": org_wide,
+            "team_id": team_id,
+        }
+    )
+    return await self.api_call("admin.conversations.create", params=kwargs)
+
+

Create a public or private channel-based conversation. +https://docs.slack.dev/reference/methods/admin.conversations.create

+
+
+async def admin_conversations_createForObjects(self,
*,
object_id: str,
salesforce_org_id: str,
invite_object_team: bool | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def admin_conversations_createForObjects(
+    self,
+    *,
+    object_id: str,
+    salesforce_org_id: str,
+    invite_object_team: Optional[bool] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Create a Salesforce channel for the corresponding object provided.
+    https://docs.slack.dev/reference/methods/admin.conversations.createForObjects
+    """
+    kwargs.update(
+        {"object_id": object_id, "salesforce_org_id": salesforce_org_id, "invite_object_team": invite_object_team}
+    )
+    return await self.api_call("admin.conversations.createForObjects", params=kwargs)
+
+

Create a Salesforce channel for the corresponding object provided. +https://docs.slack.dev/reference/methods/admin.conversations.createForObjects

+
+
+async def admin_conversations_delete(self, *, channel_id: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def admin_conversations_delete(
+    self,
+    *,
+    channel_id: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Delete a public or private channel.
+    https://docs.slack.dev/reference/methods/admin.conversations.delete
+    """
+    kwargs.update({"channel_id": channel_id})
+    return await self.api_call("admin.conversations.delete", params=kwargs)
+
+ +
+
+async def admin_conversations_disconnectShared(self,
*,
channel_id: str,
leaving_team_ids: str | Sequence[str] | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def admin_conversations_disconnectShared(
+    self,
+    *,
+    channel_id: str,
+    leaving_team_ids: Optional[Union[str, Sequence[str]]] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Disconnect a connected channel from one or more workspaces.
+    https://docs.slack.dev/reference/methods/admin.conversations.disconnectShared
+    """
+    kwargs.update({"channel_id": channel_id})
+    if isinstance(leaving_team_ids, (list, tuple)):
+        kwargs.update({"leaving_team_ids": ",".join(leaving_team_ids)})
+    else:
+        kwargs.update({"leaving_team_ids": leaving_team_ids})
+    return await self.api_call("admin.conversations.disconnectShared", params=kwargs)
+
+

Disconnect a connected channel from one or more workspaces. +https://docs.slack.dev/reference/methods/admin.conversations.disconnectShared

+
+
+async def admin_conversations_ekm_listOriginalConnectedChannelInfo(self,
*,
channel_ids: str | Sequence[str] | None = None,
cursor: str | None = None,
limit: int | None = None,
team_ids: str | Sequence[str] | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def admin_conversations_ekm_listOriginalConnectedChannelInfo(
+    self,
+    *,
+    channel_ids: Optional[Union[str, Sequence[str]]] = None,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    team_ids: Optional[Union[str, Sequence[str]]] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """List all disconnected channels—i.e.,
+    channels that were once connected to other workspaces and then disconnected—and
+    the corresponding original channel IDs for key revocation with EKM.
+    https://docs.slack.dev/reference/methods/admin.conversations.ekm.listOriginalConnectedChannelInfo
+    """
+    kwargs.update(
+        {
+            "cursor": cursor,
+            "limit": limit,
+        }
+    )
+    if isinstance(channel_ids, (list, tuple)):
+        kwargs.update({"channel_ids": ",".join(channel_ids)})
+    else:
+        kwargs.update({"channel_ids": channel_ids})
+    if isinstance(team_ids, (list, tuple)):
+        kwargs.update({"team_ids": ",".join(team_ids)})
+    else:
+        kwargs.update({"team_ids": team_ids})
+    return await self.api_call("admin.conversations.ekm.listOriginalConnectedChannelInfo", params=kwargs)
+
+

List all disconnected channels—i.e., +channels that were once connected to other workspaces and then disconnected—and +the corresponding original channel IDs for key revocation with EKM. +https://docs.slack.dev/reference/methods/admin.conversations.ekm.listOriginalConnectedChannelInfo

+
+
+async def admin_conversations_getConversationPrefs(self, *, channel_id: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def admin_conversations_getConversationPrefs(
+    self,
+    *,
+    channel_id: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Get conversation preferences for a public or private channel.
+    https://docs.slack.dev/reference/methods/admin.conversations.getConversationPrefs
+    """
+    kwargs.update({"channel_id": channel_id})
+    return await self.api_call("admin.conversations.getConversationPrefs", params=kwargs)
+
+

Get conversation preferences for a public or private channel. +https://docs.slack.dev/reference/methods/admin.conversations.getConversationPrefs

+
+
+async def admin_conversations_getCustomRetention(self, *, channel_id: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def admin_conversations_getCustomRetention(
+    self,
+    *,
+    channel_id: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Get a channel's retention policy
+    https://docs.slack.dev/reference/methods/admin.conversations.getCustomRetention
+    """
+    kwargs.update({"channel_id": channel_id})
+    return await self.api_call("admin.conversations.getCustomRetention", params=kwargs)
+
+ +
+
+async def admin_conversations_getTeams(self,
*,
channel_id: str,
cursor: str | None = None,
limit: int | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def admin_conversations_getTeams(
+    self,
+    *,
+    channel_id: str,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Set the workspaces in an Enterprise grid org that connect to a channel.
+    https://docs.slack.dev/reference/methods/admin.conversations.getTeams
+    """
+    kwargs.update(
+        {
+            "channel_id": channel_id,
+            "cursor": cursor,
+            "limit": limit,
+        }
+    )
+    return await self.api_call("admin.conversations.getTeams", params=kwargs)
+
+

Set the workspaces in an Enterprise grid org that connect to a channel. +https://docs.slack.dev/reference/methods/admin.conversations.getTeams

+
+
+async def admin_conversations_invite(self, *, channel_id: str, user_ids: str | Sequence[str], **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def admin_conversations_invite(
+    self,
+    *,
+    channel_id: str,
+    user_ids: Union[str, Sequence[str]],
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Invite a user to a public or private channel.
+    https://docs.slack.dev/reference/methods/admin.conversations.invite
+    """
+    kwargs.update({"channel_id": channel_id})
+    if isinstance(user_ids, (list, tuple)):
+        kwargs.update({"user_ids": ",".join(user_ids)})
+    else:
+        kwargs.update({"user_ids": user_ids})
+    # NOTE: the endpoint is unable to handle Content-Type: application/json as of Sep 3, 2020.
+    return await self.api_call("admin.conversations.invite", params=kwargs)
+
+

Invite a user to a public or private channel. +https://docs.slack.dev/reference/methods/admin.conversations.invite

+
+
+async def admin_conversations_linkObjects(self, *, channel: str, record_id: str, salesforce_org_id: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def admin_conversations_linkObjects(
+    self,
+    *,
+    channel: str,
+    record_id: str,
+    salesforce_org_id: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Link a Salesforce record to a channel.
+    https://docs.slack.dev/reference/methods/admin.conversations.linkObjects
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "record_id": record_id,
+            "salesforce_org_id": salesforce_org_id,
+        }
+    )
+    return await self.api_call("admin.conversations.linkObjects", params=kwargs)
+
+ +
+
+async def admin_conversations_lookup(self,
*,
last_message_activity_before: int,
team_ids: str | Sequence[str],
cursor: str | None = None,
limit: int | None = None,
max_member_count: int | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def admin_conversations_lookup(
+    self,
+    *,
+    last_message_activity_before: int,
+    team_ids: Union[str, Sequence[str]],
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    max_member_count: Optional[int] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Returns channels on the given team using the filters.
+    https://docs.slack.dev/reference/methods/admin.conversations.lookup
+    """
+    kwargs.update(
+        {
+            "last_message_activity_before": last_message_activity_before,
+            "cursor": cursor,
+            "limit": limit,
+            "max_member_count": max_member_count,
+        }
+    )
+    if isinstance(team_ids, (list, tuple)):
+        kwargs.update({"team_ids": ",".join(team_ids)})
+    else:
+        kwargs.update({"team_ids": team_ids})
+    return await self.api_call("admin.conversations.lookup", params=kwargs)
+
+

Returns channels on the given team using the filters. +https://docs.slack.dev/reference/methods/admin.conversations.lookup

+
+
+async def admin_conversations_removeCustomRetention(self, *, channel_id: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def admin_conversations_removeCustomRetention(
+    self,
+    *,
+    channel_id: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Remove a channel's retention policy
+    https://docs.slack.dev/reference/methods/admin.conversations.removeCustomRetention
+    """
+    kwargs.update({"channel_id": channel_id})
+    return await self.api_call("admin.conversations.removeCustomRetention", params=kwargs)
+
+ +
+
+async def admin_conversations_rename(self, *, channel_id: str, name: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def admin_conversations_rename(
+    self,
+    *,
+    channel_id: str,
+    name: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Rename a public or private channel.
+    https://docs.slack.dev/reference/methods/admin.conversations.rename
+    """
+    kwargs.update({"channel_id": channel_id, "name": name})
+    return await self.api_call("admin.conversations.rename", params=kwargs)
+
+ +
+
+async def admin_conversations_restrictAccess_addGroup(self, *, channel_id: str, group_id: str, team_id: str | None = None, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def admin_conversations_restrictAccess_addGroup(
+    self,
+    *,
+    channel_id: str,
+    group_id: str,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Add an allowlist of IDP groups for accessing a channel.
+    https://docs.slack.dev/reference/methods/admin.conversations.restrictAccess.addGroup
+    """
+    kwargs.update(
+        {
+            "channel_id": channel_id,
+            "group_id": group_id,
+            "team_id": team_id,
+        }
+    )
+    return await self.api_call(
+        "admin.conversations.restrictAccess.addGroup",
+        http_verb="GET",
+        params=kwargs,
+    )
+
+

Add an allowlist of IDP groups for accessing a channel. +https://docs.slack.dev/reference/methods/admin.conversations.restrictAccess.addGroup

+
+
+async def admin_conversations_restrictAccess_listGroups(self, *, channel_id: str, team_id: str | None = None, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def admin_conversations_restrictAccess_listGroups(
+    self,
+    *,
+    channel_id: str,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """List all IDP Groups linked to a channel.
+    https://docs.slack.dev/reference/methods/admin.conversations.restrictAccess.listGroups
+    """
+    kwargs.update(
+        {
+            "channel_id": channel_id,
+            "team_id": team_id,
+        }
+    )
+    return await self.api_call(
+        "admin.conversations.restrictAccess.listGroups",
+        http_verb="GET",
+        params=kwargs,
+    )
+
+ +
+
+async def admin_conversations_restrictAccess_removeGroup(self, *, channel_id: str, group_id: str, team_id: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def admin_conversations_restrictAccess_removeGroup(
+    self,
+    *,
+    channel_id: str,
+    group_id: str,
+    team_id: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Remove a linked IDP group linked from a private channel.
+    https://docs.slack.dev/reference/methods/admin.conversations.restrictAccess.removeGroup
+    """
+    kwargs.update(
+        {
+            "channel_id": channel_id,
+            "group_id": group_id,
+            "team_id": team_id,
+        }
+    )
+    return await self.api_call(
+        "admin.conversations.restrictAccess.removeGroup",
+        http_verb="GET",
+        params=kwargs,
+    )
+
+

Remove a linked IDP group linked from a private channel. +https://docs.slack.dev/reference/methods/admin.conversations.restrictAccess.removeGroup

+
+ +
+
+ +Expand source code + +
async def admin_conversations_search(
+    self,
+    *,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    query: Optional[str] = None,
+    search_channel_types: Optional[Union[str, Sequence[str]]] = None,
+    sort: Optional[str] = None,
+    sort_dir: Optional[str] = None,
+    team_ids: Optional[Union[str, Sequence[str]]] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Search for public or private channels in an Enterprise organization.
+    https://docs.slack.dev/reference/methods/admin.conversations.search
+    """
+    kwargs.update(
+        {
+            "cursor": cursor,
+            "limit": limit,
+            "query": query,
+            "sort": sort,
+            "sort_dir": sort_dir,
+        }
+    )
+
+    if isinstance(search_channel_types, (list, tuple)):
+        kwargs.update({"search_channel_types": ",".join(search_channel_types)})
+    else:
+        kwargs.update({"search_channel_types": search_channel_types})
+
+    if isinstance(team_ids, (list, tuple)):
+        kwargs.update({"team_ids": ",".join(team_ids)})
+    else:
+        kwargs.update({"team_ids": team_ids})
+
+    return await self.api_call("admin.conversations.search", params=kwargs)
+
+

Search for public or private channels in an Enterprise organization. +https://docs.slack.dev/reference/methods/admin.conversations.search

+
+
+async def admin_conversations_setConversationPrefs(self, *, channel_id: str, prefs: str | Dict[str, str], **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def admin_conversations_setConversationPrefs(
+    self,
+    *,
+    channel_id: str,
+    prefs: Union[str, Dict[str, str]],
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Set the posting permissions for a public or private channel.
+    https://docs.slack.dev/reference/methods/admin.conversations.setConversationPrefs
+    """
+    kwargs.update({"channel_id": channel_id})
+    if isinstance(prefs, dict):
+        kwargs.update({"prefs": json.dumps(prefs)})
+    else:
+        kwargs.update({"prefs": prefs})
+    return await self.api_call("admin.conversations.setConversationPrefs", params=kwargs)
+
+

Set the posting permissions for a public or private channel. +https://docs.slack.dev/reference/methods/admin.conversations.setConversationPrefs

+
+
+async def admin_conversations_setCustomRetention(self, *, channel_id: str, duration_days: int, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def admin_conversations_setCustomRetention(
+    self,
+    *,
+    channel_id: str,
+    duration_days: int,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Set a channel's retention policy
+    https://docs.slack.dev/reference/methods/admin.conversations.setCustomRetention
+    """
+    kwargs.update({"channel_id": channel_id, "duration_days": duration_days})
+    return await self.api_call("admin.conversations.setCustomRetention", params=kwargs)
+
+ +
+
+async def admin_conversations_setTeams(self,
*,
channel_id: str,
org_channel: bool | None = None,
target_team_ids: str | Sequence[str] | None = None,
team_id: str | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def admin_conversations_setTeams(
+    self,
+    *,
+    channel_id: str,
+    org_channel: Optional[bool] = None,
+    target_team_ids: Optional[Union[str, Sequence[str]]] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Set the workspaces in an Enterprise grid org that connect to a public or private channel.
+    https://docs.slack.dev/reference/methods/admin.conversations.setTeams
+    """
+    kwargs.update(
+        {
+            "channel_id": channel_id,
+            "org_channel": org_channel,
+            "team_id": team_id,
+        }
+    )
+    if isinstance(target_team_ids, (list, tuple)):
+        kwargs.update({"target_team_ids": ",".join(target_team_ids)})
+    else:
+        kwargs.update({"target_team_ids": target_team_ids})
+    return await self.api_call("admin.conversations.setTeams", params=kwargs)
+
+

Set the workspaces in an Enterprise grid org that connect to a public or private channel. +https://docs.slack.dev/reference/methods/admin.conversations.setTeams

+
+
+async def admin_conversations_unarchive(self, *, channel_id: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def admin_conversations_unarchive(
+    self,
+    *,
+    channel_id: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Unarchive a public or private channel.
+    https://docs.slack.dev/reference/methods/admin.conversations.archive
+    """
+    kwargs.update({"channel_id": channel_id})
+    return await self.api_call("admin.conversations.unarchive", params=kwargs)
+
+ +
+
+async def admin_conversations_unlinkObjects(self, *, channel: str, new_name: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def admin_conversations_unlinkObjects(
+    self,
+    *,
+    channel: str,
+    new_name: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Unlink a Salesforce record from a channel.
+    https://docs.slack.dev/reference/methods/admin.conversations.unlinkObjects
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "new_name": new_name,
+        }
+    )
+    return await self.api_call("admin.conversations.unlinkObjects", params=kwargs)
+
+ +
+
+async def admin_emoji_add(self, *, name: str, url: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def admin_emoji_add(
+    self,
+    *,
+    name: str,
+    url: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Add an emoji.
+    https://docs.slack.dev/reference/methods/admin.emoji.add
+    """
+    kwargs.update({"name": name, "url": url})
+    return await self.api_call("admin.emoji.add", http_verb="GET", params=kwargs)
+
+ +
+
+async def admin_emoji_addAlias(self, *, alias_for: str, name: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def admin_emoji_addAlias(
+    self,
+    *,
+    alias_for: str,
+    name: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Add an emoji alias.
+    https://docs.slack.dev/reference/methods/admin.emoji.addAlias
+    """
+    kwargs.update({"alias_for": alias_for, "name": name})
+    return await self.api_call("admin.emoji.addAlias", http_verb="GET", params=kwargs)
+
+ +
+
+async def admin_emoji_list(self, *, cursor: str | None = None, limit: int | None = None, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def admin_emoji_list(
+    self,
+    *,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """List emoji for an Enterprise Grid organization.
+    https://docs.slack.dev/reference/methods/admin.emoji.list
+    """
+    kwargs.update({"cursor": cursor, "limit": limit})
+    return await self.api_call("admin.emoji.list", http_verb="GET", params=kwargs)
+
+

List emoji for an Enterprise Grid organization. +https://docs.slack.dev/reference/methods/admin.emoji.list

+
+
+async def admin_emoji_remove(self, *, name: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def admin_emoji_remove(
+    self,
+    *,
+    name: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Remove an emoji across an Enterprise Grid organization.
+    https://docs.slack.dev/reference/methods/admin.emoji.remove
+    """
+    kwargs.update({"name": name})
+    return await self.api_call("admin.emoji.remove", http_verb="GET", params=kwargs)
+
+

Remove an emoji across an Enterprise Grid organization. +https://docs.slack.dev/reference/methods/admin.emoji.remove

+
+
+async def admin_emoji_rename(self, *, name: str, new_name: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def admin_emoji_rename(
+    self,
+    *,
+    name: str,
+    new_name: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Rename an emoji.
+    https://docs.slack.dev/reference/methods/admin.emoji.rename
+    """
+    kwargs.update({"name": name, "new_name": new_name})
+    return await self.api_call("admin.emoji.rename", http_verb="GET", params=kwargs)
+
+ +
+
+async def admin_functions_list(self,
*,
app_ids: str | Sequence[str],
team_id: str | None = None,
cursor: str | None = None,
limit: int | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def admin_functions_list(
+    self,
+    *,
+    app_ids: Union[str, Sequence[str]],
+    team_id: Optional[str] = None,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Look up functions by a set of apps
+    https://docs.slack.dev/reference/methods/admin.functions.list
+    """
+    if isinstance(app_ids, (list, tuple)):
+        kwargs.update({"app_ids": ",".join(app_ids)})
+    else:
+        kwargs.update({"app_ids": app_ids})
+    kwargs.update(
+        {
+            "team_id": team_id,
+            "cursor": cursor,
+            "limit": limit,
+        }
+    )
+    return await self.api_call("admin.functions.list", params=kwargs)
+
+ +
+
+async def admin_functions_permissions_lookup(self, *, function_ids: str | Sequence[str], **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def admin_functions_permissions_lookup(
+    self,
+    *,
+    function_ids: Union[str, Sequence[str]],
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Lookup the visibility of multiple Slack functions
+    and include the users if it is limited to particular named entities.
+    https://docs.slack.dev/reference/methods/admin.functions.permissions.lookup
+    """
+    if isinstance(function_ids, (list, tuple)):
+        kwargs.update({"function_ids": ",".join(function_ids)})
+    else:
+        kwargs.update({"function_ids": function_ids})
+    return await self.api_call("admin.functions.permissions.lookup", params=kwargs)
+
+

Lookup the visibility of multiple Slack functions +and include the users if it is limited to particular named entities. +https://docs.slack.dev/reference/methods/admin.functions.permissions.lookup

+
+
+async def admin_functions_permissions_set(self,
*,
function_id: str,
visibility: str,
user_ids: str | Sequence[str] | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def admin_functions_permissions_set(
+    self,
+    *,
+    function_id: str,
+    visibility: str,
+    user_ids: Optional[Union[str, Sequence[str]]] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Set the visibility of a Slack function
+    and define the users or workspaces if it is set to named_entities
+    https://docs.slack.dev/reference/methods/admin.functions.permissions.set
+    """
+    kwargs.update(
+        {
+            "function_id": function_id,
+            "visibility": visibility,
+        }
+    )
+    if user_ids is not None:
+        if isinstance(user_ids, (list, tuple)):
+            kwargs.update({"user_ids": ",".join(user_ids)})
+        else:
+            kwargs.update({"user_ids": user_ids})
+    return await self.api_call("admin.functions.permissions.set", params=kwargs)
+
+

Set the visibility of a Slack function +and define the users or workspaces if it is set to named_entities +https://docs.slack.dev/reference/methods/admin.functions.permissions.set

+
+
+async def admin_inviteRequests_approve(self, *, invite_request_id: str, team_id: str | None = None, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def admin_inviteRequests_approve(
+    self,
+    *,
+    invite_request_id: str,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Approve a workspace invite request.
+    https://docs.slack.dev/reference/methods/admin.inviteRequests.approve
+    """
+    kwargs.update({"invite_request_id": invite_request_id, "team_id": team_id})
+    return await self.api_call("admin.inviteRequests.approve", params=kwargs)
+
+ +
+
+async def admin_inviteRequests_approved_list(self,
*,
cursor: str | None = None,
limit: int | None = None,
team_id: str | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def admin_inviteRequests_approved_list(
+    self,
+    *,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """List all approved workspace invite requests.
+    https://docs.slack.dev/reference/methods/admin.inviteRequests.approved.list
+    """
+    kwargs.update(
+        {
+            "cursor": cursor,
+            "limit": limit,
+            "team_id": team_id,
+        }
+    )
+    return await self.api_call("admin.inviteRequests.approved.list", params=kwargs)
+
+ +
+
+async def admin_inviteRequests_denied_list(self,
*,
cursor: str | None = None,
limit: int | None = None,
team_id: str | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def admin_inviteRequests_denied_list(
+    self,
+    *,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """List all denied workspace invite requests.
+    https://docs.slack.dev/reference/methods/admin.inviteRequests.denied.list
+    """
+    kwargs.update(
+        {
+            "cursor": cursor,
+            "limit": limit,
+            "team_id": team_id,
+        }
+    )
+    return await self.api_call("admin.inviteRequests.denied.list", params=kwargs)
+
+ +
+
+async def admin_inviteRequests_deny(self, *, invite_request_id: str, team_id: str | None = None, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def admin_inviteRequests_deny(
+    self,
+    *,
+    invite_request_id: str,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Deny a workspace invite request.
+    https://docs.slack.dev/reference/methods/admin.inviteRequests.deny
+    """
+    kwargs.update({"invite_request_id": invite_request_id, "team_id": team_id})
+    return await self.api_call("admin.inviteRequests.deny", params=kwargs)
+
+ +
+
+async def admin_inviteRequests_list(self, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def admin_inviteRequests_list(
+    self,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """List all pending workspace invite requests."""
+    return await self.api_call("admin.inviteRequests.list", params=kwargs)
+
+

List all pending workspace invite requests.

+
+
+async def admin_roles_addAssignments(self,
*,
role_id: str,
entity_ids: str | Sequence[str],
user_ids: str | Sequence[str],
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def admin_roles_addAssignments(
+    self,
+    *,
+    role_id: str,
+    entity_ids: Union[str, Sequence[str]],
+    user_ids: Union[str, Sequence[str]],
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Adds members to the specified role with the specified scopes
+    https://docs.slack.dev/reference/methods/admin.roles.addAssignments
+    """
+    kwargs.update({"role_id": role_id})
+    if isinstance(entity_ids, (list, tuple)):
+        kwargs.update({"entity_ids": ",".join(entity_ids)})
+    else:
+        kwargs.update({"entity_ids": entity_ids})
+    if isinstance(user_ids, (list, tuple)):
+        kwargs.update({"user_ids": ",".join(user_ids)})
+    else:
+        kwargs.update({"user_ids": user_ids})
+    return await self.api_call("admin.roles.addAssignments", params=kwargs)
+
+

Adds members to the specified role with the specified scopes +https://docs.slack.dev/reference/methods/admin.roles.addAssignments

+
+
+async def admin_roles_listAssignments(self,
*,
role_ids: str | Sequence[str] | None = None,
entity_ids: str | Sequence[str] | None = None,
cursor: str | None = None,
limit: str | int | None = None,
sort_dir: str | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def admin_roles_listAssignments(
+    self,
+    *,
+    role_ids: Optional[Union[str, Sequence[str]]] = None,
+    entity_ids: Optional[Union[str, Sequence[str]]] = None,
+    cursor: Optional[str] = None,
+    limit: Optional[Union[str, int]] = None,
+    sort_dir: Optional[str] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Lists assignments for all roles across entities.
+        Options to scope results by any combination of roles or entities
+    https://docs.slack.dev/reference/methods/admin.roles.listAssignments
+    """
+    kwargs.update({"cursor": cursor, "limit": limit, "sort_dir": sort_dir})
+    if isinstance(entity_ids, (list, tuple)):
+        kwargs.update({"entity_ids": ",".join(entity_ids)})
+    else:
+        kwargs.update({"entity_ids": entity_ids})
+    if isinstance(role_ids, (list, tuple)):
+        kwargs.update({"role_ids": ",".join(role_ids)})
+    else:
+        kwargs.update({"role_ids": role_ids})
+    return await self.api_call("admin.roles.listAssignments", params=kwargs)
+
+

Lists assignments for all roles across entities. +Options to scope results by any combination of roles or entities +https://docs.slack.dev/reference/methods/admin.roles.listAssignments

+
+
+async def admin_roles_removeAssignments(self,
*,
role_id: str,
entity_ids: str | Sequence[str],
user_ids: str | Sequence[str],
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def admin_roles_removeAssignments(
+    self,
+    *,
+    role_id: str,
+    entity_ids: Union[str, Sequence[str]],
+    user_ids: Union[str, Sequence[str]],
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Removes a set of users from a role for the given scopes and entities
+    https://docs.slack.dev/reference/methods/admin.roles.removeAssignments
+    """
+    kwargs.update({"role_id": role_id})
+    if isinstance(entity_ids, (list, tuple)):
+        kwargs.update({"entity_ids": ",".join(entity_ids)})
+    else:
+        kwargs.update({"entity_ids": entity_ids})
+    if isinstance(user_ids, (list, tuple)):
+        kwargs.update({"user_ids": ",".join(user_ids)})
+    else:
+        kwargs.update({"user_ids": user_ids})
+    return await self.api_call("admin.roles.removeAssignments", params=kwargs)
+
+

Removes a set of users from a role for the given scopes and entities +https://docs.slack.dev/reference/methods/admin.roles.removeAssignments

+
+
+async def admin_teams_admins_list(self, *, team_id: str, cursor: str | None = None, limit: int | None = None, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def admin_teams_admins_list(
+    self,
+    *,
+    team_id: str,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """List all of the admins on a given workspace.
+    https://docs.slack.dev/reference/methods/admin.inviteRequests.list
+    """
+    kwargs.update(
+        {
+            "cursor": cursor,
+            "limit": limit,
+            "team_id": team_id,
+        }
+    )
+    return await self.api_call("admin.teams.admins.list", http_verb="GET", params=kwargs)
+
+

List all of the admins on a given workspace. +https://docs.slack.dev/reference/methods/admin.inviteRequests.list

+
+
+async def admin_teams_create(self,
*,
team_domain: str,
team_name: str,
team_description: str | None = None,
team_discoverability: str | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def admin_teams_create(
+    self,
+    *,
+    team_domain: str,
+    team_name: str,
+    team_description: Optional[str] = None,
+    team_discoverability: Optional[str] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Create an Enterprise team.
+    https://docs.slack.dev/reference/methods/admin.teams.create
+    """
+    kwargs.update(
+        {
+            "team_domain": team_domain,
+            "team_name": team_name,
+            "team_description": team_description,
+            "team_discoverability": team_discoverability,
+        }
+    )
+    return await self.api_call("admin.teams.create", params=kwargs)
+
+ +
+
+async def admin_teams_list(self, *, cursor: str | None = None, limit: int | None = None, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def admin_teams_list(
+    self,
+    *,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """List all teams on an Enterprise organization.
+    https://docs.slack.dev/reference/methods/admin.teams.list
+    """
+    kwargs.update({"cursor": cursor, "limit": limit})
+    return await self.api_call("admin.teams.list", params=kwargs)
+
+

List all teams on an Enterprise organization. +https://docs.slack.dev/reference/methods/admin.teams.list

+
+
+async def admin_teams_owners_list(self, *, team_id: str, cursor: str | None = None, limit: int | None = None, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def admin_teams_owners_list(
+    self,
+    *,
+    team_id: str,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """List all of the admins on a given workspace.
+    https://docs.slack.dev/reference/methods/admin.teams.owners.list
+    """
+    kwargs.update({"team_id": team_id, "cursor": cursor, "limit": limit})
+    return await self.api_call("admin.teams.owners.list", http_verb="GET", params=kwargs)
+
+

List all of the admins on a given workspace. +https://docs.slack.dev/reference/methods/admin.teams.owners.list

+
+
+async def admin_teams_settings_info(self, *, team_id: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def admin_teams_settings_info(
+    self,
+    *,
+    team_id: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Fetch information about settings in a workspace
+    https://docs.slack.dev/reference/methods/admin.teams.settings.info
+    """
+    kwargs.update({"team_id": team_id})
+    return await self.api_call("admin.teams.settings.info", params=kwargs)
+
+

Fetch information about settings in a workspace +https://docs.slack.dev/reference/methods/admin.teams.settings.info

+
+
+async def admin_teams_settings_setDefaultChannels(self, *, team_id: str, channel_ids: str | Sequence[str], **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def admin_teams_settings_setDefaultChannels(
+    self,
+    *,
+    team_id: str,
+    channel_ids: Union[str, Sequence[str]],
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Set the default channels of a workspace.
+    https://docs.slack.dev/reference/methods/admin.teams.settings.setDefaultChannels
+    """
+    kwargs.update({"team_id": team_id})
+    if isinstance(channel_ids, (list, tuple)):
+        kwargs.update({"channel_ids": ",".join(channel_ids)})
+    else:
+        kwargs.update({"channel_ids": channel_ids})
+    return await self.api_call("admin.teams.settings.setDefaultChannels", http_verb="GET", params=kwargs)
+
+ +
+
+async def admin_teams_settings_setDescription(self, *, team_id: str, description: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def admin_teams_settings_setDescription(
+    self,
+    *,
+    team_id: str,
+    description: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Set the description of a given workspace.
+    https://docs.slack.dev/reference/methods/admin.teams.settings.setDescription
+    """
+    kwargs.update({"team_id": team_id, "description": description})
+    return await self.api_call("admin.teams.settings.setDescription", params=kwargs)
+
+ +
+
+async def admin_teams_settings_setDiscoverability(self, *, team_id: str, discoverability: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def admin_teams_settings_setDiscoverability(
+    self,
+    *,
+    team_id: str,
+    discoverability: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Sets the icon of a workspace.
+    https://docs.slack.dev/reference/methods/admin.teams.settings.setDiscoverability
+    """
+    kwargs.update({"team_id": team_id, "discoverability": discoverability})
+    return await self.api_call("admin.teams.settings.setDiscoverability", params=kwargs)
+
+ +
+
+async def admin_teams_settings_setIcon(self, *, team_id: str, image_url: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def admin_teams_settings_setIcon(
+    self,
+    *,
+    team_id: str,
+    image_url: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Sets the icon of a workspace.
+    https://docs.slack.dev/reference/methods/admin.teams.settings.setIcon
+    """
+    kwargs.update({"team_id": team_id, "image_url": image_url})
+    return await self.api_call("admin.teams.settings.setIcon", http_verb="GET", params=kwargs)
+
+ +
+
+async def admin_teams_settings_setName(self, *, team_id: str, name: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def admin_teams_settings_setName(
+    self,
+    *,
+    team_id: str,
+    name: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Sets the icon of a workspace.
+    https://docs.slack.dev/reference/methods/admin.teams.settings.setName
+    """
+    kwargs.update({"team_id": team_id, "name": name})
+    return await self.api_call("admin.teams.settings.setName", params=kwargs)
+
+ +
+
+async def admin_usergroups_addChannels(self,
*,
channel_ids: str | Sequence[str],
usergroup_id: str,
team_id: str | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def admin_usergroups_addChannels(
+    self,
+    *,
+    channel_ids: Union[str, Sequence[str]],
+    usergroup_id: str,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Add one or more default channels to an IDP group.
+    https://docs.slack.dev/reference/methods/admin.usergroups.addChannels
+    """
+    kwargs.update({"team_id": team_id, "usergroup_id": usergroup_id})
+    if isinstance(channel_ids, (list, tuple)):
+        kwargs.update({"channel_ids": ",".join(channel_ids)})
+    else:
+        kwargs.update({"channel_ids": channel_ids})
+    return await self.api_call("admin.usergroups.addChannels", params=kwargs)
+
+

Add one or more default channels to an IDP group. +https://docs.slack.dev/reference/methods/admin.usergroups.addChannels

+
+
+async def admin_usergroups_addTeams(self,
*,
usergroup_id: str,
team_ids: str | Sequence[str],
auto_provision: bool | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def admin_usergroups_addTeams(
+    self,
+    *,
+    usergroup_id: str,
+    team_ids: Union[str, Sequence[str]],
+    auto_provision: Optional[bool] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Associate one or more default workspaces with an organization-wide IDP group.
+    https://docs.slack.dev/reference/methods/admin.usergroups.addTeams
+    """
+    kwargs.update({"usergroup_id": usergroup_id, "auto_provision": auto_provision})
+    if isinstance(team_ids, (list, tuple)):
+        kwargs.update({"team_ids": ",".join(team_ids)})
+    else:
+        kwargs.update({"team_ids": team_ids})
+    return await self.api_call("admin.usergroups.addTeams", params=kwargs)
+
+

Associate one or more default workspaces with an organization-wide IDP group. +https://docs.slack.dev/reference/methods/admin.usergroups.addTeams

+
+
+async def admin_usergroups_listChannels(self,
*,
usergroup_id: str,
include_num_members: bool | None = None,
team_id: bool | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def admin_usergroups_listChannels(
+    self,
+    *,
+    usergroup_id: str,
+    include_num_members: Optional[bool] = None,
+    team_id: Optional[bool] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Add one or more default channels to an IDP group.
+    https://docs.slack.dev/reference/methods/admin.usergroups.listChannels
+    """
+    kwargs.update(
+        {
+            "usergroup_id": usergroup_id,
+            "include_num_members": include_num_members,
+            "team_id": team_id,
+        }
+    )
+    return await self.api_call("admin.usergroups.listChannels", params=kwargs)
+
+

Add one or more default channels to an IDP group. +https://docs.slack.dev/reference/methods/admin.usergroups.listChannels

+
+
+async def admin_usergroups_removeChannels(self, *, usergroup_id: str, channel_ids: str | Sequence[str], **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def admin_usergroups_removeChannels(
+    self,
+    *,
+    usergroup_id: str,
+    channel_ids: Union[str, Sequence[str]],
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Add one or more default channels to an IDP group.
+    https://docs.slack.dev/reference/methods/admin.usergroups.removeChannels
+    """
+    kwargs.update({"usergroup_id": usergroup_id})
+    if isinstance(channel_ids, (list, tuple)):
+        kwargs.update({"channel_ids": ",".join(channel_ids)})
+    else:
+        kwargs.update({"channel_ids": channel_ids})
+    return await self.api_call("admin.usergroups.removeChannels", params=kwargs)
+
+

Add one or more default channels to an IDP group. +https://docs.slack.dev/reference/methods/admin.usergroups.removeChannels

+
+
+async def admin_users_assign(self,
*,
team_id: str,
user_id: str,
channel_ids: str | Sequence[str] | None = None,
is_restricted: bool | None = None,
is_ultra_restricted: bool | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def admin_users_assign(
+    self,
+    *,
+    team_id: str,
+    user_id: str,
+    channel_ids: Optional[Union[str, Sequence[str]]] = None,
+    is_restricted: Optional[bool] = None,
+    is_ultra_restricted: Optional[bool] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Add an Enterprise user to a workspace.
+    https://docs.slack.dev/reference/methods/admin.users.assign
+    """
+    kwargs.update(
+        {
+            "team_id": team_id,
+            "user_id": user_id,
+            "is_restricted": is_restricted,
+            "is_ultra_restricted": is_ultra_restricted,
+        }
+    )
+    if isinstance(channel_ids, (list, tuple)):
+        kwargs.update({"channel_ids": ",".join(channel_ids)})
+    else:
+        kwargs.update({"channel_ids": channel_ids})
+    return await self.api_call("admin.users.assign", params=kwargs)
+
+

Add an Enterprise user to a workspace. +https://docs.slack.dev/reference/methods/admin.users.assign

+
+
+async def admin_users_invite(self,
*,
team_id: str,
email: str,
channel_ids: str | Sequence[str],
custom_message: str | None = None,
email_password_policy_enabled: bool | None = None,
guest_expiration_ts: str | float | None = None,
is_restricted: bool | None = None,
is_ultra_restricted: bool | None = None,
real_name: str | None = None,
resend: bool | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def admin_users_invite(
+    self,
+    *,
+    team_id: str,
+    email: str,
+    channel_ids: Union[str, Sequence[str]],
+    custom_message: Optional[str] = None,
+    email_password_policy_enabled: Optional[bool] = None,
+    guest_expiration_ts: Optional[Union[str, float]] = None,
+    is_restricted: Optional[bool] = None,
+    is_ultra_restricted: Optional[bool] = None,
+    real_name: Optional[str] = None,
+    resend: Optional[bool] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Invite a user to a workspace.
+    https://docs.slack.dev/reference/methods/admin.users.invite
+    """
+    kwargs.update(
+        {
+            "team_id": team_id,
+            "email": email,
+            "custom_message": custom_message,
+            "email_password_policy_enabled": email_password_policy_enabled,
+            "guest_expiration_ts": str(guest_expiration_ts) if guest_expiration_ts is not None else None,
+            "is_restricted": is_restricted,
+            "is_ultra_restricted": is_ultra_restricted,
+            "real_name": real_name,
+            "resend": resend,
+        }
+    )
+    if isinstance(channel_ids, (list, tuple)):
+        kwargs.update({"channel_ids": ",".join(channel_ids)})
+    else:
+        kwargs.update({"channel_ids": channel_ids})
+    return await self.api_call("admin.users.invite", params=kwargs)
+
+ +
+
+async def admin_users_list(self,
*,
team_id: str | None = None,
include_deactivated_user_workspaces: bool | None = None,
is_active: bool | None = None,
cursor: str | None = None,
limit: int | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def admin_users_list(
+    self,
+    *,
+    team_id: Optional[str] = None,
+    include_deactivated_user_workspaces: Optional[bool] = None,
+    is_active: Optional[bool] = None,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """List users on a workspace
+    https://docs.slack.dev/reference/methods/admin.users.list
+    """
+    kwargs.update(
+        {
+            "team_id": team_id,
+            "include_deactivated_user_workspaces": include_deactivated_user_workspaces,
+            "is_active": is_active,
+            "cursor": cursor,
+            "limit": limit,
+        }
+    )
+    return await self.api_call("admin.users.list", params=kwargs)
+
+ +
+
+async def admin_users_remove(self, *, team_id: str, user_id: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def admin_users_remove(
+    self,
+    *,
+    team_id: str,
+    user_id: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Remove a user from a workspace.
+    https://docs.slack.dev/reference/methods/admin.users.remove
+    """
+    kwargs.update({"team_id": team_id, "user_id": user_id})
+    return await self.api_call("admin.users.remove", params=kwargs)
+
+ +
+
+async def admin_users_session_clearSettings(self, *, user_ids: str | Sequence[str], **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def admin_users_session_clearSettings(
+    self,
+    *,
+    user_ids: Union[str, Sequence[str]],
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Clear user-specific session settings—the session duration
+    and what happens when the client closes—for a list of users.
+    https://docs.slack.dev/reference/methods/admin.users.session.clearSettings
+    """
+    if isinstance(user_ids, (list, tuple)):
+        kwargs.update({"user_ids": ",".join(user_ids)})
+    else:
+        kwargs.update({"user_ids": user_ids})
+    return await self.api_call("admin.users.session.clearSettings", params=kwargs)
+
+

Clear user-specific session settings—the session duration +and what happens when the client closes—for a list of users. +https://docs.slack.dev/reference/methods/admin.users.session.clearSettings

+
+
+async def admin_users_session_getSettings(self, *, user_ids: str | Sequence[str], **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def admin_users_session_getSettings(
+    self,
+    *,
+    user_ids: Union[str, Sequence[str]],
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Get user-specific session settings—the session duration
+    and what happens when the client closes—given a list of users.
+    https://docs.slack.dev/reference/methods/admin.users.session.getSettings
+    """
+    if isinstance(user_ids, (list, tuple)):
+        kwargs.update({"user_ids": ",".join(user_ids)})
+    else:
+        kwargs.update({"user_ids": user_ids})
+    return await self.api_call("admin.users.session.getSettings", params=kwargs)
+
+

Get user-specific session settings—the session duration +and what happens when the client closes—given a list of users. +https://docs.slack.dev/reference/methods/admin.users.session.getSettings

+
+
+async def admin_users_session_invalidate(self, *, session_id: str, team_id: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def admin_users_session_invalidate(
+    self,
+    *,
+    session_id: str,
+    team_id: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Invalidate a single session for a user by session_id.
+    https://docs.slack.dev/reference/methods/admin.users.session.invalidate
+    """
+    kwargs.update({"session_id": session_id, "team_id": team_id})
+    return await self.api_call("admin.users.session.invalidate", params=kwargs)
+
+

Invalidate a single session for a user by session_id. +https://docs.slack.dev/reference/methods/admin.users.session.invalidate

+
+
+async def admin_users_session_list(self,
*,
cursor: str | None = None,
limit: int | None = None,
team_id: str | None = None,
user_id: str | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def admin_users_session_list(
+    self,
+    *,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    team_id: Optional[str] = None,
+    user_id: Optional[str] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Lists all active user sessions for an organization
+    https://docs.slack.dev/reference/methods/admin.users.session.list
+    """
+    kwargs.update(
+        {
+            "cursor": cursor,
+            "limit": limit,
+            "team_id": team_id,
+            "user_id": user_id,
+        }
+    )
+    return await self.api_call("admin.users.session.list", params=kwargs)
+
+

Lists all active user sessions for an organization +https://docs.slack.dev/reference/methods/admin.users.session.list

+
+
+async def admin_users_session_reset(self,
*,
user_id: str,
mobile_only: bool | None = None,
web_only: bool | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def admin_users_session_reset(
+    self,
+    *,
+    user_id: str,
+    mobile_only: Optional[bool] = None,
+    web_only: Optional[bool] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Wipes all valid sessions on all devices for a given user.
+    https://docs.slack.dev/reference/methods/admin.users.session.reset
+    """
+    kwargs.update(
+        {
+            "user_id": user_id,
+            "mobile_only": mobile_only,
+            "web_only": web_only,
+        }
+    )
+    return await self.api_call("admin.users.session.reset", params=kwargs)
+
+

Wipes all valid sessions on all devices for a given user. +https://docs.slack.dev/reference/methods/admin.users.session.reset

+
+
+async def admin_users_session_resetBulk(self,
*,
user_ids: str | Sequence[str],
mobile_only: bool | None = None,
web_only: bool | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def admin_users_session_resetBulk(
+    self,
+    *,
+    user_ids: Union[str, Sequence[str]],
+    mobile_only: Optional[bool] = None,
+    web_only: Optional[bool] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Enqueues an asynchronous job to wipe all valid sessions on all devices for a given list of users
+    https://docs.slack.dev/reference/methods/admin.users.session.resetBulk
+    """
+    if isinstance(user_ids, (list, tuple)):
+        kwargs.update({"user_ids": ",".join(user_ids)})
+    else:
+        kwargs.update({"user_ids": user_ids})
+    kwargs.update(
+        {
+            "mobile_only": mobile_only,
+            "web_only": web_only,
+        }
+    )
+    return await self.api_call("admin.users.session.resetBulk", params=kwargs)
+
+

Enqueues an asynchronous job to wipe all valid sessions on all devices for a given list of users +https://docs.slack.dev/reference/methods/admin.users.session.resetBulk

+
+
+async def admin_users_session_setSettings(self,
*,
user_ids: str | Sequence[str],
desktop_app_browser_quit: bool | None = None,
duration: int | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def admin_users_session_setSettings(
+    self,
+    *,
+    user_ids: Union[str, Sequence[str]],
+    desktop_app_browser_quit: Optional[bool] = None,
+    duration: Optional[int] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Configure the user-level session settings—the session duration
+    and what happens when the client closes—for one or more users.
+    https://docs.slack.dev/reference/methods/admin.users.session.setSettings
+    """
+    if isinstance(user_ids, (list, tuple)):
+        kwargs.update({"user_ids": ",".join(user_ids)})
+    else:
+        kwargs.update({"user_ids": user_ids})
+    kwargs.update(
+        {
+            "desktop_app_browser_quit": desktop_app_browser_quit,
+            "duration": duration,
+        }
+    )
+    return await self.api_call("admin.users.session.setSettings", params=kwargs)
+
+

Configure the user-level session settings—the session duration +and what happens when the client closes—for one or more users. +https://docs.slack.dev/reference/methods/admin.users.session.setSettings

+
+
+async def admin_users_setAdmin(self, *, team_id: str, user_id: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def admin_users_setAdmin(
+    self,
+    *,
+    team_id: str,
+    user_id: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Set an existing guest, regular user, or owner to be an admin user.
+    https://docs.slack.dev/reference/methods/admin.users.setAdmin
+    """
+    kwargs.update({"team_id": team_id, "user_id": user_id})
+    return await self.api_call("admin.users.setAdmin", params=kwargs)
+
+

Set an existing guest, regular user, or owner to be an admin user. +https://docs.slack.dev/reference/methods/admin.users.setAdmin

+
+
+async def admin_users_setExpiration(self, *, expiration_ts: int, user_id: str, team_id: str | None = None, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def admin_users_setExpiration(
+    self,
+    *,
+    expiration_ts: int,
+    user_id: str,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Set an expiration for a guest user.
+    https://docs.slack.dev/reference/methods/admin.users.setExpiration
+    """
+    kwargs.update({"expiration_ts": expiration_ts, "team_id": team_id, "user_id": user_id})
+    return await self.api_call("admin.users.setExpiration", params=kwargs)
+
+ +
+
+async def admin_users_setOwner(self, *, team_id: str, user_id: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def admin_users_setOwner(
+    self,
+    *,
+    team_id: str,
+    user_id: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Set an existing guest, regular user, or admin user to be a workspace owner.
+    https://docs.slack.dev/reference/methods/admin.users.setOwner
+    """
+    kwargs.update({"team_id": team_id, "user_id": user_id})
+    return await self.api_call("admin.users.setOwner", params=kwargs)
+
+

Set an existing guest, regular user, or admin user to be a workspace owner. +https://docs.slack.dev/reference/methods/admin.users.setOwner

+
+
+async def admin_users_setRegular(self, *, team_id: str, user_id: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def admin_users_setRegular(
+    self,
+    *,
+    team_id: str,
+    user_id: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Set an existing guest user, admin user, or owner to be a regular user.
+    https://docs.slack.dev/reference/methods/admin.users.setRegular
+    """
+    kwargs.update({"team_id": team_id, "user_id": user_id})
+    return await self.api_call("admin.users.setRegular", params=kwargs)
+
+

Set an existing guest user, admin user, or owner to be a regular user. +https://docs.slack.dev/reference/methods/admin.users.setRegular

+
+
+async def admin_users_unsupportedVersions_export(self,
*,
date_end_of_support: str | int | None = None,
date_sessions_started: str | int | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def admin_users_unsupportedVersions_export(
+    self,
+    *,
+    date_end_of_support: Optional[Union[str, int]] = None,
+    date_sessions_started: Optional[Union[str, int]] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Ask Slackbot to send you an export listing all workspace members using unsupported software,
+    presented as a zipped CSV file.
+    https://docs.slack.dev/reference/methods/admin.users.unsupportedVersions.export
+    """
+    kwargs.update(
+        {
+            "date_end_of_support": date_end_of_support,
+            "date_sessions_started": date_sessions_started,
+        }
+    )
+    return await self.api_call("admin.users.unsupportedVersions.export", params=kwargs)
+
+

Ask Slackbot to send you an export listing all workspace members using unsupported software, +presented as a zipped CSV file. +https://docs.slack.dev/reference/methods/admin.users.unsupportedVersions.export

+
+
+async def admin_workflows_collaborators_add(self,
*,
collaborator_ids: str | Sequence[str],
workflow_ids: str | Sequence[str],
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def admin_workflows_collaborators_add(
+    self,
+    *,
+    collaborator_ids: Union[str, Sequence[str]],
+    workflow_ids: Union[str, Sequence[str]],
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Add collaborators to workflows within the team or enterprise
+    https://docs.slack.dev/reference/methods/admin.workflows.collaborators.add
+    """
+    if isinstance(collaborator_ids, (list, tuple)):
+        kwargs.update({"collaborator_ids": ",".join(collaborator_ids)})
+    else:
+        kwargs.update({"collaborator_ids": collaborator_ids})
+    if isinstance(workflow_ids, (list, tuple)):
+        kwargs.update({"workflow_ids": ",".join(workflow_ids)})
+    else:
+        kwargs.update({"workflow_ids": workflow_ids})
+    return await self.api_call("admin.workflows.collaborators.add", params=kwargs)
+
+

Add collaborators to workflows within the team or enterprise +https://docs.slack.dev/reference/methods/admin.workflows.collaborators.add

+
+
+async def admin_workflows_collaborators_remove(self,
*,
collaborator_ids: str | Sequence[str],
workflow_ids: str | Sequence[str],
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def admin_workflows_collaborators_remove(
+    self,
+    *,
+    collaborator_ids: Union[str, Sequence[str]],
+    workflow_ids: Union[str, Sequence[str]],
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Remove collaborators from workflows within the team or enterprise
+    https://docs.slack.dev/reference/methods/admin.workflows.collaborators.remove
+    """
+    if isinstance(collaborator_ids, (list, tuple)):
+        kwargs.update({"collaborator_ids": ",".join(collaborator_ids)})
+    else:
+        kwargs.update({"collaborator_ids": collaborator_ids})
+    if isinstance(workflow_ids, (list, tuple)):
+        kwargs.update({"workflow_ids": ",".join(workflow_ids)})
+    else:
+        kwargs.update({"workflow_ids": workflow_ids})
+    return await self.api_call("admin.workflows.collaborators.remove", params=kwargs)
+
+

Remove collaborators from workflows within the team or enterprise +https://docs.slack.dev/reference/methods/admin.workflows.collaborators.remove

+
+
+async def admin_workflows_permissions_lookup(self,
*,
workflow_ids: str | Sequence[str],
max_workflow_triggers: int | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def admin_workflows_permissions_lookup(
+    self,
+    *,
+    workflow_ids: Union[str, Sequence[str]],
+    max_workflow_triggers: Optional[int] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Look up the permissions for a set of workflows
+    https://docs.slack.dev/reference/methods/admin.workflows.permissions.lookup
+    """
+    if isinstance(workflow_ids, (list, tuple)):
+        kwargs.update({"workflow_ids": ",".join(workflow_ids)})
+    else:
+        kwargs.update({"workflow_ids": workflow_ids})
+    kwargs.update(
+        {
+            "max_workflow_triggers": max_workflow_triggers,
+        }
+    )
+    return await self.api_call("admin.workflows.permissions.lookup", params=kwargs)
+
+ +
+ +
+
+ +Expand source code + +
async def admin_workflows_search(
+    self,
+    *,
+    app_id: Optional[str] = None,
+    collaborator_ids: Optional[Union[str, Sequence[str]]] = None,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    no_collaborators: Optional[bool] = None,
+    num_trigger_ids: Optional[int] = None,
+    query: Optional[str] = None,
+    sort: Optional[str] = None,
+    sort_dir: Optional[str] = None,
+    source: Optional[str] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Search workflows within the team or enterprise
+    https://docs.slack.dev/reference/methods/admin.workflows.search
+    """
+    if collaborator_ids is not None:
+        if isinstance(collaborator_ids, (list, tuple)):
+            kwargs.update({"collaborator_ids": ",".join(collaborator_ids)})
+        else:
+            kwargs.update({"collaborator_ids": collaborator_ids})
+    kwargs.update(
+        {
+            "app_id": app_id,
+            "cursor": cursor,
+            "limit": limit,
+            "no_collaborators": no_collaborators,
+            "num_trigger_ids": num_trigger_ids,
+            "query": query,
+            "sort": sort,
+            "sort_dir": sort_dir,
+            "source": source,
+        }
+    )
+    return await self.api_call("admin.workflows.search", params=kwargs)
+
+

Search workflows within the team or enterprise +https://docs.slack.dev/reference/methods/admin.workflows.search

+
+
+async def admin_workflows_unpublish(self, *, workflow_ids: str | Sequence[str], **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def admin_workflows_unpublish(
+    self,
+    *,
+    workflow_ids: Union[str, Sequence[str]],
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Unpublish workflows within the team or enterprise
+    https://docs.slack.dev/reference/methods/admin.workflows.unpublish
+    """
+    if isinstance(workflow_ids, (list, tuple)):
+        kwargs.update({"workflow_ids": ",".join(workflow_ids)})
+    else:
+        kwargs.update({"workflow_ids": workflow_ids})
+    return await self.api_call("admin.workflows.unpublish", params=kwargs)
+
+

Unpublish workflows within the team or enterprise +https://docs.slack.dev/reference/methods/admin.workflows.unpublish

+
+
+async def api_test(self, *, error: str | None = None, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def api_test(
+    self,
+    *,
+    error: Optional[str] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Checks API calling code.
+    https://docs.slack.dev/reference/methods/api.test
+    """
+    kwargs.update({"error": error})
+    return await self.api_call("api.test", params=kwargs)
+
+ +
+
+async def apps_connections_open(self, *, app_token: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def apps_connections_open(
+    self,
+    *,
+    app_token: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Generate a temporary Socket Mode WebSocket URL that your app can connect to
+    in order to receive events and interactive payloads
+    https://docs.slack.dev/reference/methods/apps.connections.open
+    """
+    kwargs.update({"token": app_token})
+    return await self.api_call("apps.connections.open", http_verb="POST", params=kwargs)
+
+

Generate a temporary Socket Mode WebSocket URL that your app can connect to +in order to receive events and interactive payloads +https://docs.slack.dev/reference/methods/apps.connections.open

+
+
+async def apps_event_authorizations_list(self,
*,
event_context: str,
cursor: str | None = None,
limit: int | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def apps_event_authorizations_list(
+    self,
+    *,
+    event_context: str,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Get a list of authorizations for the given event context.
+    Each authorization represents an app installation that the event is visible to.
+    https://docs.slack.dev/reference/methods/apps.event.authorizations.list
+    """
+    kwargs.update({"event_context": event_context, "cursor": cursor, "limit": limit})
+    return await self.api_call("apps.event.authorizations.list", params=kwargs)
+
+

Get a list of authorizations for the given event context. +Each authorization represents an app installation that the event is visible to. +https://docs.slack.dev/reference/methods/apps.event.authorizations.list

+
+
+async def apps_manifest_create(self, *, manifest: str | Dict[str, Any], **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def apps_manifest_create(
+    self,
+    *,
+    manifest: Union[str, Dict[str, Any]],
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Create an app from an app manifest
+    https://docs.slack.dev/reference/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 +
+
+
+ +Expand source code + +
async def apps_manifest_delete(
+    self,
+    *,
+    app_id: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Permanently deletes an app created through app manifests
+    https://docs.slack.dev/reference/methods/apps.manifest.delete
+    """
+    kwargs.update({"app_id": app_id})
+    return await self.api_call("apps.manifest.delete", params=kwargs)
+
+

Permanently deletes an app created through app manifests +https://docs.slack.dev/reference/methods/apps.manifest.delete

+
+
+async def apps_manifest_export(self, *, app_id: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def apps_manifest_export(
+    self,
+    *,
+    app_id: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Export an app manifest from an existing app
+    https://docs.slack.dev/reference/methods/apps.manifest.export
+    """
+    kwargs.update({"app_id": app_id})
+    return await self.api_call("apps.manifest.export", params=kwargs)
+
+

Export an app manifest from an existing app +https://docs.slack.dev/reference/methods/apps.manifest.export

+
+
+async def apps_manifest_update(self, *, app_id: str, manifest: str | Dict[str, Any], **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
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://docs.slack.dev/reference/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: str | Dict[str, Any], app_id: str | None = None, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def apps_manifest_validate(
+    self,
+    *,
+    manifest: Union[str, Dict[str, Any]],
+    app_id: Optional[str] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Validate an app manifest
+    https://docs.slack.dev/reference/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 apps_uninstall(self, *, client_id: str, client_secret: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def apps_uninstall(
+    self,
+    *,
+    client_id: str,
+    client_secret: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Uninstalls your app from a workspace.
+    https://docs.slack.dev/reference/methods/apps.uninstall
+    """
+    kwargs.update({"client_id": client_id, "client_secret": client_secret})
+    return await self.api_call("apps.uninstall", params=kwargs)
+
+

Uninstalls your app from a workspace. +https://docs.slack.dev/reference/methods/apps.uninstall

+
+
+async def assistant_threads_setStatus(self,
*,
channel_id: str,
thread_ts: str,
status: str,
loading_messages: List[str] | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def assistant_threads_setStatus(
+    self,
+    *,
+    channel_id: str,
+    thread_ts: str,
+    status: str,
+    loading_messages: Optional[List[str]] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Set the status for an AI assistant thread.
+    https://docs.slack.dev/reference/methods/assistant.threads.setStatus
+    """
+    kwargs.update(
+        {"channel_id": channel_id, "thread_ts": thread_ts, "status": status, "loading_messages": loading_messages}
+    )
+    kwargs = _remove_none_values(kwargs)
+    return await self.api_call("assistant.threads.setStatus", json=kwargs)
+
+ +
+
+async def assistant_threads_setSuggestedPrompts(self,
*,
channel_id: str,
thread_ts: str,
title: str | None = None,
prompts: List[Dict[str, str]],
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def assistant_threads_setSuggestedPrompts(
+    self,
+    *,
+    channel_id: str,
+    thread_ts: str,
+    title: Optional[str] = None,
+    prompts: List[Dict[str, str]],
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Set suggested prompts for the given assistant thread.
+    https://docs.slack.dev/reference/methods/assistant.threads.setSuggestedPrompts
+    """
+    kwargs.update({"channel_id": channel_id, "thread_ts": thread_ts, "prompts": prompts})
+    if title is not None:
+        kwargs.update({"title": title})
+    return await self.api_call("assistant.threads.setSuggestedPrompts", json=kwargs)
+
+

Set suggested prompts for the given assistant thread. +https://docs.slack.dev/reference/methods/assistant.threads.setSuggestedPrompts

+
+
+async def assistant_threads_setTitle(self, *, channel_id: str, thread_ts: str, title: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def assistant_threads_setTitle(
+    self,
+    *,
+    channel_id: str,
+    thread_ts: str,
+    title: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Set the title for the given assistant thread.
+    https://docs.slack.dev/reference/methods/assistant.threads.setTitle
+    """
+    kwargs.update({"channel_id": channel_id, "thread_ts": thread_ts, "title": title})
+    return await self.api_call("assistant.threads.setTitle", params=kwargs)
+
+

Set the title for the given assistant thread. +https://docs.slack.dev/reference/methods/assistant.threads.setTitle

+
+
+async def auth_revoke(self, *, test: bool | None = None, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def auth_revoke(
+    self,
+    *,
+    test: Optional[bool] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Revokes a token.
+    https://docs.slack.dev/reference/methods/auth.revoke
+    """
+    kwargs.update({"test": test})
+    return await self.api_call("auth.revoke", http_verb="GET", params=kwargs)
+
+ +
+
+async def auth_teams_list(self,
cursor: str | None = None,
limit: int | None = None,
include_icon: bool | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def auth_teams_list(
+    self,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    include_icon: Optional[bool] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """List the workspaces a token can access.
+    https://docs.slack.dev/reference/methods/auth.teams.list
+    """
+    kwargs.update({"cursor": cursor, "limit": limit, "include_icon": include_icon})
+    return await self.api_call("auth.teams.list", params=kwargs)
+
+

List the workspaces a token can access. +https://docs.slack.dev/reference/methods/auth.teams.list

+
+
+async def auth_test(self, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def auth_test(
+    self,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Checks authentication & identity.
+    https://docs.slack.dev/reference/methods/auth.test
+    """
+    return await self.api_call("auth.test", params=kwargs)
+
+

Checks authentication & identity. +https://docs.slack.dev/reference/methods/auth.test

+
+
+async def bookmarks_add(self,
*,
channel_id: str,
title: str,
type: str,
emoji: str | None = None,
entity_id: str | None = None,
link: str | None = None,
parent_id: str | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def bookmarks_add(
+    self,
+    *,
+    channel_id: str,
+    title: str,
+    type: str,
+    emoji: Optional[str] = None,
+    entity_id: Optional[str] = None,
+    link: Optional[str] = None,  # include when type is 'link'
+    parent_id: Optional[str] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Add bookmark to a channel.
+    https://docs.slack.dev/reference/methods/bookmarks.add
+    """
+    kwargs.update(
+        {
+            "channel_id": channel_id,
+            "title": title,
+            "type": type,
+            "emoji": emoji,
+            "entity_id": entity_id,
+            "link": link,
+            "parent_id": parent_id,
+        }
+    )
+    return await self.api_call("bookmarks.add", http_verb="POST", params=kwargs)
+
+ +
+
+async def bookmarks_edit(self,
*,
bookmark_id: str,
channel_id: str,
emoji: str | None = None,
link: str | None = None,
title: str | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def bookmarks_edit(
+    self,
+    *,
+    bookmark_id: str,
+    channel_id: str,
+    emoji: Optional[str] = None,
+    link: Optional[str] = None,
+    title: Optional[str] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Edit bookmark.
+    https://docs.slack.dev/reference/methods/bookmarks.edit
+    """
+    kwargs.update(
+        {
+            "bookmark_id": bookmark_id,
+            "channel_id": channel_id,
+            "emoji": emoji,
+            "link": link,
+            "title": title,
+        }
+    )
+    return await self.api_call("bookmarks.edit", http_verb="POST", params=kwargs)
+
+ +
+
+async def bookmarks_list(self, *, channel_id: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def bookmarks_list(
+    self,
+    *,
+    channel_id: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """List bookmark for the channel.
+    https://docs.slack.dev/reference/methods/bookmarks.list
+    """
+    kwargs.update({"channel_id": channel_id})
+    return await self.api_call("bookmarks.list", http_verb="POST", params=kwargs)
+
+ +
+
+async def bookmarks_remove(self, *, bookmark_id: str, channel_id: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def bookmarks_remove(
+    self,
+    *,
+    bookmark_id: str,
+    channel_id: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Remove bookmark from the channel.
+    https://docs.slack.dev/reference/methods/bookmarks.remove
+    """
+    kwargs.update({"bookmark_id": bookmark_id, "channel_id": channel_id})
+    return await self.api_call("bookmarks.remove", http_verb="POST", params=kwargs)
+
+ +
+
+async def bots_info(self, *, bot: str | None = None, team_id: str | None = None, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def bots_info(
+    self,
+    *,
+    bot: Optional[str] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Gets information about a bot user.
+    https://docs.slack.dev/reference/methods/bots.info
+    """
+    kwargs.update({"bot": bot, "team_id": team_id})
+    return await self.api_call("bots.info", http_verb="GET", params=kwargs)
+
+

Gets information about a bot user. +https://docs.slack.dev/reference/methods/bots.info

+
+
+async def calls_add(self,
*,
external_unique_id: str,
join_url: str,
created_by: str | None = None,
date_start: int | None = None,
desktop_app_join_url: str | None = None,
external_display_id: str | None = None,
title: str | None = None,
users: str | Sequence[Dict[str, str]] | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def calls_add(
+    self,
+    *,
+    external_unique_id: str,
+    join_url: str,
+    created_by: Optional[str] = None,
+    date_start: Optional[int] = None,
+    desktop_app_join_url: Optional[str] = None,
+    external_display_id: Optional[str] = None,
+    title: Optional[str] = None,
+    users: Optional[Union[str, Sequence[Dict[str, str]]]] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Registers a new Call.
+    https://docs.slack.dev/reference/methods/calls.add
+    """
+    kwargs.update(
+        {
+            "external_unique_id": external_unique_id,
+            "join_url": join_url,
+            "created_by": created_by,
+            "date_start": date_start,
+            "desktop_app_join_url": desktop_app_join_url,
+            "external_display_id": external_display_id,
+            "title": title,
+        }
+    )
+    _update_call_participants(
+        kwargs,
+        users if users is not None else kwargs.get("users"),  # type: ignore[arg-type]
+    )
+    return await self.api_call("calls.add", http_verb="POST", params=kwargs)
+
+ +
+
+async def calls_end(self, *, id: str, duration: int | None = None, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def calls_end(
+    self,
+    *,
+    id: str,
+    duration: Optional[int] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Ends a Call.
+    https://docs.slack.dev/reference/methods/calls.end
+    """
+    kwargs.update({"id": id, "duration": duration})
+    return await self.api_call("calls.end", http_verb="POST", params=kwargs)
+
+ +
+
+async def calls_info(self, *, id: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def calls_info(
+    self,
+    *,
+    id: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Returns information about a Call.
+    https://docs.slack.dev/reference/methods/calls.info
+    """
+    kwargs.update({"id": id})
+    return await self.api_call("calls.info", http_verb="POST", params=kwargs)
+
+

Returns information about a Call. +https://docs.slack.dev/reference/methods/calls.info

+
+
+async def calls_participants_add(self, *, id: str, users: str | Sequence[Dict[str, str]], **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def calls_participants_add(
+    self,
+    *,
+    id: str,
+    users: Union[str, Sequence[Dict[str, str]]],
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Registers new participants added to a Call.
+    https://docs.slack.dev/reference/methods/calls.participants.add
+    """
+    kwargs.update({"id": id})
+    _update_call_participants(kwargs, users)
+    return await self.api_call("calls.participants.add", http_verb="POST", params=kwargs)
+
+

Registers new participants added to a Call. +https://docs.slack.dev/reference/methods/calls.participants.add

+
+
+async def calls_participants_remove(self, *, id: str, users: str | Sequence[Dict[str, str]], **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def calls_participants_remove(
+    self,
+    *,
+    id: str,
+    users: Union[str, Sequence[Dict[str, str]]],
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Registers participants removed from a Call.
+    https://docs.slack.dev/reference/methods/calls.participants.remove
+    """
+    kwargs.update({"id": id})
+    _update_call_participants(kwargs, users)
+    return await self.api_call("calls.participants.remove", http_verb="POST", params=kwargs)
+
+

Registers participants removed from a Call. +https://docs.slack.dev/reference/methods/calls.participants.remove

+
+
+async def calls_update(self,
*,
id: str,
desktop_app_join_url: str | None = None,
join_url: str | None = None,
title: str | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def calls_update(
+    self,
+    *,
+    id: str,
+    desktop_app_join_url: Optional[str] = None,
+    join_url: Optional[str] = None,
+    title: Optional[str] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Updates information about a Call.
+    https://docs.slack.dev/reference/methods/calls.update
+    """
+    kwargs.update(
+        {
+            "id": id,
+            "desktop_app_join_url": desktop_app_join_url,
+            "join_url": join_url,
+            "title": title,
+        }
+    )
+    return await self.api_call("calls.update", http_verb="POST", params=kwargs)
+
+ +
+
+async def canvases_access_delete(self,
*,
canvas_id: str,
channel_ids: str | Sequence[str] | None = None,
user_ids: str | Sequence[str] | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def canvases_access_delete(
+    self,
+    *,
+    canvas_id: str,
+    channel_ids: Optional[Union[Sequence[str], str]] = None,
+    user_ids: Optional[Union[Sequence[str], str]] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Create a Channel Canvas for a channel
+    https://docs.slack.dev/reference/methods/canvases.access.delete
+    """
+    kwargs.update({"canvas_id": canvas_id})
+    if channel_ids is not None:
+        if isinstance(channel_ids, (list, tuple)):
+            kwargs.update({"channel_ids": ",".join(channel_ids)})
+        else:
+            kwargs.update({"channel_ids": channel_ids})
+    if user_ids is not None:
+        if isinstance(user_ids, (list, tuple)):
+            kwargs.update({"user_ids": ",".join(user_ids)})
+        else:
+            kwargs.update({"user_ids": user_ids})
+    return await self.api_call("canvases.access.delete", params=kwargs)
+
+ +
+
+async def canvases_access_set(self,
*,
canvas_id: str,
access_level: str,
channel_ids: str | Sequence[str] | None = None,
user_ids: str | Sequence[str] | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def canvases_access_set(
+    self,
+    *,
+    canvas_id: str,
+    access_level: str,
+    channel_ids: Optional[Union[Sequence[str], str]] = None,
+    user_ids: Optional[Union[Sequence[str], str]] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Sets the access level to a canvas for specified entities
+    https://docs.slack.dev/reference/methods/canvases.access.set
+    """
+    kwargs.update({"canvas_id": canvas_id, "access_level": access_level})
+    if channel_ids is not None:
+        if isinstance(channel_ids, (list, tuple)):
+            kwargs.update({"channel_ids": ",".join(channel_ids)})
+        else:
+            kwargs.update({"channel_ids": channel_ids})
+    if user_ids is not None:
+        if isinstance(user_ids, (list, tuple)):
+            kwargs.update({"user_ids": ",".join(user_ids)})
+        else:
+            kwargs.update({"user_ids": user_ids})
+
+    return await self.api_call("canvases.access.set", params=kwargs)
+
+

Sets the access level to a canvas for specified entities +https://docs.slack.dev/reference/methods/canvases.access.set

+
+
+async def canvases_create(self, *, title: str | None = None, document_content: Dict[str, str], **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def canvases_create(
+    self,
+    *,
+    title: Optional[str] = None,
+    document_content: Dict[str, str],
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Create Canvas for a user
+    https://docs.slack.dev/reference/methods/canvases.create
+    """
+    kwargs.update({"title": title, "document_content": document_content})
+    return await self.api_call("canvases.create", json=kwargs)
+
+ +
+
+async def canvases_delete(self, *, canvas_id: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def canvases_delete(
+    self,
+    *,
+    canvas_id: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Deletes a canvas
+    https://docs.slack.dev/reference/methods/canvases.delete
+    """
+    kwargs.update({"canvas_id": canvas_id})
+    return await self.api_call("canvases.delete", params=kwargs)
+
+ +
+
+async def canvases_edit(self, *, canvas_id: str, changes: Sequence[Dict[str, Any]], **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def canvases_edit(
+    self,
+    *,
+    canvas_id: str,
+    changes: Sequence[Dict[str, Any]],
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Update an existing canvas
+    https://docs.slack.dev/reference/methods/canvases.edit
+    """
+    kwargs.update({"canvas_id": canvas_id, "changes": changes})
+    return await self.api_call("canvases.edit", json=kwargs)
+
+ +
+
+async def canvases_sections_lookup(self, *, canvas_id: str, criteria: Dict[str, Any], **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def canvases_sections_lookup(
+    self,
+    *,
+    canvas_id: str,
+    criteria: Dict[str, Any],
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Find sections matching the provided criteria
+    https://docs.slack.dev/reference/methods/canvases.sections.lookup
+    """
+    kwargs.update({"canvas_id": canvas_id, "criteria": json.dumps(criteria)})
+    return await self.api_call("canvases.sections.lookup", params=kwargs)
+
+

Find sections matching the provided criteria +https://docs.slack.dev/reference/methods/canvases.sections.lookup

+
+
+async def channels_archive(self, *, channel: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def channels_archive(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Archives a channel."""
+    kwargs.update({"channel": channel})
+    kwargs = _remove_none_values(kwargs)
+    return await self.api_call("channels.archive", json=kwargs)
+
+

Archives a channel.

+
+
+async def channels_create(self, *, name: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def channels_create(
+    self,
+    *,
+    name: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Creates a channel."""
+    kwargs.update({"name": name})
+    kwargs = _remove_none_values(kwargs)
+    return await self.api_call("channels.create", json=kwargs)
+
+

Creates a channel.

+
+
+async def channels_history(self, *, channel: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def channels_history(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Fetches history of messages and events from a channel."""
+    kwargs.update({"channel": channel})
+    return await self.api_call("channels.history", http_verb="GET", params=kwargs)
+
+

Fetches history of messages and events from a channel.

+
+
+async def channels_info(self, *, channel: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def channels_info(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Gets information about a channel."""
+    kwargs.update({"channel": channel})
+    return await self.api_call("channels.info", http_verb="GET", params=kwargs)
+
+

Gets information about a channel.

+
+
+async def channels_invite(self, *, channel: str, user: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def channels_invite(
+    self,
+    *,
+    channel: str,
+    user: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Invites a user to a channel."""
+    kwargs.update({"channel": channel, "user": user})
+    kwargs = _remove_none_values(kwargs)
+    return await self.api_call("channels.invite", json=kwargs)
+
+

Invites a user to a channel.

+
+
+async def channels_join(self, *, name: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def channels_join(
+    self,
+    *,
+    name: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Joins a channel, creating it if needed."""
+    kwargs.update({"name": name})
+    kwargs = _remove_none_values(kwargs)
+    return await self.api_call("channels.join", json=kwargs)
+
+

Joins a channel, creating it if needed.

+
+
+async def channels_kick(self, *, channel: str, user: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def channels_kick(
+    self,
+    *,
+    channel: str,
+    user: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Removes a user from a channel."""
+    kwargs.update({"channel": channel, "user": user})
+    kwargs = _remove_none_values(kwargs)
+    return await self.api_call("channels.kick", json=kwargs)
+
+

Removes a user from a channel.

+
+
+async def channels_leave(self, *, channel: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def channels_leave(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Leaves a channel."""
+    kwargs.update({"channel": channel})
+    kwargs = _remove_none_values(kwargs)
+    return await self.api_call("channels.leave", json=kwargs)
+
+

Leaves a channel.

+
+
+async def channels_list(self, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def channels_list(
+    self,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Lists all channels in a Slack team."""
+    return await self.api_call("channels.list", http_verb="GET", params=kwargs)
+
+

Lists all channels in a Slack team.

+
+
+async def channels_mark(self, *, channel: str, ts: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def channels_mark(
+    self,
+    *,
+    channel: str,
+    ts: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Sets the read cursor in a channel."""
+    kwargs.update({"channel": channel, "ts": ts})
+    kwargs = _remove_none_values(kwargs)
+    return await self.api_call("channels.mark", json=kwargs)
+
+

Sets the read cursor in a channel.

+
+
+async def channels_rename(self, *, channel: str, name: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def channels_rename(
+    self,
+    *,
+    channel: str,
+    name: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Renames a channel."""
+    kwargs.update({"channel": channel, "name": name})
+    kwargs = _remove_none_values(kwargs)
+    return await self.api_call("channels.rename", json=kwargs)
+
+

Renames a channel.

+
+
+async def channels_replies(self, *, channel: str, thread_ts: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def channels_replies(
+    self,
+    *,
+    channel: str,
+    thread_ts: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Retrieve a thread of messages posted to a channel"""
+    kwargs.update({"channel": channel, "thread_ts": thread_ts})
+    return await self.api_call("channels.replies", http_verb="GET", params=kwargs)
+
+

Retrieve a thread of messages posted to a channel

+
+
+async def channels_setPurpose(self, *, channel: str, purpose: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def channels_setPurpose(
+    self,
+    *,
+    channel: str,
+    purpose: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Sets the purpose for a channel."""
+    kwargs.update({"channel": channel, "purpose": purpose})
+    kwargs = _remove_none_values(kwargs)
+    return await self.api_call("channels.setPurpose", json=kwargs)
+
+

Sets the purpose for a channel.

+
+
+async def channels_setTopic(self, *, channel: str, topic: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def channels_setTopic(
+    self,
+    *,
+    channel: str,
+    topic: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Sets the topic for a channel."""
+    kwargs.update({"channel": channel, "topic": topic})
+    kwargs = _remove_none_values(kwargs)
+    return await self.api_call("channels.setTopic", json=kwargs)
+
+

Sets the topic for a channel.

+
+
+async def channels_unarchive(self, *, channel: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def channels_unarchive(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Unarchives a channel."""
+    kwargs.update({"channel": channel})
+    kwargs = _remove_none_values(kwargs)
+    return await self.api_call("channels.unarchive", json=kwargs)
+
+

Unarchives a channel.

+
+
+async def chat_appendStream(self, *, channel: str, ts: str, markdown_text: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def chat_appendStream(
+    self,
+    *,
+    channel: str,
+    ts: str,
+    markdown_text: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Appends text to an existing streaming conversation.
+    https://docs.slack.dev/reference/methods/chat.appendStream
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "ts": ts,
+            "markdown_text": markdown_text,
+        }
+    )
+    kwargs = _remove_none_values(kwargs)
+    return await self.api_call("chat.appendStream", json=kwargs)
+
+

Appends text to an existing streaming conversation. +https://docs.slack.dev/reference/methods/chat.appendStream

+
+
+async def chat_delete(self, *, channel: str, ts: str, as_user: bool | None = None, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def chat_delete(
+    self,
+    *,
+    channel: str,
+    ts: str,
+    as_user: Optional[bool] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Deletes a message.
+    https://docs.slack.dev/reference/methods/chat.delete
+    """
+    kwargs.update({"channel": channel, "ts": ts, "as_user": as_user})
+    return await self.api_call("chat.delete", params=kwargs)
+
+ +
+
+async def chat_deleteScheduledMessage(self,
*,
channel: str,
scheduled_message_id: str,
as_user: bool | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def chat_deleteScheduledMessage(
+    self,
+    *,
+    channel: str,
+    scheduled_message_id: str,
+    as_user: Optional[bool] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Deletes a scheduled message.
+    https://docs.slack.dev/reference/methods/chat.deleteScheduledMessage
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "scheduled_message_id": scheduled_message_id,
+            "as_user": as_user,
+        }
+    )
+    return await self.api_call("chat.deleteScheduledMessage", params=kwargs)
+
+ +
+ +
+
+ +Expand source code + +
async def chat_getPermalink(
+    self,
+    *,
+    channel: str,
+    message_ts: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Retrieve a permalink URL for a specific extant message
+    https://docs.slack.dev/reference/methods/chat.getPermalink
+    """
+    kwargs.update({"channel": channel, "message_ts": message_ts})
+    return await self.api_call("chat.getPermalink", http_verb="GET", params=kwargs)
+
+

Retrieve a permalink URL for a specific extant message +https://docs.slack.dev/reference/methods/chat.getPermalink

+
+
+async def chat_meMessage(self, *, channel: str, text: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def chat_meMessage(
+    self,
+    *,
+    channel: str,
+    text: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Share a me message into a channel.
+    https://docs.slack.dev/reference/methods/chat.meMessage
+    """
+    kwargs.update({"channel": channel, "text": text})
+    return await self.api_call("chat.meMessage", params=kwargs)
+
+ +
+
+async def chat_postEphemeral(self,
*,
channel: str,
user: str,
text: str | None = None,
as_user: bool | None = None,
attachments: str | Sequence[Dict | Attachment] | None = None,
blocks: str | Sequence[Dict | Block] | None = None,
thread_ts: str | None = None,
icon_emoji: str | None = None,
icon_url: str | None = None,
link_names: bool | None = None,
username: str | None = None,
parse: str | None = None,
markdown_text: str | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def chat_postEphemeral(
+    self,
+    *,
+    channel: str,
+    user: str,
+    text: Optional[str] = None,
+    as_user: Optional[bool] = None,
+    attachments: Optional[Union[str, Sequence[Union[Dict, Attachment]]]] = None,
+    blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None,
+    thread_ts: Optional[str] = None,
+    icon_emoji: Optional[str] = None,
+    icon_url: Optional[str] = None,
+    link_names: Optional[bool] = None,
+    username: Optional[str] = None,
+    parse: Optional[str] = None,
+    markdown_text: Optional[str] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Sends an ephemeral message to a user in a channel.
+    https://docs.slack.dev/reference/methods/chat.postEphemeral
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "user": user,
+            "text": text,
+            "as_user": as_user,
+            "attachments": attachments,
+            "blocks": blocks,
+            "thread_ts": thread_ts,
+            "icon_emoji": icon_emoji,
+            "icon_url": icon_url,
+            "link_names": link_names,
+            "username": username,
+            "parse": parse,
+            "markdown_text": markdown_text,
+        }
+    )
+    _parse_web_class_objects(kwargs)
+    kwargs = _remove_none_values(kwargs)
+    _warn_if_message_text_content_is_missing("chat.postEphemeral", kwargs)
+    # NOTE: intentionally using json over params for the API methods using blocks/attachments
+    return await self.api_call("chat.postEphemeral", json=kwargs)
+
+

Sends an ephemeral message to a user in a channel. +https://docs.slack.dev/reference/methods/chat.postEphemeral

+
+
+async def chat_postMessage(self,
*,
channel: str,
text: str | None = None,
as_user: bool | None = None,
attachments: str | Sequence[Dict | Attachment] | None = None,
blocks: str | Sequence[Dict | Block] | None = None,
thread_ts: str | None = None,
reply_broadcast: bool | None = None,
unfurl_links: bool | None = None,
unfurl_media: bool | None = None,
container_id: str | None = None,
icon_emoji: str | None = None,
icon_url: str | None = None,
mrkdwn: bool | None = None,
link_names: bool | None = None,
username: str | None = None,
parse: str | None = None,
metadata: Dict | Metadata | EventAndEntityMetadata | None = None,
markdown_text: str | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def chat_postMessage(
+    self,
+    *,
+    channel: str,
+    text: Optional[str] = None,
+    as_user: Optional[bool] = None,
+    attachments: Optional[Union[str, Sequence[Union[Dict, Attachment]]]] = None,
+    blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None,
+    thread_ts: Optional[str] = None,
+    reply_broadcast: Optional[bool] = None,
+    unfurl_links: Optional[bool] = None,
+    unfurl_media: Optional[bool] = None,
+    container_id: Optional[str] = None,
+    icon_emoji: Optional[str] = None,
+    icon_url: Optional[str] = None,
+    mrkdwn: Optional[bool] = None,
+    link_names: Optional[bool] = None,
+    username: Optional[str] = None,
+    parse: Optional[str] = None,  # none, full
+    metadata: Optional[Union[Dict, Metadata, EventAndEntityMetadata]] = None,
+    markdown_text: Optional[str] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Sends a message to a channel.
+    https://docs.slack.dev/reference/methods/chat.postMessage
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "text": text,
+            "as_user": as_user,
+            "attachments": attachments,
+            "blocks": blocks,
+            "thread_ts": thread_ts,
+            "reply_broadcast": reply_broadcast,
+            "unfurl_links": unfurl_links,
+            "unfurl_media": unfurl_media,
+            "container_id": container_id,
+            "icon_emoji": icon_emoji,
+            "icon_url": icon_url,
+            "mrkdwn": mrkdwn,
+            "link_names": link_names,
+            "username": username,
+            "parse": parse,
+            "metadata": metadata,
+            "markdown_text": markdown_text,
+        }
+    )
+    _parse_web_class_objects(kwargs)
+    kwargs = _remove_none_values(kwargs)
+    _warn_if_message_text_content_is_missing("chat.postMessage", kwargs)
+    # NOTE: intentionally using json over params for the API methods using blocks/attachments
+    return await self.api_call("chat.postMessage", json=kwargs)
+
+ +
+
+async def chat_scheduleMessage(self,
*,
channel: str,
post_at: str | int,
text: str | None = None,
as_user: bool | None = None,
attachments: str | Sequence[Dict | Attachment] | None = None,
blocks: str | Sequence[Dict | Block] | None = None,
thread_ts: str | None = None,
parse: str | None = None,
reply_broadcast: bool | None = None,
unfurl_links: bool | None = None,
unfurl_media: bool | None = None,
link_names: bool | None = None,
metadata: Dict | Metadata | None = None,
markdown_text: str | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def chat_scheduleMessage(
+    self,
+    *,
+    channel: str,
+    post_at: Union[str, int],
+    text: Optional[str] = None,
+    as_user: Optional[bool] = None,
+    attachments: Optional[Union[str, Sequence[Union[Dict, Attachment]]]] = None,
+    blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None,
+    thread_ts: Optional[str] = None,
+    parse: Optional[str] = None,
+    reply_broadcast: Optional[bool] = None,
+    unfurl_links: Optional[bool] = None,
+    unfurl_media: Optional[bool] = None,
+    link_names: Optional[bool] = None,
+    metadata: Optional[Union[Dict, Metadata]] = None,
+    markdown_text: Optional[str] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Schedules a message.
+    https://docs.slack.dev/reference/methods/chat.scheduleMessage
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "post_at": post_at,
+            "text": text,
+            "as_user": as_user,
+            "attachments": attachments,
+            "blocks": blocks,
+            "thread_ts": thread_ts,
+            "reply_broadcast": reply_broadcast,
+            "parse": parse,
+            "unfurl_links": unfurl_links,
+            "unfurl_media": unfurl_media,
+            "link_names": link_names,
+            "metadata": metadata,
+            "markdown_text": markdown_text,
+        }
+    )
+    _parse_web_class_objects(kwargs)
+    kwargs = _remove_none_values(kwargs)
+    _warn_if_message_text_content_is_missing("chat.scheduleMessage", kwargs)
+    # NOTE: intentionally using json over params for the API methods using blocks/attachments
+    return await self.api_call("chat.scheduleMessage", json=kwargs)
+
+ +
+
+async def chat_scheduledMessages_list(self,
*,
channel: str | None = None,
cursor: str | None = None,
latest: str | None = None,
limit: int | None = None,
oldest: str | None = None,
team_id: str | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def chat_scheduledMessages_list(
+    self,
+    *,
+    channel: Optional[str] = None,
+    cursor: Optional[str] = None,
+    latest: Optional[str] = None,
+    limit: Optional[int] = None,
+    oldest: Optional[str] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Lists all scheduled messages.
+    https://docs.slack.dev/reference/methods/chat.scheduledMessages.list
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "cursor": cursor,
+            "latest": latest,
+            "limit": limit,
+            "oldest": oldest,
+            "team_id": team_id,
+        }
+    )
+    return await self.api_call("chat.scheduledMessages.list", params=kwargs)
+
+ +
+
+async def chat_startStream(self,
*,
channel: str,
thread_ts: str,
markdown_text: str | None = None,
recipient_team_id: str | None = None,
recipient_user_id: str | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def chat_startStream(
+    self,
+    *,
+    channel: str,
+    thread_ts: str,
+    markdown_text: Optional[str] = None,
+    recipient_team_id: Optional[str] = None,
+    recipient_user_id: Optional[str] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Starts a new streaming conversation.
+    https://docs.slack.dev/reference/methods/chat.startStream
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "thread_ts": thread_ts,
+            "markdown_text": markdown_text,
+            "recipient_team_id": recipient_team_id,
+            "recipient_user_id": recipient_user_id,
+        }
+    )
+    kwargs = _remove_none_values(kwargs)
+    return await self.api_call("chat.startStream", json=kwargs)
+
+

Starts a new streaming conversation. +https://docs.slack.dev/reference/methods/chat.startStream

+
+
+async def chat_stopStream(self,
*,
channel: str,
ts: str,
markdown_text: str | None = None,
blocks: str | Sequence[Dict | Block] | None = None,
metadata: Dict | Metadata | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def chat_stopStream(
+    self,
+    *,
+    channel: str,
+    ts: str,
+    markdown_text: Optional[str] = None,
+    blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None,
+    metadata: Optional[Union[Dict, Metadata]] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Stops a streaming conversation.
+    https://docs.slack.dev/reference/methods/chat.stopStream
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "ts": ts,
+            "markdown_text": markdown_text,
+            "blocks": blocks,
+            "metadata": metadata,
+        }
+    )
+    _parse_web_class_objects(kwargs)
+    kwargs = _remove_none_values(kwargs)
+    return await self.api_call("chat.stopStream", json=kwargs)
+
+ +
+
+async def chat_stream(self,
*,
buffer_size: int = 256,
channel: str,
thread_ts: str,
recipient_team_id: str | None = None,
recipient_user_id: str | None = None,
**kwargs) ‑> AsyncChatStream
+
+
+
+ +Expand source code + +
async def chat_stream(
+    self,
+    *,
+    buffer_size: int = 256,
+    channel: str,
+    thread_ts: str,
+    recipient_team_id: Optional[str] = None,
+    recipient_user_id: Optional[str] = None,
+    **kwargs,
+) -> AsyncChatStream:
+    """Stream markdown text into a conversation.
+
+    This method starts a new chat stream in a conversation that can be appended to. After appending an entire message,
+    the stream can be stopped with concluding arguments such as "blocks" for gathering feedback.
+
+    The following methods are used:
+
+    - chat.startStream: Starts a new streaming conversation.
+      [Reference](https://docs.slack.dev/reference/methods/chat.startStream).
+    - chat.appendStream: Appends text to an existing streaming conversation.
+      [Reference](https://docs.slack.dev/reference/methods/chat.appendStream).
+    - chat.stopStream: Stops a streaming conversation.
+      [Reference](https://docs.slack.dev/reference/methods/chat.stopStream).
+
+    Args:
+        buffer_size: The length of markdown_text to buffer in-memory before calling a stream method. Increasing this
+          value decreases the number of method calls made for the same amount of text, which is useful to avoid rate
+          limits. Default: 256.
+        channel: An encoded ID that represents a channel, private group, or DM.
+        thread_ts: Provide another message's ts value to reply to. Streamed messages should always be replies to a user
+          request.
+        recipient_team_id: The encoded ID of the team the user receiving the streaming text belongs to. Required when
+          streaming to channels.
+        recipient_user_id: The encoded ID of the user to receive the streaming text. Required when streaming to channels.
+        **kwargs: Additional arguments passed to the underlying API calls.
+
+    Returns:
+        ChatStream instance for managing the stream
+
+    Example:
+        ```python
+        streamer = await client.chat_stream(
+            channel="C0123456789",
+            thread_ts="1700000001.123456",
+            recipient_team_id="T0123456789",
+            recipient_user_id="U0123456789",
+        )
+        await streamer.append(markdown_text="**hello wo")
+        await streamer.append(markdown_text="rld!**")
+        await streamer.stop()
+        ```
+    """
+    return AsyncChatStream(
+        self,
+        logger=self._logger,
+        channel=channel,
+        thread_ts=thread_ts,
+        recipient_team_id=recipient_team_id,
+        recipient_user_id=recipient_user_id,
+        buffer_size=buffer_size,
+        **kwargs,
+    )
+
+

Stream markdown text into a conversation.

+

This method starts a new chat stream in a conversation that can be appended to. After appending an entire message, +the stream can be stopped with concluding arguments such as "blocks" for gathering feedback.

+

The following methods are used:

+
    +
  • chat.startStream: Starts a new streaming conversation. +Reference.
  • +
  • chat.appendStream: Appends text to an existing streaming conversation. +Reference.
  • +
  • chat.stopStream: Stops a streaming conversation. +Reference.
  • +
+

Args

+
+
buffer_size
+
The length of markdown_text to buffer in-memory before calling a stream method. Increasing this +value decreases the number of method calls made for the same amount of text, which is useful to avoid rate +limits. Default: 256.
+
channel
+
An encoded ID that represents a channel, private group, or DM.
+
thread_ts
+
Provide another message's ts value to reply to. Streamed messages should always be replies to a user +request.
+
recipient_team_id
+
The encoded ID of the team the user receiving the streaming text belongs to. Required when +streaming to channels.
+
recipient_user_id
+
The encoded ID of the user to receive the streaming text. Required when streaming to channels.
+
**kwargs
+
Additional arguments passed to the underlying API calls.
+
+

Returns

+

ChatStream instance for managing the stream

+

Example

+
streamer = await client.chat_stream(
+    channel="C0123456789",
+    thread_ts="1700000001.123456",
+    recipient_team_id="T0123456789",
+    recipient_user_id="U0123456789",
+)
+await streamer.append(markdown_text="**hello wo")
+await streamer.append(markdown_text="rld!**")
+await streamer.stop()
+
+
+
+async def chat_unfurl(self,
*,
channel: str | None = None,
ts: str | None = None,
source: str | None = None,
unfurl_id: str | None = None,
unfurls: Dict[str, Dict] | None = None,
metadata: Dict | EventAndEntityMetadata | None = None,
user_auth_blocks: str | Sequence[Dict | Block] | None = None,
user_auth_message: str | None = None,
user_auth_required: bool | None = None,
user_auth_url: str | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def chat_unfurl(
+    self,
+    *,
+    channel: Optional[str] = None,
+    ts: Optional[str] = None,
+    source: Optional[str] = None,
+    unfurl_id: Optional[str] = None,
+    unfurls: Optional[Dict[str, Dict]] = None,  # or user_auth_*
+    metadata: Optional[Union[Dict, EventAndEntityMetadata]] = None,
+    user_auth_blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None,
+    user_auth_message: Optional[str] = None,
+    user_auth_required: Optional[bool] = None,
+    user_auth_url: Optional[str] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Provide custom unfurl behavior for user-posted URLs.
+    https://docs.slack.dev/reference/methods/chat.unfurl
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "ts": ts,
+            "source": source,
+            "unfurl_id": unfurl_id,
+            "unfurls": unfurls,
+            "metadata": metadata,
+            "user_auth_blocks": user_auth_blocks,
+            "user_auth_message": user_auth_message,
+            "user_auth_required": user_auth_required,
+            "user_auth_url": user_auth_url,
+        }
+    )
+    _parse_web_class_objects(kwargs)  # for user_auth_blocks
+    kwargs = _remove_none_values(kwargs)
+    # NOTE: intentionally using json over params for API methods using blocks/attachments
+    return await self.api_call("chat.unfurl", json=kwargs)
+
+

Provide custom unfurl behavior for user-posted URLs. +https://docs.slack.dev/reference/methods/chat.unfurl

+
+
+async def chat_update(self,
*,
channel: str,
ts: str,
text: str | None = None,
attachments: str | Sequence[Dict | Attachment] | None = None,
blocks: str | Sequence[Dict | Block] | None = None,
as_user: bool | None = None,
file_ids: str | Sequence[str] | None = None,
link_names: bool | None = None,
parse: str | None = None,
reply_broadcast: bool | None = None,
metadata: Dict | Metadata | None = None,
markdown_text: str | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def chat_update(
+    self,
+    *,
+    channel: str,
+    ts: str,
+    text: Optional[str] = None,
+    attachments: Optional[Union[str, Sequence[Union[Dict, Attachment]]]] = None,
+    blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None,
+    as_user: Optional[bool] = None,
+    file_ids: Optional[Union[str, Sequence[str]]] = None,
+    link_names: Optional[bool] = None,
+    parse: Optional[str] = None,  # none, full
+    reply_broadcast: Optional[bool] = None,
+    metadata: Optional[Union[Dict, Metadata]] = None,
+    markdown_text: Optional[str] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Updates a message in a channel.
+    https://docs.slack.dev/reference/methods/chat.update
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "ts": ts,
+            "text": text,
+            "attachments": attachments,
+            "blocks": blocks,
+            "as_user": as_user,
+            "link_names": link_names,
+            "parse": parse,
+            "reply_broadcast": reply_broadcast,
+            "metadata": metadata,
+            "markdown_text": markdown_text,
+        }
+    )
+    if isinstance(file_ids, (list, tuple)):
+        kwargs.update({"file_ids": ",".join(file_ids)})
+    else:
+        kwargs.update({"file_ids": file_ids})
+    _parse_web_class_objects(kwargs)
+    kwargs = _remove_none_values(kwargs)
+    _warn_if_message_text_content_is_missing("chat.update", kwargs)
+    # NOTE: intentionally using json over params for API methods using blocks/attachments
+    return await self.api_call("chat.update", json=kwargs)
+
+ +
+
+async def conversations_acceptSharedInvite(self,
*,
channel_name: str,
channel_id: str | None = None,
invite_id: str | None = None,
free_trial_accepted: bool | None = None,
is_private: bool | None = None,
team_id: str | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def conversations_acceptSharedInvite(
+    self,
+    *,
+    channel_name: str,
+    channel_id: Optional[str] = None,
+    invite_id: Optional[str] = None,
+    free_trial_accepted: Optional[bool] = None,
+    is_private: Optional[bool] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Accepts an invitation to a Slack Connect channel.
+    https://docs.slack.dev/reference/methods/conversations.acceptSharedInvite
+    """
+    if channel_id is None and invite_id is None:
+        raise e.SlackRequestError("Either channel_id or invite_id must be provided.")
+    kwargs.update(
+        {
+            "channel_name": channel_name,
+            "channel_id": channel_id,
+            "invite_id": invite_id,
+            "free_trial_accepted": free_trial_accepted,
+            "is_private": is_private,
+            "team_id": team_id,
+        }
+    )
+    return await self.api_call("conversations.acceptSharedInvite", http_verb="POST", params=kwargs)
+
+

Accepts an invitation to a Slack Connect channel. +https://docs.slack.dev/reference/methods/conversations.acceptSharedInvite

+
+
+async def conversations_approveSharedInvite(self, *, invite_id: str, target_team: str | None = None, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def conversations_approveSharedInvite(
+    self,
+    *,
+    invite_id: str,
+    target_team: Optional[str] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Approves an invitation to a Slack Connect channel.
+    https://docs.slack.dev/reference/methods/conversations.approveSharedInvite
+    """
+    kwargs.update({"invite_id": invite_id, "target_team": target_team})
+    return await self.api_call("conversations.approveSharedInvite", http_verb="POST", params=kwargs)
+
+

Approves an invitation to a Slack Connect channel. +https://docs.slack.dev/reference/methods/conversations.approveSharedInvite

+
+
+async def conversations_archive(self, *, channel: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def conversations_archive(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Archives a conversation.
+    https://docs.slack.dev/reference/methods/conversations.archive
+    """
+    kwargs.update({"channel": channel})
+    return await self.api_call("conversations.archive", params=kwargs)
+
+ +
+
+async def conversations_canvases_create(self, *, channel_id: str, document_content: Dict[str, str], **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def conversations_canvases_create(
+    self,
+    *,
+    channel_id: str,
+    document_content: Dict[str, str],
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Create a Channel Canvas for a channel
+    https://docs.slack.dev/reference/methods/conversations.canvases.create
+    """
+    kwargs.update({"channel_id": channel_id, "document_content": document_content})
+    return await self.api_call("conversations.canvases.create", json=kwargs)
+
+ +
+
+async def conversations_close(self, *, channel: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def conversations_close(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Closes a direct message or multi-person direct message.
+    https://docs.slack.dev/reference/methods/conversations.close
+    """
+    kwargs.update({"channel": channel})
+    return await self.api_call("conversations.close", params=kwargs)
+
+

Closes a direct message or multi-person direct message. +https://docs.slack.dev/reference/methods/conversations.close

+
+
+async def conversations_create(self,
*,
name: str,
is_private: bool | None = None,
team_id: str | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def conversations_create(
+    self,
+    *,
+    name: str,
+    is_private: Optional[bool] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Initiates a public or private channel-based conversation
+    https://docs.slack.dev/reference/methods/conversations.create
+    """
+    kwargs.update({"name": name, "is_private": is_private, "team_id": team_id})
+    return await self.api_call("conversations.create", params=kwargs)
+
+

Initiates a public or private channel-based conversation +https://docs.slack.dev/reference/methods/conversations.create

+
+
+async def conversations_declineSharedInvite(self, *, invite_id: str, target_team: str | None = None, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def conversations_declineSharedInvite(
+    self,
+    *,
+    invite_id: str,
+    target_team: Optional[str] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Declines a Slack Connect channel invite.
+    https://docs.slack.dev/reference/methods/conversations.declineSharedInvite
+    """
+    kwargs.update({"invite_id": invite_id, "target_team": target_team})
+    return await self.api_call("conversations.declineSharedInvite", http_verb="GET", params=kwargs)
+
+ +
+
+async def conversations_externalInvitePermissions_set(self, *, action: str, channel: str, target_team: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def conversations_externalInvitePermissions_set(
+    self, *, action: str, channel: str, target_team: str, **kwargs
+) -> AsyncSlackResponse:
+    """Sets a team in a shared External Limited channel to a shared Slack Connect channel or vice versa.
+    https://docs.slack.dev/reference/methods/conversations.externalInvitePermissions.set
+    """
+    kwargs.update(
+        {
+            "action": action,
+            "channel": channel,
+            "target_team": target_team,
+        }
+    )
+    return await self.api_call("conversations.externalInvitePermissions.set", params=kwargs)
+
+

Sets a team in a shared External Limited channel to a shared Slack Connect channel or vice versa. +https://docs.slack.dev/reference/methods/conversations.externalInvitePermissions.set

+
+
+async def conversations_history(self,
*,
channel: str,
cursor: str | None = None,
inclusive: bool | None = None,
include_all_metadata: bool | None = None,
latest: str | None = None,
limit: int | None = None,
oldest: str | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def conversations_history(
+    self,
+    *,
+    channel: str,
+    cursor: Optional[str] = None,
+    inclusive: Optional[bool] = None,
+    include_all_metadata: Optional[bool] = None,
+    latest: Optional[str] = None,
+    limit: Optional[int] = None,
+    oldest: Optional[str] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Fetches a conversation's history of messages and events.
+    https://docs.slack.dev/reference/methods/conversations.history
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "cursor": cursor,
+            "inclusive": inclusive,
+            "include_all_metadata": include_all_metadata,
+            "limit": limit,
+            "latest": latest,
+            "oldest": oldest,
+        }
+    )
+    return await self.api_call("conversations.history", http_verb="GET", params=kwargs)
+
+

Fetches a conversation's history of messages and events. +https://docs.slack.dev/reference/methods/conversations.history

+
+
+async def conversations_info(self,
*,
channel: str,
include_locale: bool | None = None,
include_num_members: bool | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def conversations_info(
+    self,
+    *,
+    channel: str,
+    include_locale: Optional[bool] = None,
+    include_num_members: Optional[bool] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Retrieve information about a conversation.
+    https://docs.slack.dev/reference/methods/conversations.info
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "include_locale": include_locale,
+            "include_num_members": include_num_members,
+        }
+    )
+    return await self.api_call("conversations.info", http_verb="GET", params=kwargs)
+
+

Retrieve information about a conversation. +https://docs.slack.dev/reference/methods/conversations.info

+
+
+async def conversations_invite(self,
*,
channel: str,
users: str | Sequence[str],
force: bool | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def conversations_invite(
+    self,
+    *,
+    channel: str,
+    users: Union[str, Sequence[str]],
+    force: Optional[bool] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Invites users to a channel.
+    https://docs.slack.dev/reference/methods/conversations.invite
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "force": force,
+        }
+    )
+    if isinstance(users, (list, tuple)):
+        kwargs.update({"users": ",".join(users)})
+    else:
+        kwargs.update({"users": users})
+    return await self.api_call("conversations.invite", params=kwargs)
+
+ +
+
+async def conversations_inviteShared(self,
*,
channel: str,
emails: str | Sequence[str] | None = None,
user_ids: str | Sequence[str] | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def conversations_inviteShared(
+    self,
+    *,
+    channel: str,
+    emails: Optional[Union[str, Sequence[str]]] = None,
+    user_ids: Optional[Union[str, Sequence[str]]] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Sends an invitation to a Slack Connect channel.
+    https://docs.slack.dev/reference/methods/conversations.inviteShared
+    """
+    if emails is None and user_ids is None:
+        raise e.SlackRequestError("Either emails or user ids must be provided.")
+    kwargs.update({"channel": channel})
+    if isinstance(emails, (list, tuple)):
+        kwargs.update({"emails": ",".join(emails)})
+    else:
+        kwargs.update({"emails": emails})
+    if isinstance(user_ids, (list, tuple)):
+        kwargs.update({"user_ids": ",".join(user_ids)})
+    else:
+        kwargs.update({"user_ids": user_ids})
+    return await self.api_call("conversations.inviteShared", http_verb="GET", params=kwargs)
+
+

Sends an invitation to a Slack Connect channel. +https://docs.slack.dev/reference/methods/conversations.inviteShared

+
+
+async def conversations_join(self, *, channel: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def conversations_join(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Joins an existing conversation.
+    https://docs.slack.dev/reference/methods/conversations.join
+    """
+    kwargs.update({"channel": channel})
+    return await self.api_call("conversations.join", params=kwargs)
+
+ +
+
+async def conversations_kick(self, *, channel: str, user: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def conversations_kick(
+    self,
+    *,
+    channel: str,
+    user: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Removes a user from a conversation.
+    https://docs.slack.dev/reference/methods/conversations.kick
+    """
+    kwargs.update({"channel": channel, "user": user})
+    return await self.api_call("conversations.kick", params=kwargs)
+
+ +
+
+async def conversations_leave(self, *, channel: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def conversations_leave(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Leaves a conversation.
+    https://docs.slack.dev/reference/methods/conversations.leave
+    """
+    kwargs.update({"channel": channel})
+    return await self.api_call("conversations.leave", params=kwargs)
+
+ +
+
+async def conversations_list(self,
*,
cursor: str | None = None,
exclude_archived: bool | None = None,
limit: int | None = None,
team_id: str | None = None,
types: str | Sequence[str] | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def conversations_list(
+    self,
+    *,
+    cursor: Optional[str] = None,
+    exclude_archived: Optional[bool] = None,
+    limit: Optional[int] = None,
+    team_id: Optional[str] = None,
+    types: Optional[Union[str, Sequence[str]]] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Lists all channels in a Slack team.
+    https://docs.slack.dev/reference/methods/conversations.list
+    """
+    kwargs.update(
+        {
+            "cursor": cursor,
+            "exclude_archived": exclude_archived,
+            "limit": limit,
+            "team_id": team_id,
+        }
+    )
+    if isinstance(types, (list, tuple)):
+        kwargs.update({"types": ",".join(types)})
+    else:
+        kwargs.update({"types": types})
+    return await self.api_call("conversations.list", http_verb="GET", params=kwargs)
+
+ +
+
+async def conversations_listConnectInvites(self,
*,
count: int | None = None,
cursor: str | None = None,
team_id: str | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def conversations_listConnectInvites(
+    self,
+    *,
+    count: Optional[int] = None,
+    cursor: Optional[str] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """List shared channel invites that have been generated
+    or received but have not yet been approved by all parties.
+    https://docs.slack.dev/reference/methods/conversations.listConnectInvites
+    """
+    kwargs.update({"count": count, "cursor": cursor, "team_id": team_id})
+    return await self.api_call("conversations.listConnectInvites", params=kwargs)
+
+

List shared channel invites that have been generated +or received but have not yet been approved by all parties. +https://docs.slack.dev/reference/methods/conversations.listConnectInvites

+
+
+async def conversations_mark(self, *, channel: str, ts: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def conversations_mark(
+    self,
+    *,
+    channel: str,
+    ts: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Sets the read cursor in a channel.
+    https://docs.slack.dev/reference/methods/conversations.mark
+    """
+    kwargs.update({"channel": channel, "ts": ts})
+    return await self.api_call("conversations.mark", params=kwargs)
+
+ +
+
+async def conversations_members(self, *, channel: str, cursor: str | None = None, limit: int | None = None, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def conversations_members(
+    self,
+    *,
+    channel: str,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Retrieve members of a conversation.
+    https://docs.slack.dev/reference/methods/conversations.members
+    """
+    kwargs.update({"channel": channel, "cursor": cursor, "limit": limit})
+    return await self.api_call("conversations.members", http_verb="GET", params=kwargs)
+
+ +
+
+async def conversations_open(self,
*,
channel: str | None = None,
return_im: bool | None = None,
users: str | Sequence[str] | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def conversations_open(
+    self,
+    *,
+    channel: Optional[str] = None,
+    return_im: Optional[bool] = None,
+    users: Optional[Union[str, Sequence[str]]] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Opens or resumes a direct message or multi-person direct message.
+    https://docs.slack.dev/reference/methods/conversations.open
+    """
+    if channel is None and users is None:
+        raise e.SlackRequestError("Either channel or users must be provided.")
+    kwargs.update({"channel": channel, "return_im": return_im})
+    if isinstance(users, (list, tuple)):
+        kwargs.update({"users": ",".join(users)})
+    else:
+        kwargs.update({"users": users})
+    return await self.api_call("conversations.open", params=kwargs)
+
+

Opens or resumes a direct message or multi-person direct message. +https://docs.slack.dev/reference/methods/conversations.open

+
+
+async def conversations_rename(self, *, channel: str, name: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def conversations_rename(
+    self,
+    *,
+    channel: str,
+    name: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Renames a conversation.
+    https://docs.slack.dev/reference/methods/conversations.rename
+    """
+    kwargs.update({"channel": channel, "name": name})
+    return await self.api_call("conversations.rename", params=kwargs)
+
+ +
+
+async def conversations_replies(self,
*,
channel: str,
ts: str,
cursor: str | None = None,
inclusive: bool | None = None,
include_all_metadata: bool | None = None,
latest: str | None = None,
limit: int | None = None,
oldest: str | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def conversations_replies(
+    self,
+    *,
+    channel: str,
+    ts: str,
+    cursor: Optional[str] = None,
+    inclusive: Optional[bool] = None,
+    include_all_metadata: Optional[bool] = None,
+    latest: Optional[str] = None,
+    limit: Optional[int] = None,
+    oldest: Optional[str] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Retrieve a thread of messages posted to a conversation
+    https://docs.slack.dev/reference/methods/conversations.replies
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "ts": ts,
+            "cursor": cursor,
+            "inclusive": inclusive,
+            "include_all_metadata": include_all_metadata,
+            "limit": limit,
+            "latest": latest,
+            "oldest": oldest,
+        }
+    )
+    return await self.api_call("conversations.replies", http_verb="GET", params=kwargs)
+
+

Retrieve a thread of messages posted to a conversation +https://docs.slack.dev/reference/methods/conversations.replies

+
+
+async def conversations_requestSharedInvite_approve(self,
*,
invite_id: str,
channel_id: str | None = None,
is_external_limited: str | None = None,
message: Dict[str, Any] | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def conversations_requestSharedInvite_approve(
+    self,
+    *,
+    invite_id: str,
+    channel_id: Optional[str] = None,
+    is_external_limited: Optional[str] = None,
+    message: Optional[Dict[str, Any]] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Approve a request to add an external user to a channel. This also sends them a Slack Connect invite.
+    https://docs.slack.dev/reference/methods/conversations.requestSharedInvite.approve
+    """
+    kwargs.update(
+        {
+            "invite_id": invite_id,
+            "channel_id": channel_id,
+            "is_external_limited": is_external_limited,
+        }
+    )
+    if message is not None:
+        kwargs.update({"message": json.dumps(message)})
+    return await self.api_call("conversations.requestSharedInvite.approve", params=kwargs)
+
+

Approve a request to add an external user to a channel. This also sends them a Slack Connect invite. +https://docs.slack.dev/reference/methods/conversations.requestSharedInvite.approve

+
+
+async def conversations_requestSharedInvite_deny(self, *, invite_id: str, message: str | None = None, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def conversations_requestSharedInvite_deny(
+    self,
+    *,
+    invite_id: str,
+    message: Optional[str] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Deny a request to invite an external user to a channel.
+    https://docs.slack.dev/reference/methods/conversations.requestSharedInvite.deny
+    """
+    kwargs.update({"invite_id": invite_id, "message": message})
+    return await self.api_call("conversations.requestSharedInvite.deny", params=kwargs)
+
+

Deny a request to invite an external user to a channel. +https://docs.slack.dev/reference/methods/conversations.requestSharedInvite.deny

+
+
+async def conversations_requestSharedInvite_list(self,
*,
cursor: str | None = None,
include_approved: bool | None = None,
include_denied: bool | None = None,
include_expired: bool | None = None,
invite_ids: str | Sequence[str] | None = None,
limit: int | None = None,
user_id: str | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def conversations_requestSharedInvite_list(
+    self,
+    *,
+    cursor: Optional[str] = None,
+    include_approved: Optional[bool] = None,
+    include_denied: Optional[bool] = None,
+    include_expired: Optional[bool] = None,
+    invite_ids: Optional[Union[str, Sequence[str]]] = None,
+    limit: Optional[int] = None,
+    user_id: Optional[str] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Lists requests to add external users to channels with ability to filter.
+    https://docs.slack.dev/reference/methods/conversations.requestSharedInvite.list
+    """
+    kwargs.update(
+        {
+            "cursor": cursor,
+            "include_approved": include_approved,
+            "include_denied": include_denied,
+            "include_expired": include_expired,
+            "limit": limit,
+            "user_id": user_id,
+        }
+    )
+    if invite_ids is not None:
+        if isinstance(invite_ids, (list, tuple)):
+            kwargs.update({"invite_ids": ",".join(invite_ids)})
+        else:
+            kwargs.update({"invite_ids": invite_ids})
+    return await self.api_call("conversations.requestSharedInvite.list", params=kwargs)
+
+

Lists requests to add external users to channels with ability to filter. +https://docs.slack.dev/reference/methods/conversations.requestSharedInvite.list

+
+
+async def conversations_setPurpose(self, *, channel: str, purpose: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def conversations_setPurpose(
+    self,
+    *,
+    channel: str,
+    purpose: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Sets the purpose for a conversation.
+    https://docs.slack.dev/reference/methods/conversations.setPurpose
+    """
+    kwargs.update({"channel": channel, "purpose": purpose})
+    return await self.api_call("conversations.setPurpose", params=kwargs)
+
+ +
+
+async def conversations_setTopic(self, *, channel: str, topic: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def conversations_setTopic(
+    self,
+    *,
+    channel: str,
+    topic: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Sets the topic for a conversation.
+    https://docs.slack.dev/reference/methods/conversations.setTopic
+    """
+    kwargs.update({"channel": channel, "topic": topic})
+    return await self.api_call("conversations.setTopic", params=kwargs)
+
+ +
+
+async def conversations_unarchive(self, *, channel: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def conversations_unarchive(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Reverses conversation archival.
+    https://docs.slack.dev/reference/methods/conversations.unarchive
+    """
+    kwargs.update({"channel": channel})
+    return await self.api_call("conversations.unarchive", params=kwargs)
+
+ +
+
+async def dialog_open(self, *, dialog: Dict[str, Any], trigger_id: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def dialog_open(
+    self,
+    *,
+    dialog: Dict[str, Any],
+    trigger_id: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Open a dialog with a user.
+    https://docs.slack.dev/reference/methods/dialog.open
+    """
+    kwargs.update({"dialog": dialog, "trigger_id": trigger_id})
+    kwargs = _remove_none_values(kwargs)
+    # NOTE: As the dialog can be a dict, this API call works only with json format.
+    return await self.api_call("dialog.open", json=kwargs)
+
+ +
+
+async def dnd_endDnd(self, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def dnd_endDnd(
+    self,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Ends the current user's Do Not Disturb session immediately.
+    https://docs.slack.dev/reference/methods/dnd.endDnd
+    """
+    return await self.api_call("dnd.endDnd", params=kwargs)
+
+

Ends the current user's Do Not Disturb session immediately. +https://docs.slack.dev/reference/methods/dnd.endDnd

+
+
+async def dnd_endSnooze(self, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def dnd_endSnooze(
+    self,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Ends the current user's snooze mode immediately.
+    https://docs.slack.dev/reference/methods/dnd.endSnooze
+    """
+    return await self.api_call("dnd.endSnooze", params=kwargs)
+
+

Ends the current user's snooze mode immediately. +https://docs.slack.dev/reference/methods/dnd.endSnooze

+
+
+async def dnd_info(self, *, team_id: str | None = None, user: str | None = None, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def dnd_info(
+    self,
+    *,
+    team_id: Optional[str] = None,
+    user: Optional[str] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Retrieves a user's current Do Not Disturb status.
+    https://docs.slack.dev/reference/methods/dnd.info
+    """
+    kwargs.update({"team_id": team_id, "user": user})
+    return await self.api_call("dnd.info", http_verb="GET", params=kwargs)
+
+

Retrieves a user's current Do Not Disturb status. +https://docs.slack.dev/reference/methods/dnd.info

+
+
+async def dnd_setSnooze(self, *, num_minutes: str | int, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def dnd_setSnooze(
+    self,
+    *,
+    num_minutes: Union[int, str],
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Turns on Do Not Disturb mode for the current user, or changes its duration.
+    https://docs.slack.dev/reference/methods/dnd.setSnooze
+    """
+    kwargs.update({"num_minutes": num_minutes})
+    return await self.api_call("dnd.setSnooze", http_verb="GET", params=kwargs)
+
+

Turns on Do Not Disturb mode for the current user, or changes its duration. +https://docs.slack.dev/reference/methods/dnd.setSnooze

+
+
+async def dnd_teamInfo(self, users: str | Sequence[str], team_id: str | None = None, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def dnd_teamInfo(
+    self,
+    users: Union[str, Sequence[str]],
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Retrieves the Do Not Disturb status for users on a team.
+    https://docs.slack.dev/reference/methods/dnd.teamInfo
+    """
+    if isinstance(users, (list, tuple)):
+        kwargs.update({"users": ",".join(users)})
+    else:
+        kwargs.update({"users": users})
+    kwargs.update({"team_id": team_id})
+    return await self.api_call("dnd.teamInfo", http_verb="GET", params=kwargs)
+
+

Retrieves the Do Not Disturb status for users on a team. +https://docs.slack.dev/reference/methods/dnd.teamInfo

+
+
+async def emoji_list(self, include_categories: bool | None = None, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def emoji_list(
+    self,
+    include_categories: Optional[bool] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Lists custom emoji for a team.
+    https://docs.slack.dev/reference/methods/emoji.list
+    """
+    kwargs.update({"include_categories": include_categories})
+    return await self.api_call("emoji.list", http_verb="GET", params=kwargs)
+
+ +
+
+async def entity_presentDetails(self,
trigger_id: str,
metadata: Dict | EntityMetadata | None = None,
user_auth_required: bool | None = None,
user_auth_url: str | None = None,
error: Dict[str, Any] | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def entity_presentDetails(
+    self,
+    trigger_id: str,
+    metadata: Optional[Union[Dict, EntityMetadata]] = None,
+    user_auth_required: Optional[bool] = None,
+    user_auth_url: Optional[str] = None,
+    error: Optional[Dict[str, Any]] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Provides entity details for the flexpane.
+    https://docs.slack.dev/reference/methods/entity.presentDetails/
+    """
+    kwargs.update({"trigger_id": trigger_id})
+    if metadata is not None:
+        kwargs.update({"metadata": metadata})
+    if user_auth_required is not None:
+        kwargs.update({"user_auth_required": user_auth_required})
+    if user_auth_url is not None:
+        kwargs.update({"user_auth_url": user_auth_url})
+    if error is not None:
+        kwargs.update({"error": error})
+    _parse_web_class_objects(kwargs)
+    return await self.api_call("entity.presentDetails", json=kwargs)
+
+

Provides entity details for the flexpane. +https://docs.slack.dev/reference/methods/entity.presentDetails/

+
+
+async def files_comments_delete(self, *, file: str, id: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def files_comments_delete(
+    self,
+    *,
+    file: str,
+    id: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Deletes an existing comment on a file.
+    https://docs.slack.dev/reference/methods/files.comments.delete
+    """
+    kwargs.update({"file": file, "id": id})
+    return await self.api_call("files.comments.delete", params=kwargs)
+
+ +
+
+async def files_completeUploadExternal(self,
*,
files: List[Dict[str, str]],
channel_id: str | None = None,
channels: List[str] | None = None,
initial_comment: str | None = None,
thread_ts: str | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def files_completeUploadExternal(
+    self,
+    *,
+    files: List[Dict[str, str]],
+    channel_id: Optional[str] = None,
+    channels: Optional[List[str]] = None,
+    initial_comment: Optional[str] = None,
+    thread_ts: Optional[str] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Finishes an upload started with files.getUploadURLExternal.
+    https://docs.slack.dev/reference/methods/files.completeUploadExternal
+    """
+    _files = [{k: v for k, v in f.items() if v is not None} for f in files]
+    kwargs.update(
+        {
+            "files": json.dumps(_files),
+            "channel_id": channel_id,
+            "initial_comment": initial_comment,
+            "thread_ts": thread_ts,
+        }
+    )
+    if channels:
+        kwargs["channels"] = ",".join(channels)
+    return await self.api_call("files.completeUploadExternal", params=kwargs)
+
+

Finishes an upload started with files.getUploadURLExternal. +https://docs.slack.dev/reference/methods/files.completeUploadExternal

+
+
+async def files_delete(self, *, file: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def files_delete(
+    self,
+    *,
+    file: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Deletes a file.
+    https://docs.slack.dev/reference/methods/files.delete
+    """
+    kwargs.update({"file": file})
+    return await self.api_call("files.delete", params=kwargs)
+
+ +
+
+async def files_getUploadURLExternal(self,
*,
filename: str,
length: int,
alt_txt: str | None = None,
snippet_type: str | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def files_getUploadURLExternal(
+    self,
+    *,
+    filename: str,
+    length: int,
+    alt_txt: Optional[str] = None,
+    snippet_type: Optional[str] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Gets a URL for an edge external upload.
+    https://docs.slack.dev/reference/methods/files.getUploadURLExternal
+    """
+    kwargs.update(
+        {
+            "filename": filename,
+            "length": length,
+            "alt_txt": alt_txt,
+            "snippet_type": snippet_type,
+        }
+    )
+    return await self.api_call("files.getUploadURLExternal", params=kwargs)
+
+ +
+
+async def files_info(self,
*,
file: str,
count: int | None = None,
cursor: str | None = None,
limit: int | None = None,
page: int | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def files_info(
+    self,
+    *,
+    file: str,
+    count: Optional[int] = None,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    page: Optional[int] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Gets information about a team file.
+    https://docs.slack.dev/reference/methods/files.info
+    """
+    kwargs.update(
+        {
+            "file": file,
+            "count": count,
+            "cursor": cursor,
+            "limit": limit,
+            "page": page,
+        }
+    )
+    return await self.api_call("files.info", http_verb="GET", params=kwargs)
+
+

Gets information about a team file. +https://docs.slack.dev/reference/methods/files.info

+
+
+async def files_list(self,
*,
channel: str | None = None,
count: int | None = None,
page: int | None = None,
show_files_hidden_by_limit: bool | None = None,
team_id: str | None = None,
ts_from: str | None = None,
ts_to: str | None = None,
types: str | Sequence[str] | None = None,
user: str | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def files_list(
+    self,
+    *,
+    channel: Optional[str] = None,
+    count: Optional[int] = None,
+    page: Optional[int] = None,
+    show_files_hidden_by_limit: Optional[bool] = None,
+    team_id: Optional[str] = None,
+    ts_from: Optional[str] = None,
+    ts_to: Optional[str] = None,
+    types: Optional[Union[str, Sequence[str]]] = None,
+    user: Optional[str] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Lists & filters team files.
+    https://docs.slack.dev/reference/methods/files.list
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "count": count,
+            "page": page,
+            "show_files_hidden_by_limit": show_files_hidden_by_limit,
+            "team_id": team_id,
+            "ts_from": ts_from,
+            "ts_to": ts_to,
+            "user": user,
+        }
+    )
+    if isinstance(types, (list, tuple)):
+        kwargs.update({"types": ",".join(types)})
+    else:
+        kwargs.update({"types": types})
+    return await self.api_call("files.list", http_verb="GET", params=kwargs)
+
+ +
+
+async def files_remote_add(self,
*,
external_id: str,
external_url: str,
title: str,
filetype: str | None = None,
indexable_file_contents: str | bytes | io.IOBase | None = None,
preview_image: str | bytes | io.IOBase | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def files_remote_add(
+    self,
+    *,
+    external_id: str,
+    external_url: str,
+    title: str,
+    filetype: Optional[str] = None,
+    indexable_file_contents: Optional[Union[str, bytes, IOBase]] = None,
+    preview_image: Optional[Union[str, bytes, IOBase]] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Adds a file from a remote service.
+    https://docs.slack.dev/reference/methods/files.remote.add
+    """
+    kwargs.update(
+        {
+            "external_id": external_id,
+            "external_url": external_url,
+            "title": title,
+            "filetype": filetype,
+        }
+    )
+    files = None
+    # preview_image (file): Preview of the document via multipart/form-data.
+    if preview_image is not None or indexable_file_contents is not None:
+        files = {
+            "preview_image": preview_image,
+            "indexable_file_contents": indexable_file_contents,
+        }
+
+    return await self.api_call(
+        # Intentionally using "POST" method over "GET" here
+        "files.remote.add",
+        http_verb="POST",
+        data=kwargs,
+        files=files,
+    )
+
+ +
+
+async def files_remote_info(self, *, external_id: str | None = None, file: str | None = None, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def files_remote_info(
+    self,
+    *,
+    external_id: Optional[str] = None,
+    file: Optional[str] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Retrieve information about a remote file added to Slack.
+    https://docs.slack.dev/reference/methods/files.remote.info
+    """
+    kwargs.update({"external_id": external_id, "file": file})
+    return await self.api_call("files.remote.info", http_verb="GET", params=kwargs)
+
+

Retrieve information about a remote file added to Slack. +https://docs.slack.dev/reference/methods/files.remote.info

+
+
+async def files_remote_list(self,
*,
channel: str | None = None,
cursor: str | None = None,
limit: int | None = None,
ts_from: str | None = None,
ts_to: str | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def files_remote_list(
+    self,
+    *,
+    channel: Optional[str] = None,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    ts_from: Optional[str] = None,
+    ts_to: Optional[str] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Retrieve information about a remote file added to Slack.
+    https://docs.slack.dev/reference/methods/files.remote.list
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "cursor": cursor,
+            "limit": limit,
+            "ts_from": ts_from,
+            "ts_to": ts_to,
+        }
+    )
+    return await self.api_call("files.remote.list", http_verb="GET", params=kwargs)
+
+

Retrieve information about a remote file added to Slack. +https://docs.slack.dev/reference/methods/files.remote.list

+
+
+async def files_remote_remove(self, *, external_id: str | None = None, file: str | None = None, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def files_remote_remove(
+    self,
+    *,
+    external_id: Optional[str] = None,
+    file: Optional[str] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Remove a remote file.
+    https://docs.slack.dev/reference/methods/files.remote.remove
+    """
+    kwargs.update({"external_id": external_id, "file": file})
+    return await self.api_call("files.remote.remove", http_verb="POST", params=kwargs)
+
+ +
+
+async def files_remote_share(self,
*,
channels: str | Sequence[str],
external_id: str | None = None,
file: str | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def files_remote_share(
+    self,
+    *,
+    channels: Union[str, Sequence[str]],
+    external_id: Optional[str] = None,
+    file: Optional[str] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Share a remote file into a channel.
+    https://docs.slack.dev/reference/methods/files.remote.share
+    """
+    if external_id is None and file is None:
+        raise e.SlackRequestError("Either external_id or file must be provided.")
+    if isinstance(channels, (list, tuple)):
+        kwargs.update({"channels": ",".join(channels)})
+    else:
+        kwargs.update({"channels": channels})
+    kwargs.update({"external_id": external_id, "file": file})
+    return await self.api_call("files.remote.share", http_verb="GET", params=kwargs)
+
+ +
+
+async def files_remote_update(self,
*,
external_id: str | None = None,
external_url: str | None = None,
file: str | None = None,
title: str | None = None,
filetype: str | None = None,
indexable_file_contents: str | None = None,
preview_image: str | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def files_remote_update(
+    self,
+    *,
+    external_id: Optional[str] = None,
+    external_url: Optional[str] = None,
+    file: Optional[str] = None,
+    title: Optional[str] = None,
+    filetype: Optional[str] = None,
+    indexable_file_contents: Optional[str] = None,
+    preview_image: Optional[str] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Updates an existing remote file.
+    https://docs.slack.dev/reference/methods/files.remote.update
+    """
+    kwargs.update(
+        {
+            "external_id": external_id,
+            "external_url": external_url,
+            "file": file,
+            "title": title,
+            "filetype": filetype,
+        }
+    )
+    files = None
+    # preview_image (file): Preview of the document via multipart/form-data.
+    if preview_image is not None or indexable_file_contents is not None:
+        files = {
+            "preview_image": preview_image,
+            "indexable_file_contents": indexable_file_contents,
+        }
+
+    return await self.api_call(
+        # Intentionally using "POST" method over "GET" here
+        "files.remote.update",
+        http_verb="POST",
+        data=kwargs,
+        files=files,
+    )
+
+ +
+
+async def files_revokePublicURL(self, *, file: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def files_revokePublicURL(
+    self,
+    *,
+    file: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Revokes public/external sharing access for a file
+    https://docs.slack.dev/reference/methods/files.revokePublicURL
+    """
+    kwargs.update({"file": file})
+    return await self.api_call("files.revokePublicURL", params=kwargs)
+
+

Revokes public/external sharing access for a file +https://docs.slack.dev/reference/methods/files.revokePublicURL

+
+
+async def files_sharedPublicURL(self, *, file: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def files_sharedPublicURL(
+    self,
+    *,
+    file: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Enables a file for public/external sharing.
+    https://docs.slack.dev/reference/methods/files.sharedPublicURL
+    """
+    kwargs.update({"file": file})
+    return await self.api_call("files.sharedPublicURL", params=kwargs)
+
+

Enables a file for public/external sharing. +https://docs.slack.dev/reference/methods/files.sharedPublicURL

+
+
+async def files_upload(self,
*,
file: str | bytes | io.IOBase | None = None,
content: str | bytes | None = None,
filename: str | None = None,
filetype: str | None = None,
initial_comment: str | None = None,
thread_ts: str | None = None,
title: str | None = None,
channels: str | Sequence[str] | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def files_upload(
+    self,
+    *,
+    file: Optional[Union[str, bytes, IOBase]] = None,
+    content: Optional[Union[str, bytes]] = None,
+    filename: Optional[str] = None,
+    filetype: Optional[str] = None,
+    initial_comment: Optional[str] = None,
+    thread_ts: Optional[str] = None,
+    title: Optional[str] = None,
+    channels: Optional[Union[str, Sequence[str]]] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Uploads or creates a file.
+    https://docs.slack.dev/reference/methods/files.upload
+    """
+    _print_files_upload_v2_suggestion()
+
+    if file is None and content is None:
+        raise e.SlackRequestError("The file or content argument must be specified.")
+    if file is not None and content is not None:
+        raise e.SlackRequestError("You cannot specify both the file and the content argument.")
+
+    if isinstance(channels, (list, tuple)):
+        kwargs.update({"channels": ",".join(channels)})
+    else:
+        kwargs.update({"channels": channels})
+    kwargs.update(
+        {
+            "filename": filename,
+            "filetype": filetype,
+            "initial_comment": initial_comment,
+            "thread_ts": thread_ts,
+            "title": title,
+        }
+    )
+    if file:
+        if kwargs.get("filename") is None and isinstance(file, str):
+            # use the local filename if filename is missing
+            if kwargs.get("filename") is None:
+                kwargs["filename"] = file.split(os.path.sep)[-1]
+        return await self.api_call("files.upload", files={"file": file}, data=kwargs)
+    else:
+        kwargs["content"] = content
+        return await self.api_call("files.upload", data=kwargs)
+
+ +
+
+async def files_upload_v2(self,
*,
filename: str | None = None,
file: str | bytes | io.IOBase | os.PathLike | None = None,
content: str | bytes | None = None,
title: str | None = None,
alt_txt: str | None = None,
snippet_type: str | None = None,
file_uploads: List[Dict[str, Any]] | None = None,
channel: str | None = None,
channels: List[str] | None = None,
initial_comment: str | None = None,
thread_ts: str | None = None,
request_file_info: bool = True,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def files_upload_v2(
+    self,
+    *,
+    # for sending a single file
+    filename: Optional[str] = None,  # you can skip this only when sending along with content parameter
+    file: Optional[Union[str, bytes, IOBase, os.PathLike]] = None,
+    content: Optional[Union[str, bytes]] = None,
+    title: Optional[str] = None,
+    alt_txt: Optional[str] = None,
+    snippet_type: Optional[str] = None,
+    # To upload multiple files at a time
+    file_uploads: Optional[List[Dict[str, Any]]] = None,
+    channel: Optional[str] = None,
+    channels: Optional[List[str]] = None,
+    initial_comment: Optional[str] = None,
+    thread_ts: Optional[str] = None,
+    request_file_info: bool = True,  # since v3.23, this flag is no longer necessary
+    **kwargs,
+) -> AsyncSlackResponse:
+    """This wrapper method provides an easy way to upload files using the following endpoints:
+
+    - step1: https://docs.slack.dev/reference/methods/files.getUploadURLExternal
+
+    - step2: "https://files.slack.com/upload/v1/..." URLs returned from files.getUploadURLExternal API
+
+    - step3: https://docs.slack.dev/reference/methods/files.completeUploadExternal
+        and https://docs.slack.dev/reference/methods/files.info
+
+    """
+    if file is None and content is None and file_uploads is None:
+        raise e.SlackRequestError("Any of file, content, and file_uploads must be specified.")
+    if file is not None and content is not None:
+        raise e.SlackRequestError("You cannot specify both the file and the content argument.")
+
+    # deprecated arguments:
+    filetype = kwargs.get("filetype")
+
+    if filetype is not None:
+        warnings.warn("The filetype parameter is no longer supported. Please remove it from the arguments.")
+
+    # step1: files.getUploadURLExternal per file
+    files: List[Dict[str, Any]] = []
+    if file_uploads is not None:
+        for f in file_uploads:
+            files.append(_to_v2_file_upload_item(f))
+    else:
+        f = _to_v2_file_upload_item(
+            {
+                "filename": filename,
+                "file": file,
+                "content": content,
+                "title": title,
+                "alt_txt": alt_txt,
+                "snippet_type": snippet_type,
+            }
+        )
+        files.append(f)
+
+    for f in files:
+        url_response = await self.files_getUploadURLExternal(
+            filename=f.get("filename"),  # type: ignore[arg-type]
+            length=f.get("length"),  # type: ignore[arg-type]
+            alt_txt=f.get("alt_txt"),
+            snippet_type=f.get("snippet_type"),
+            token=kwargs.get("token"),
+        )
+        _validate_for_legacy_client(url_response)
+        f["file_id"] = url_response.get("file_id")  # type: ignore[union-attr, unused-ignore]
+        f["upload_url"] = url_response.get("upload_url")  # type: ignore[union-attr, unused-ignore]
+
+    # step2: "https://files.slack.com/upload/v1/..." per file
+    for f in files:
+        upload_result = await self._upload_file(
+            url=f["upload_url"],
+            data=f["data"],
+            logger=self._logger,
+            timeout=self.timeout,
+            proxy=self.proxy,
+            ssl=self.ssl,
+        )
+        if upload_result.status != 200:
+            status = upload_result.status
+            body = upload_result.body
+            message = (
+                "Failed to upload a file "
+                f"(status: {status}, body: {body}, filename: {f.get('filename')}, title: {f.get('title')})"
+            )
+            raise e.SlackRequestError(message)
+
+    # step3: files.completeUploadExternal with all the sets of (file_id + title)
+    completion = await self.files_completeUploadExternal(
+        files=[{"id": f["file_id"], "title": f["title"]} for f in files],
+        channel_id=channel,
+        channels=channels,
+        initial_comment=initial_comment,
+        thread_ts=thread_ts,
+        **kwargs,
+    )
+    if len(completion.get("files")) == 1:  # type: ignore[arg-type, union-attr, unused-ignore]
+        completion.data["file"] = completion.get("files")[0]  # type: ignore[index, union-attr, unused-ignore]
+    return completion
+
+

This wrapper method provides an easy way to upload files using the following endpoints:

+
+
+
+async def functions_completeError(self, *, function_execution_id: str, error: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def functions_completeError(
+    self,
+    *,
+    function_execution_id: str,
+    error: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Signal the failure to execute a function
+    https://docs.slack.dev/reference/methods/functions.completeError
+    """
+    kwargs.update({"function_execution_id": function_execution_id, "error": error})
+    return await self.api_call("functions.completeError", params=kwargs)
+
+ +
+
+async def functions_completeSuccess(self, *, function_execution_id: str, outputs: Dict[str, Any], **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def functions_completeSuccess(
+    self,
+    *,
+    function_execution_id: str,
+    outputs: Dict[str, Any],
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Signal the successful completion of a function
+    https://docs.slack.dev/reference/methods/functions.completeSuccess
+    """
+    kwargs.update({"function_execution_id": function_execution_id, "outputs": json.dumps(outputs)})
+    return await self.api_call("functions.completeSuccess", params=kwargs)
+
+

Signal the successful completion of a function +https://docs.slack.dev/reference/methods/functions.completeSuccess

+
+
+async def groups_archive(self, *, channel: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def groups_archive(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Archives a private channel."""
+    kwargs.update({"channel": channel})
+    kwargs = _remove_none_values(kwargs)
+    return await self.api_call("groups.archive", json=kwargs)
+
+

Archives a private channel.

+
+
+async def groups_create(self, *, name: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def groups_create(
+    self,
+    *,
+    name: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Creates a private channel."""
+    kwargs.update({"name": name})
+    kwargs = _remove_none_values(kwargs)
+    return await self.api_call("groups.create", json=kwargs)
+
+

Creates a private channel.

+
+
+async def groups_createChild(self, *, channel: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def groups_createChild(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Clones and archives a private channel."""
+    kwargs.update({"channel": channel})
+    return await self.api_call("groups.createChild", http_verb="GET", params=kwargs)
+
+

Clones and archives a private channel.

+
+
+async def groups_history(self, *, channel: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def groups_history(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Fetches history of messages and events from a private channel."""
+    kwargs.update({"channel": channel})
+    return await self.api_call("groups.history", http_verb="GET", params=kwargs)
+
+

Fetches history of messages and events from a private channel.

+
+
+async def groups_info(self, *, channel: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def groups_info(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Gets information about a private channel."""
+    kwargs.update({"channel": channel})
+    return await self.api_call("groups.info", http_verb="GET", params=kwargs)
+
+

Gets information about a private channel.

+
+
+async def groups_invite(self, *, channel: str, user: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def groups_invite(
+    self,
+    *,
+    channel: str,
+    user: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Invites a user to a private channel."""
+    kwargs.update({"channel": channel, "user": user})
+    kwargs = _remove_none_values(kwargs)
+    return await self.api_call("groups.invite", json=kwargs)
+
+

Invites a user to a private channel.

+
+
+async def groups_kick(self, *, channel: str, user: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def groups_kick(
+    self,
+    *,
+    channel: str,
+    user: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Removes a user from a private channel."""
+    kwargs.update({"channel": channel, "user": user})
+    kwargs = _remove_none_values(kwargs)
+    return await self.api_call("groups.kick", json=kwargs)
+
+

Removes a user from a private channel.

+
+
+async def groups_leave(self, *, channel: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def groups_leave(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Leaves a private channel."""
+    kwargs.update({"channel": channel})
+    kwargs = _remove_none_values(kwargs)
+    return await self.api_call("groups.leave", json=kwargs)
+
+

Leaves a private channel.

+
+
+async def groups_list(self, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def groups_list(
+    self,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Lists private channels that the calling user has access to."""
+    return await self.api_call("groups.list", http_verb="GET", params=kwargs)
+
+

Lists private channels that the calling user has access to.

+
+
+async def groups_mark(self, *, channel: str, ts: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def groups_mark(
+    self,
+    *,
+    channel: str,
+    ts: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Sets the read cursor in a private channel."""
+    kwargs.update({"channel": channel, "ts": ts})
+    kwargs = _remove_none_values(kwargs)
+    return await self.api_call("groups.mark", json=kwargs)
+
+

Sets the read cursor in a private channel.

+
+
+async def groups_open(self, *, channel: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def groups_open(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Opens a private channel."""
+    kwargs.update({"channel": channel})
+    kwargs = _remove_none_values(kwargs)
+    return await self.api_call("groups.open", json=kwargs)
+
+

Opens a private channel.

+
+
+async def groups_rename(self, *, channel: str, name: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def groups_rename(
+    self,
+    *,
+    channel: str,
+    name: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Renames a private channel."""
+    kwargs.update({"channel": channel, "name": name})
+    kwargs = _remove_none_values(kwargs)
+    return await self.api_call("groups.rename", json=kwargs)
+
+

Renames a private channel.

+
+
+async def groups_replies(self, *, channel: str, thread_ts: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def groups_replies(
+    self,
+    *,
+    channel: str,
+    thread_ts: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Retrieve a thread of messages posted to a private channel"""
+    kwargs.update({"channel": channel, "thread_ts": thread_ts})
+    return await self.api_call("groups.replies", http_verb="GET", params=kwargs)
+
+

Retrieve a thread of messages posted to a private channel

+
+
+async def groups_setPurpose(self, *, channel: str, purpose: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def groups_setPurpose(
+    self,
+    *,
+    channel: str,
+    purpose: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Sets the purpose for a private channel."""
+    kwargs.update({"channel": channel, "purpose": purpose})
+    kwargs = _remove_none_values(kwargs)
+    return await self.api_call("groups.setPurpose", json=kwargs)
+
+

Sets the purpose for a private channel.

+
+
+async def groups_setTopic(self, *, channel: str, topic: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def groups_setTopic(
+    self,
+    *,
+    channel: str,
+    topic: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Sets the topic for a private channel."""
+    kwargs.update({"channel": channel, "topic": topic})
+    kwargs = _remove_none_values(kwargs)
+    return await self.api_call("groups.setTopic", json=kwargs)
+
+

Sets the topic for a private channel.

+
+
+async def groups_unarchive(self, *, channel: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def groups_unarchive(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Unarchives a private channel."""
+    kwargs.update({"channel": channel})
+    kwargs = _remove_none_values(kwargs)
+    return await self.api_call("groups.unarchive", json=kwargs)
+
+

Unarchives a private channel.

+
+
+async def im_close(self, *, channel: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def im_close(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Close a direct message channel."""
+    kwargs.update({"channel": channel})
+    kwargs = _remove_none_values(kwargs)
+    return await self.api_call("im.close", json=kwargs)
+
+

Close a direct message channel.

+
+
+async def im_history(self, *, channel: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def im_history(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Fetches history of messages and events from direct message channel."""
+    kwargs.update({"channel": channel})
+    return await self.api_call("im.history", http_verb="GET", params=kwargs)
+
+

Fetches history of messages and events from direct message channel.

+
+
+async def im_list(self, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def im_list(
+    self,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Lists direct message channels for the calling user."""
+    return await self.api_call("im.list", http_verb="GET", params=kwargs)
+
+

Lists direct message channels for the calling user.

+
+
+async def im_mark(self, *, channel: str, ts: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def im_mark(
+    self,
+    *,
+    channel: str,
+    ts: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Sets the read cursor in a direct message channel."""
+    kwargs.update({"channel": channel, "ts": ts})
+    kwargs = _remove_none_values(kwargs)
+    return await self.api_call("im.mark", json=kwargs)
+
+

Sets the read cursor in a direct message channel.

+
+
+async def im_open(self, *, user: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def im_open(
+    self,
+    *,
+    user: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Opens a direct message channel."""
+    kwargs.update({"user": user})
+    kwargs = _remove_none_values(kwargs)
+    return await self.api_call("im.open", json=kwargs)
+
+

Opens a direct message channel.

+
+
+async def im_replies(self, *, channel: str, thread_ts: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def im_replies(
+    self,
+    *,
+    channel: str,
+    thread_ts: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Retrieve a thread of messages posted to a direct message conversation"""
+    kwargs.update({"channel": channel, "thread_ts": thread_ts})
+    return await self.api_call("im.replies", http_verb="GET", params=kwargs)
+
+

Retrieve a thread of messages posted to a direct message conversation

+
+
+async def migration_exchange(self,
*,
users: str | Sequence[str],
team_id: str | None = None,
to_old: bool | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def migration_exchange(
+    self,
+    *,
+    users: Union[str, Sequence[str]],
+    team_id: Optional[str] = None,
+    to_old: Optional[bool] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """For Enterprise Grid workspaces, map local user IDs to global user IDs
+    https://docs.slack.dev/reference/methods/migration.exchange
+    """
+    if isinstance(users, (list, tuple)):
+        kwargs.update({"users": ",".join(users)})
+    else:
+        kwargs.update({"users": users})
+    kwargs.update({"team_id": team_id, "to_old": to_old})
+    return await self.api_call("migration.exchange", http_verb="GET", params=kwargs)
+
+

For Enterprise Grid workspaces, map local user IDs to global user IDs +https://docs.slack.dev/reference/methods/migration.exchange

+
+
+async def mpim_close(self, *, channel: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def mpim_close(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Closes a multiparty direct message channel."""
+    kwargs.update({"channel": channel})
+    kwargs = _remove_none_values(kwargs)
+    return await self.api_call("mpim.close", json=kwargs)
+
+

Closes a multiparty direct message channel.

+
+
+async def mpim_history(self, *, channel: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def mpim_history(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Fetches history of messages and events from a multiparty direct message."""
+    kwargs.update({"channel": channel})
+    return await self.api_call("mpim.history", http_verb="GET", params=kwargs)
+
+

Fetches history of messages and events from a multiparty direct message.

+
+
+async def mpim_list(self, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def mpim_list(
+    self,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Lists multiparty direct message channels for the calling user."""
+    return await self.api_call("mpim.list", http_verb="GET", params=kwargs)
+
+

Lists multiparty direct message channels for the calling user.

+
+
+async def mpim_mark(self, *, channel: str, ts: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def mpim_mark(
+    self,
+    *,
+    channel: str,
+    ts: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Sets the read cursor in a multiparty direct message channel."""
+    kwargs.update({"channel": channel, "ts": ts})
+    kwargs = _remove_none_values(kwargs)
+    return await self.api_call("mpim.mark", json=kwargs)
+
+

Sets the read cursor in a multiparty direct message channel.

+
+
+async def mpim_open(self, *, users: str | Sequence[str], **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def mpim_open(
+    self,
+    *,
+    users: Union[str, Sequence[str]],
+    **kwargs,
+) -> AsyncSlackResponse:
+    """This method opens a multiparty direct message."""
+    if isinstance(users, (list, tuple)):
+        kwargs.update({"users": ",".join(users)})
+    else:
+        kwargs.update({"users": users})
+    return await self.api_call("mpim.open", params=kwargs)
+
+

This method opens a multiparty direct message.

+
+
+async def mpim_replies(self, *, channel: str, thread_ts: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def mpim_replies(
+    self,
+    *,
+    channel: str,
+    thread_ts: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Retrieve a thread of messages posted to a direct message conversation from a
+    multiparty direct message.
+    """
+    kwargs.update({"channel": channel, "thread_ts": thread_ts})
+    return await self.api_call("mpim.replies", http_verb="GET", params=kwargs)
+
+

Retrieve a thread of messages posted to a direct message conversation from a +multiparty direct message.

+
+
+async def oauth_access(self,
*,
client_id: str,
client_secret: str,
code: str,
redirect_uri: str | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def oauth_access(
+    self,
+    *,
+    client_id: str,
+    client_secret: str,
+    code: str,
+    redirect_uri: Optional[str] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Exchanges a temporary OAuth verifier code for an access token.
+    https://docs.slack.dev/reference/methods/oauth.access
+    """
+    if redirect_uri is not None:
+        kwargs.update({"redirect_uri": redirect_uri})
+    kwargs.update({"code": code})
+    return await self.api_call(
+        "oauth.access",
+        data=kwargs,
+        auth={"client_id": client_id, "client_secret": client_secret},
+    )
+
+

Exchanges a temporary OAuth verifier code for an access token. +https://docs.slack.dev/reference/methods/oauth.access

+
+
+async def oauth_v2_access(self,
*,
client_id: str,
client_secret: str,
code: str | None = None,
redirect_uri: str | None = None,
grant_type: str | None = None,
refresh_token: str | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def oauth_v2_access(
+    self,
+    *,
+    client_id: str,
+    client_secret: str,
+    # This field is required when processing the OAuth redirect URL requests
+    # while it's absent for token rotation
+    code: Optional[str] = None,
+    redirect_uri: Optional[str] = None,
+    # This field is required for token rotation
+    grant_type: Optional[str] = None,
+    # This field is required for token rotation
+    refresh_token: Optional[str] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Exchanges a temporary OAuth verifier code for an access token.
+    https://docs.slack.dev/reference/methods/oauth.v2.access
+    """
+    if redirect_uri is not None:
+        kwargs.update({"redirect_uri": redirect_uri})
+    if code is not None:
+        kwargs.update({"code": code})
+    if grant_type is not None:
+        kwargs.update({"grant_type": grant_type})
+    if refresh_token is not None:
+        kwargs.update({"refresh_token": refresh_token})
+    return await self.api_call(
+        "oauth.v2.access",
+        data=kwargs,
+        auth={"client_id": client_id, "client_secret": client_secret},
+    )
+
+

Exchanges a temporary OAuth verifier code for an access token. +https://docs.slack.dev/reference/methods/oauth.v2.access

+
+
+async def oauth_v2_exchange(self, *, token: str, client_id: str, client_secret: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def oauth_v2_exchange(
+    self,
+    *,
+    token: str,
+    client_id: str,
+    client_secret: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Exchanges a legacy access token for a new expiring access token and refresh token
+    https://docs.slack.dev/reference/methods/oauth.v2.exchange
+    """
+    kwargs.update({"client_id": client_id, "client_secret": client_secret, "token": token})
+    return await self.api_call("oauth.v2.exchange", params=kwargs)
+
+

Exchanges a legacy access token for a new expiring access token and refresh token +https://docs.slack.dev/reference/methods/oauth.v2.exchange

+
+
+async def openid_connect_token(self,
client_id: str,
client_secret: str,
code: str | None = None,
redirect_uri: str | None = None,
grant_type: str | None = None,
refresh_token: str | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def openid_connect_token(
+    self,
+    client_id: str,
+    client_secret: str,
+    code: Optional[str] = None,
+    redirect_uri: Optional[str] = None,
+    grant_type: Optional[str] = None,
+    refresh_token: Optional[str] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Exchanges a temporary OAuth verifier code for an access token for Sign in with Slack.
+    https://docs.slack.dev/reference/methods/openid.connect.token
+    """
+    if redirect_uri is not None:
+        kwargs.update({"redirect_uri": redirect_uri})
+    if code is not None:
+        kwargs.update({"code": code})
+    if grant_type is not None:
+        kwargs.update({"grant_type": grant_type})
+    if refresh_token is not None:
+        kwargs.update({"refresh_token": refresh_token})
+    return await self.api_call(
+        "openid.connect.token",
+        data=kwargs,
+        auth={"client_id": client_id, "client_secret": client_secret},
+    )
+
+

Exchanges a temporary OAuth verifier code for an access token for Sign in with Slack. +https://docs.slack.dev/reference/methods/openid.connect.token

+
+
+async def openid_connect_userInfo(self, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def openid_connect_userInfo(
+    self,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Get the identity of a user who has authorized Sign in with Slack.
+    https://docs.slack.dev/reference/methods/openid.connect.userInfo
+    """
+    return await self.api_call("openid.connect.userInfo", params=kwargs)
+
+

Get the identity of a user who has authorized Sign in with Slack. +https://docs.slack.dev/reference/methods/openid.connect.userInfo

+
+
+async def pins_add(self, *, channel: str, timestamp: str | None = None, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def pins_add(
+    self,
+    *,
+    channel: str,
+    timestamp: Optional[str] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Pins an item to a channel.
+    https://docs.slack.dev/reference/methods/pins.add
+    """
+    kwargs.update({"channel": channel, "timestamp": timestamp})
+    return await self.api_call("pins.add", params=kwargs)
+
+ +
+
+async def pins_list(self, *, channel: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def pins_list(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Lists items pinned to a channel.
+    https://docs.slack.dev/reference/methods/pins.list
+    """
+    kwargs.update({"channel": channel})
+    return await self.api_call("pins.list", http_verb="GET", params=kwargs)
+
+

Lists items pinned to a channel. +https://docs.slack.dev/reference/methods/pins.list

+
+
+async def pins_remove(self, *, channel: str, timestamp: str | None = None, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def pins_remove(
+    self,
+    *,
+    channel: str,
+    timestamp: Optional[str] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Un-pins an item from a channel.
+    https://docs.slack.dev/reference/methods/pins.remove
+    """
+    kwargs.update({"channel": channel, "timestamp": timestamp})
+    return await self.api_call("pins.remove", params=kwargs)
+
+ +
+
+async def reactions_add(self, *, channel: str, name: str, timestamp: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def reactions_add(
+    self,
+    *,
+    channel: str,
+    name: str,
+    timestamp: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Adds a reaction to an item.
+    https://docs.slack.dev/reference/methods/reactions.add
+    """
+    kwargs.update({"channel": channel, "name": name, "timestamp": timestamp})
+    return await self.api_call("reactions.add", params=kwargs)
+
+ +
+
+async def reactions_get(self,
*,
channel: str | None = None,
file: str | None = None,
file_comment: str | None = None,
full: bool | None = None,
timestamp: str | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def reactions_get(
+    self,
+    *,
+    channel: Optional[str] = None,
+    file: Optional[str] = None,
+    file_comment: Optional[str] = None,
+    full: Optional[bool] = None,
+    timestamp: Optional[str] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Gets reactions for an item.
+    https://docs.slack.dev/reference/methods/reactions.get
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "file": file,
+            "file_comment": file_comment,
+            "full": full,
+            "timestamp": timestamp,
+        }
+    )
+    return await self.api_call("reactions.get", http_verb="GET", params=kwargs)
+
+ +
+
+async def reactions_list(self,
*,
count: int | None = None,
cursor: str | None = None,
full: bool | None = None,
limit: int | None = None,
page: int | None = None,
team_id: str | None = None,
user: str | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def reactions_list(
+    self,
+    *,
+    count: Optional[int] = None,
+    cursor: Optional[str] = None,
+    full: Optional[bool] = None,
+    limit: Optional[int] = None,
+    page: Optional[int] = None,
+    team_id: Optional[str] = None,
+    user: Optional[str] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Lists reactions made by a user.
+    https://docs.slack.dev/reference/methods/reactions.list
+    """
+    kwargs.update(
+        {
+            "count": count,
+            "cursor": cursor,
+            "full": full,
+            "limit": limit,
+            "page": page,
+            "team_id": team_id,
+            "user": user,
+        }
+    )
+    return await self.api_call("reactions.list", http_verb="GET", params=kwargs)
+
+ +
+
+async def reactions_remove(self,
*,
name: str,
channel: str | None = None,
file: str | None = None,
file_comment: str | None = None,
timestamp: str | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def reactions_remove(
+    self,
+    *,
+    name: str,
+    channel: Optional[str] = None,
+    file: Optional[str] = None,
+    file_comment: Optional[str] = None,
+    timestamp: Optional[str] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Removes a reaction from an item.
+    https://docs.slack.dev/reference/methods/reactions.remove
+    """
+    kwargs.update(
+        {
+            "name": name,
+            "channel": channel,
+            "file": file,
+            "file_comment": file_comment,
+            "timestamp": timestamp,
+        }
+    )
+    return await self.api_call("reactions.remove", params=kwargs)
+
+ +
+
+async def reminders_add(self,
*,
text: str,
time: str,
team_id: str | None = None,
user: str | None = None,
recurrence: str | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def reminders_add(
+    self,
+    *,
+    text: str,
+    time: str,
+    team_id: Optional[str] = None,
+    user: Optional[str] = None,
+    recurrence: Optional[str] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Creates a reminder.
+    https://docs.slack.dev/reference/methods/reminders.add
+    """
+    kwargs.update(
+        {
+            "text": text,
+            "time": time,
+            "team_id": team_id,
+            "user": user,
+            "recurrence": recurrence,
+        }
+    )
+    return await self.api_call("reminders.add", params=kwargs)
+
+ +
+
+async def reminders_complete(self, *, reminder: str, team_id: str | None = None, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def reminders_complete(
+    self,
+    *,
+    reminder: str,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Marks a reminder as complete.
+    https://docs.slack.dev/reference/methods/reminders.complete
+    """
+    kwargs.update({"reminder": reminder, "team_id": team_id})
+    return await self.api_call("reminders.complete", params=kwargs)
+
+ +
+
+async def reminders_delete(self, *, reminder: str, team_id: str | None = None, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def reminders_delete(
+    self,
+    *,
+    reminder: str,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Deletes a reminder.
+    https://docs.slack.dev/reference/methods/reminders.delete
+    """
+    kwargs.update({"reminder": reminder, "team_id": team_id})
+    return await self.api_call("reminders.delete", params=kwargs)
+
+ +
+
+async def reminders_info(self, *, reminder: str, team_id: str | None = None, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def reminders_info(
+    self,
+    *,
+    reminder: str,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Gets information about a reminder.
+    https://docs.slack.dev/reference/methods/reminders.info
+    """
+    kwargs.update({"reminder": reminder, "team_id": team_id})
+    return await self.api_call("reminders.info", http_verb="GET", params=kwargs)
+
+ +
+
+async def reminders_list(self, *, team_id: str | None = None, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def reminders_list(
+    self,
+    *,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Lists all reminders created by or for a given user.
+    https://docs.slack.dev/reference/methods/reminders.list
+    """
+    kwargs.update({"team_id": team_id})
+    return await self.api_call("reminders.list", http_verb="GET", params=kwargs)
+
+

Lists all reminders created by or for a given user. +https://docs.slack.dev/reference/methods/reminders.list

+
+
+async def rtm_connect(self,
*,
batch_presence_aware: bool | None = None,
presence_sub: bool | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def rtm_connect(
+    self,
+    *,
+    batch_presence_aware: Optional[bool] = None,
+    presence_sub: Optional[bool] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Starts a Real Time Messaging session.
+    https://docs.slack.dev/reference/methods/rtm.connect
+    """
+    kwargs.update({"batch_presence_aware": batch_presence_aware, "presence_sub": presence_sub})
+    return await self.api_call("rtm.connect", http_verb="GET", params=kwargs)
+
+

Starts a Real Time Messaging session. +https://docs.slack.dev/reference/methods/rtm.connect

+
+
+async def rtm_start(self,
*,
batch_presence_aware: bool | None = None,
include_locale: bool | None = None,
mpim_aware: bool | None = None,
no_latest: bool | None = None,
no_unreads: bool | None = None,
presence_sub: bool | None = None,
simple_latest: bool | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def rtm_start(
+    self,
+    *,
+    batch_presence_aware: Optional[bool] = None,
+    include_locale: Optional[bool] = None,
+    mpim_aware: Optional[bool] = None,
+    no_latest: Optional[bool] = None,
+    no_unreads: Optional[bool] = None,
+    presence_sub: Optional[bool] = None,
+    simple_latest: Optional[bool] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Starts a Real Time Messaging session.
+    https://docs.slack.dev/reference/methods/rtm.start
+    """
+    kwargs.update(
+        {
+            "batch_presence_aware": batch_presence_aware,
+            "include_locale": include_locale,
+            "mpim_aware": mpim_aware,
+            "no_latest": no_latest,
+            "no_unreads": no_unreads,
+            "presence_sub": presence_sub,
+            "simple_latest": simple_latest,
+        }
+    )
+    return await self.api_call("rtm.start", http_verb="GET", params=kwargs)
+
+

Starts a Real Time Messaging session. +https://docs.slack.dev/reference/methods/rtm.start

+
+
+async def search_all(self,
*,
query: str,
count: int | None = None,
highlight: bool | None = None,
page: int | None = None,
sort: str | None = None,
sort_dir: str | None = None,
team_id: str | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def search_all(
+    self,
+    *,
+    query: str,
+    count: Optional[int] = None,
+    highlight: Optional[bool] = None,
+    page: Optional[int] = None,
+    sort: Optional[str] = None,
+    sort_dir: Optional[str] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Searches for messages and files matching a query.
+    https://docs.slack.dev/reference/methods/search.all
+    """
+    kwargs.update(
+        {
+            "query": query,
+            "count": count,
+            "highlight": highlight,
+            "page": page,
+            "sort": sort,
+            "sort_dir": sort_dir,
+            "team_id": team_id,
+        }
+    )
+    return await self.api_call("search.all", http_verb="GET", params=kwargs)
+
+

Searches for messages and files matching a query. +https://docs.slack.dev/reference/methods/search.all

+
+
+async def search_files(self,
*,
query: str,
count: int | None = None,
highlight: bool | None = None,
page: int | None = None,
sort: str | None = None,
sort_dir: str | None = None,
team_id: str | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def search_files(
+    self,
+    *,
+    query: str,
+    count: Optional[int] = None,
+    highlight: Optional[bool] = None,
+    page: Optional[int] = None,
+    sort: Optional[str] = None,
+    sort_dir: Optional[str] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Searches for files matching a query.
+    https://docs.slack.dev/reference/methods/search.files
+    """
+    kwargs.update(
+        {
+            "query": query,
+            "count": count,
+            "highlight": highlight,
+            "page": page,
+            "sort": sort,
+            "sort_dir": sort_dir,
+            "team_id": team_id,
+        }
+    )
+    return await self.api_call("search.files", http_verb="GET", params=kwargs)
+
+

Searches for files matching a query. +https://docs.slack.dev/reference/methods/search.files

+
+
+async def search_messages(self,
*,
query: str,
count: int | None = None,
cursor: str | None = None,
highlight: bool | None = None,
page: int | None = None,
sort: str | None = None,
sort_dir: str | None = None,
team_id: str | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def search_messages(
+    self,
+    *,
+    query: str,
+    count: Optional[int] = None,
+    cursor: Optional[str] = None,
+    highlight: Optional[bool] = None,
+    page: Optional[int] = None,
+    sort: Optional[str] = None,
+    sort_dir: Optional[str] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Searches for messages matching a query.
+    https://docs.slack.dev/reference/methods/search.messages
+    """
+    kwargs.update(
+        {
+            "query": query,
+            "count": count,
+            "cursor": cursor,
+            "highlight": highlight,
+            "page": page,
+            "sort": sort,
+            "sort_dir": sort_dir,
+            "team_id": team_id,
+        }
+    )
+    return await self.api_call("search.messages", http_verb="GET", params=kwargs)
+
+

Searches for messages matching a query. +https://docs.slack.dev/reference/methods/search.messages

+
+
+async def slackLists_access_delete(self,
*,
list_id: str,
channel_ids: List[str] | None = None,
user_ids: List[str] | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def slackLists_access_delete(
+    self,
+    *,
+    list_id: str,
+    channel_ids: Optional[List[str]] = None,
+    user_ids: Optional[List[str]] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Revoke access to a List for specified entities.
+    https://docs.slack.dev/reference/methods/slackLists.access.delete
+    """
+    kwargs.update({"list_id": list_id, "channel_ids": channel_ids, "user_ids": user_ids})
+    kwargs = _remove_none_values(kwargs)
+    return await self.api_call("slackLists.access.delete", json=kwargs)
+
+

Revoke access to a List for specified entities. +https://docs.slack.dev/reference/methods/slackLists.access.delete

+
+
+async def slackLists_access_set(self,
*,
list_id: str,
access_level: str,
channel_ids: List[str] | None = None,
user_ids: List[str] | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def slackLists_access_set(
+    self,
+    *,
+    list_id: str,
+    access_level: str,
+    channel_ids: Optional[List[str]] = None,
+    user_ids: Optional[List[str]] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Set the access level to a List for specified entities.
+    https://docs.slack.dev/reference/methods/slackLists.access.set
+    """
+    kwargs.update({"list_id": list_id, "access_level": access_level, "channel_ids": channel_ids, "user_ids": user_ids})
+    kwargs = _remove_none_values(kwargs)
+    return await self.api_call("slackLists.access.set", json=kwargs)
+
+

Set the access level to a List for specified entities. +https://docs.slack.dev/reference/methods/slackLists.access.set

+
+
+async def slackLists_create(self,
*,
name: str,
description_blocks: str | Sequence[Dict | RichTextBlock] | None = None,
schema: List[Dict[str, Any]] | None = None,
copy_from_list_id: str | None = None,
include_copied_list_records: bool | None = None,
todo_mode: bool | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def slackLists_create(
+    self,
+    *,
+    name: str,
+    description_blocks: Optional[Union[str, Sequence[Union[Dict, RichTextBlock]]]] = None,
+    schema: Optional[List[Dict[str, Any]]] = None,
+    copy_from_list_id: Optional[str] = None,
+    include_copied_list_records: Optional[bool] = None,
+    todo_mode: Optional[bool] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Creates a List.
+    https://docs.slack.dev/reference/methods/slackLists.create
+    """
+    kwargs.update(
+        {
+            "name": name,
+            "description_blocks": description_blocks,
+            "schema": schema,
+            "copy_from_list_id": copy_from_list_id,
+            "include_copied_list_records": include_copied_list_records,
+            "todo_mode": todo_mode,
+        }
+    )
+    kwargs = _remove_none_values(kwargs)
+    return await self.api_call("slackLists.create", json=kwargs)
+
+ +
+
+async def slackLists_download_get(self, *, list_id: str, job_id: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def slackLists_download_get(
+    self,
+    *,
+    list_id: str,
+    job_id: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Retrieve List download URL from an export job to download List contents.
+    https://docs.slack.dev/reference/methods/slackLists.download.get
+    """
+    kwargs.update(
+        {
+            "list_id": list_id,
+            "job_id": job_id,
+        }
+    )
+    kwargs = _remove_none_values(kwargs)
+    return await self.api_call("slackLists.download.get", json=kwargs)
+
+

Retrieve List download URL from an export job to download List contents. +https://docs.slack.dev/reference/methods/slackLists.download.get

+
+
+async def slackLists_download_start(self, *, list_id: str, include_archived: bool | None = None, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def slackLists_download_start(
+    self,
+    *,
+    list_id: str,
+    include_archived: Optional[bool] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Initiate a job to export List contents.
+    https://docs.slack.dev/reference/methods/slackLists.download.start
+    """
+    kwargs.update(
+        {
+            "list_id": list_id,
+            "include_archived": include_archived,
+        }
+    )
+    kwargs = _remove_none_values(kwargs)
+    return await self.api_call("slackLists.download.start", json=kwargs)
+
+ +
+
+async def slackLists_items_create(self,
*,
list_id: str,
duplicated_item_id: str | None = None,
parent_item_id: str | None = None,
initial_fields: List[Dict[str, Any]] | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def slackLists_items_create(
+    self,
+    *,
+    list_id: str,
+    duplicated_item_id: Optional[str] = None,
+    parent_item_id: Optional[str] = None,
+    initial_fields: Optional[List[Dict[str, Any]]] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Add a new item to an existing List.
+    https://docs.slack.dev/reference/methods/slackLists.items.create
+    """
+    kwargs.update(
+        {
+            "list_id": list_id,
+            "duplicated_item_id": duplicated_item_id,
+            "parent_item_id": parent_item_id,
+            "initial_fields": initial_fields,
+        }
+    )
+    kwargs = _remove_none_values(kwargs)
+    return await self.api_call("slackLists.items.create", json=kwargs)
+
+ +
+
+async def slackLists_items_delete(self, *, list_id: str, id: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def slackLists_items_delete(
+    self,
+    *,
+    list_id: str,
+    id: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Deletes an item from an existing List.
+    https://docs.slack.dev/reference/methods/slackLists.items.delete
+    """
+    kwargs.update(
+        {
+            "list_id": list_id,
+            "id": id,
+        }
+    )
+    kwargs = _remove_none_values(kwargs)
+    return await self.api_call("slackLists.items.delete", json=kwargs)
+
+ +
+
+async def slackLists_items_deleteMultiple(self, *, list_id: str, ids: List[str], **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def slackLists_items_deleteMultiple(
+    self,
+    *,
+    list_id: str,
+    ids: List[str],
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Deletes multiple items from an existing List.
+    https://docs.slack.dev/reference/methods/slackLists.items.deleteMultiple
+    """
+    kwargs.update(
+        {
+            "list_id": list_id,
+            "ids": ids,
+        }
+    )
+    kwargs = _remove_none_values(kwargs)
+    return await self.api_call("slackLists.items.deleteMultiple", json=kwargs)
+
+ +
+
+async def slackLists_items_info(self, *, list_id: str, id: str, include_is_subscribed: bool | None = None, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def slackLists_items_info(
+    self,
+    *,
+    list_id: str,
+    id: str,
+    include_is_subscribed: Optional[bool] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Get a row from a List.
+    https://docs.slack.dev/reference/methods/slackLists.items.info
+    """
+    kwargs.update(
+        {
+            "list_id": list_id,
+            "id": id,
+            "include_is_subscribed": include_is_subscribed,
+        }
+    )
+    kwargs = _remove_none_values(kwargs)
+    return await self.api_call("slackLists.items.info", json=kwargs)
+
+ +
+
+async def slackLists_items_list(self,
*,
list_id: str,
limit: int | None = None,
cursor: str | None = None,
archived: bool | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def slackLists_items_list(
+    self,
+    *,
+    list_id: str,
+    limit: Optional[int] = None,
+    cursor: Optional[str] = None,
+    archived: Optional[bool] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Get records from a List.
+    https://docs.slack.dev/reference/methods/slackLists.items.list
+    """
+    kwargs.update(
+        {
+            "list_id": list_id,
+            "limit": limit,
+            "cursor": cursor,
+            "archived": archived,
+        }
+    )
+    kwargs = _remove_none_values(kwargs)
+    return await self.api_call("slackLists.items.list", json=kwargs)
+
+ +
+
+async def slackLists_items_update(self, *, list_id: str, cells: List[Dict[str, Any]], **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def slackLists_items_update(
+    self,
+    *,
+    list_id: str,
+    cells: List[Dict[str, Any]],
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Updates cells in a List.
+    https://docs.slack.dev/reference/methods/slackLists.items.update
+    """
+    kwargs.update(
+        {
+            "list_id": list_id,
+            "cells": cells,
+        }
+    )
+    kwargs = _remove_none_values(kwargs)
+    return await self.api_call("slackLists.items.update", json=kwargs)
+
+ +
+
+async def slackLists_update(self,
*,
id: str,
name: str | None = None,
description_blocks: str | Sequence[Dict | RichTextBlock] | None = None,
todo_mode: bool | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def slackLists_update(
+    self,
+    *,
+    id: str,
+    name: Optional[str] = None,
+    description_blocks: Optional[Union[str, Sequence[Union[Dict, RichTextBlock]]]] = None,
+    todo_mode: Optional[bool] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Update a List.
+    https://docs.slack.dev/reference/methods/slackLists.update
+    """
+    kwargs.update(
+        {
+            "id": id,
+            "name": name,
+            "description_blocks": description_blocks,
+            "todo_mode": todo_mode,
+        }
+    )
+    kwargs = _remove_none_values(kwargs)
+    return await self.api_call("slackLists.update", json=kwargs)
+
+ +
+
+async def stars_add(self,
*,
channel: str | None = None,
file: str | None = None,
file_comment: str | None = None,
timestamp: str | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def stars_add(
+    self,
+    *,
+    channel: Optional[str] = None,
+    file: Optional[str] = None,
+    file_comment: Optional[str] = None,
+    timestamp: Optional[str] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Adds a star to an item.
+    https://docs.slack.dev/reference/methods/stars.add
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "file": file,
+            "file_comment": file_comment,
+            "timestamp": timestamp,
+        }
+    )
+    return await self.api_call("stars.add", params=kwargs)
+
+ +
+
+async def stars_list(self,
*,
count: int | None = None,
cursor: str | None = None,
limit: int | None = None,
page: int | None = None,
team_id: str | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def stars_list(
+    self,
+    *,
+    count: Optional[int] = None,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    page: Optional[int] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Lists stars for a user.
+    https://docs.slack.dev/reference/methods/stars.list
+    """
+    kwargs.update(
+        {
+            "count": count,
+            "cursor": cursor,
+            "limit": limit,
+            "page": page,
+            "team_id": team_id,
+        }
+    )
+    return await self.api_call("stars.list", http_verb="GET", params=kwargs)
+
+ +
+
+async def stars_remove(self,
*,
channel: str | None = None,
file: str | None = None,
file_comment: str | None = None,
timestamp: str | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def stars_remove(
+    self,
+    *,
+    channel: Optional[str] = None,
+    file: Optional[str] = None,
+    file_comment: Optional[str] = None,
+    timestamp: Optional[str] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Removes a star from an item.
+    https://docs.slack.dev/reference/methods/stars.remove
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "file": file,
+            "file_comment": file_comment,
+            "timestamp": timestamp,
+        }
+    )
+    return await self.api_call("stars.remove", params=kwargs)
+
+ +
+
+async def team_accessLogs(self,
*,
before: str | int | None = None,
count: str | int | None = None,
page: str | int | None = None,
team_id: str | None = None,
cursor: str | None = None,
limit: int | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def team_accessLogs(
+    self,
+    *,
+    before: Optional[Union[int, str]] = None,
+    count: Optional[Union[int, str]] = None,
+    page: Optional[Union[int, str]] = None,
+    team_id: Optional[str] = None,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Gets the access logs for the current team.
+    https://docs.slack.dev/reference/methods/team.accessLogs
+    """
+    kwargs.update(
+        {
+            "before": before,
+            "count": count,
+            "page": page,
+            "team_id": team_id,
+            "cursor": cursor,
+            "limit": limit,
+        }
+    )
+    return await self.api_call("team.accessLogs", http_verb="GET", params=kwargs)
+
+

Gets the access logs for the current team. +https://docs.slack.dev/reference/methods/team.accessLogs

+
+
+async def team_billableInfo(self, *, team_id: str | None = None, user: str | None = None, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def team_billableInfo(
+    self,
+    *,
+    team_id: Optional[str] = None,
+    user: Optional[str] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Gets billable users information for the current team.
+    https://docs.slack.dev/reference/methods/team.billableInfo
+    """
+    kwargs.update({"team_id": team_id, "user": user})
+    return await self.api_call("team.billableInfo", http_verb="GET", params=kwargs)
+
+

Gets billable users information for the current team. +https://docs.slack.dev/reference/methods/team.billableInfo

+
+
+async def team_billing_info(self, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def team_billing_info(
+    self,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Reads a workspace's billing plan information.
+    https://docs.slack.dev/reference/methods/team.billing.info
+    """
+    return await self.api_call("team.billing.info", params=kwargs)
+
+

Reads a workspace's billing plan information. +https://docs.slack.dev/reference/methods/team.billing.info

+
+
+async def team_externalTeams_disconnect(self, *, target_team: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def team_externalTeams_disconnect(
+    self,
+    *,
+    target_team: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Disconnects an external organization.
+    https://docs.slack.dev/reference/methods/team.externalTeams.disconnect
+    """
+    kwargs.update(
+        {
+            "target_team": target_team,
+        }
+    )
+    return await self.api_call("team.externalTeams.disconnect", params=kwargs)
+
+ +
+
+async def team_externalTeams_list(self,
*,
connection_status_filter: str | None = None,
slack_connect_pref_filter: Sequence[str] | None = None,
sort_direction: str | None = None,
sort_field: str | None = None,
workspace_filter: Sequence[str] | None = None,
cursor: str | None = None,
limit: int | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def team_externalTeams_list(
+    self,
+    *,
+    connection_status_filter: Optional[str] = None,
+    slack_connect_pref_filter: Optional[Sequence[str]] = None,
+    sort_direction: Optional[str] = None,
+    sort_field: Optional[str] = None,
+    workspace_filter: Optional[Sequence[str]] = None,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Returns a list of all the external teams connected and details about the connection.
+    https://docs.slack.dev/reference/methods/team.externalTeams.list
+    """
+    kwargs.update(
+        {
+            "connection_status_filter": connection_status_filter,
+            "sort_direction": sort_direction,
+            "sort_field": sort_field,
+            "cursor": cursor,
+            "limit": limit,
+        }
+    )
+    if slack_connect_pref_filter is not None:
+        if isinstance(slack_connect_pref_filter, (list, tuple)):
+            kwargs.update({"slack_connect_pref_filter": ",".join(slack_connect_pref_filter)})
+        else:
+            kwargs.update({"slack_connect_pref_filter": slack_connect_pref_filter})
+    if workspace_filter is not None:
+        if isinstance(workspace_filter, (list, tuple)):
+            kwargs.update({"workspace_filter": ",".join(workspace_filter)})
+        else:
+            kwargs.update({"workspace_filter": workspace_filter})
+    return await self.api_call("team.externalTeams.list", http_verb="GET", params=kwargs)
+
+

Returns a list of all the external teams connected and details about the connection. +https://docs.slack.dev/reference/methods/team.externalTeams.list

+
+
+async def team_info(self, *, team: str | None = None, domain: str | None = None, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def team_info(
+    self,
+    *,
+    team: Optional[str] = None,
+    domain: Optional[str] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Gets information about the current team.
+    https://docs.slack.dev/reference/methods/team.info
+    """
+    kwargs.update({"team": team, "domain": domain})
+    return await self.api_call("team.info", http_verb="GET", params=kwargs)
+
+

Gets information about the current team. +https://docs.slack.dev/reference/methods/team.info

+
+
+async def team_integrationLogs(self,
*,
app_id: str | None = None,
change_type: str | None = None,
count: str | int | None = None,
page: str | int | None = None,
service_id: str | None = None,
team_id: str | None = None,
user: str | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def team_integrationLogs(
+    self,
+    *,
+    app_id: Optional[str] = None,
+    change_type: Optional[str] = None,
+    count: Optional[Union[int, str]] = None,
+    page: Optional[Union[int, str]] = None,
+    service_id: Optional[str] = None,
+    team_id: Optional[str] = None,
+    user: Optional[str] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Gets the integration logs for the current team.
+    https://docs.slack.dev/reference/methods/team.integrationLogs
+    """
+    kwargs.update(
+        {
+            "app_id": app_id,
+            "change_type": change_type,
+            "count": count,
+            "page": page,
+            "service_id": service_id,
+            "team_id": team_id,
+            "user": user,
+        }
+    )
+    return await self.api_call("team.integrationLogs", http_verb="GET", params=kwargs)
+
+

Gets the integration logs for the current team. +https://docs.slack.dev/reference/methods/team.integrationLogs

+
+
+async def team_preferences_list(self, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def team_preferences_list(
+    self,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Retrieve a list of a workspace's team preferences.
+    https://docs.slack.dev/reference/methods/team.preferences.list
+    """
+    return await self.api_call("team.preferences.list", params=kwargs)
+
+

Retrieve a list of a workspace's team preferences. +https://docs.slack.dev/reference/methods/team.preferences.list

+
+
+async def team_profile_get(self, *, visibility: str | None = None, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def team_profile_get(
+    self,
+    *,
+    visibility: Optional[str] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Retrieve a team's profile.
+    https://docs.slack.dev/reference/methods/team.profile.get
+    """
+    kwargs.update({"visibility": visibility})
+    return await self.api_call("team.profile.get", http_verb="GET", params=kwargs)
+
+ +
+
+async def tooling_tokens_rotate(self, *, refresh_token: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def tooling_tokens_rotate(
+    self,
+    *,
+    refresh_token: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Exchanges a refresh token for a new app configuration token
+    https://docs.slack.dev/reference/methods/tooling.tokens.rotate
+    """
+    kwargs.update({"refresh_token": refresh_token})
+    return await self.api_call("tooling.tokens.rotate", params=kwargs)
+
+

Exchanges a refresh token for a new app configuration token +https://docs.slack.dev/reference/methods/tooling.tokens.rotate

+
+
+async def usergroups_create(self,
*,
name: str,
channels: str | Sequence[str] | None = None,
description: str | None = None,
handle: str | None = None,
include_count: bool | None = None,
team_id: str | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def usergroups_create(
+    self,
+    *,
+    name: str,
+    channels: Optional[Union[str, Sequence[str]]] = None,
+    description: Optional[str] = None,
+    handle: Optional[str] = None,
+    include_count: Optional[bool] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Create a User Group
+    https://docs.slack.dev/reference/methods/usergroups.create
+    """
+    kwargs.update(
+        {
+            "name": name,
+            "description": description,
+            "handle": handle,
+            "include_count": include_count,
+            "team_id": team_id,
+        }
+    )
+    if isinstance(channels, (list, tuple)):
+        kwargs.update({"channels": ",".join(channels)})
+    else:
+        kwargs.update({"channels": channels})
+    return await self.api_call("usergroups.create", params=kwargs)
+
+ +
+
+async def usergroups_disable(self,
*,
usergroup: str,
include_count: bool | None = None,
team_id: str | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def usergroups_disable(
+    self,
+    *,
+    usergroup: str,
+    include_count: Optional[bool] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Disable an existing User Group
+    https://docs.slack.dev/reference/methods/usergroups.disable
+    """
+    kwargs.update({"usergroup": usergroup, "include_count": include_count, "team_id": team_id})
+    return await self.api_call("usergroups.disable", params=kwargs)
+
+ +
+
+async def usergroups_enable(self,
*,
usergroup: str,
include_count: bool | None = None,
team_id: str | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def usergroups_enable(
+    self,
+    *,
+    usergroup: str,
+    include_count: Optional[bool] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Enable a User Group
+    https://docs.slack.dev/reference/methods/usergroups.enable
+    """
+    kwargs.update({"usergroup": usergroup, "include_count": include_count, "team_id": team_id})
+    return await self.api_call("usergroups.enable", params=kwargs)
+
+ +
+
+async def usergroups_list(self,
*,
include_count: bool | None = None,
include_disabled: bool | None = None,
include_users: bool | None = None,
team_id: str | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def usergroups_list(
+    self,
+    *,
+    include_count: Optional[bool] = None,
+    include_disabled: Optional[bool] = None,
+    include_users: Optional[bool] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """List all User Groups for a team
+    https://docs.slack.dev/reference/methods/usergroups.list
+    """
+    kwargs.update(
+        {
+            "include_count": include_count,
+            "include_disabled": include_disabled,
+            "include_users": include_users,
+            "team_id": team_id,
+        }
+    )
+    return await self.api_call("usergroups.list", http_verb="GET", params=kwargs)
+
+ +
+
+async def usergroups_update(self,
*,
usergroup: str,
channels: str | Sequence[str] | None = None,
description: str | None = None,
handle: str | None = None,
include_count: bool | None = None,
name: str | None = None,
team_id: str | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def usergroups_update(
+    self,
+    *,
+    usergroup: str,
+    channels: Optional[Union[str, Sequence[str]]] = None,
+    description: Optional[str] = None,
+    handle: Optional[str] = None,
+    include_count: Optional[bool] = None,
+    name: Optional[str] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Update an existing User Group
+    https://docs.slack.dev/reference/methods/usergroups.update
+    """
+    kwargs.update(
+        {
+            "usergroup": usergroup,
+            "description": description,
+            "handle": handle,
+            "include_count": include_count,
+            "name": name,
+            "team_id": team_id,
+        }
+    )
+    if isinstance(channels, (list, tuple)):
+        kwargs.update({"channels": ",".join(channels)})
+    else:
+        kwargs.update({"channels": channels})
+    return await self.api_call("usergroups.update", params=kwargs)
+
+ +
+
+async def usergroups_users_list(self,
*,
usergroup: str,
include_disabled: bool | None = None,
team_id: str | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def usergroups_users_list(
+    self,
+    *,
+    usergroup: str,
+    include_disabled: Optional[bool] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """List all users in a User Group
+    https://docs.slack.dev/reference/methods/usergroups.users.list
+    """
+    kwargs.update(
+        {
+            "usergroup": usergroup,
+            "include_disabled": include_disabled,
+            "team_id": team_id,
+        }
+    )
+    return await self.api_call("usergroups.users.list", http_verb="GET", params=kwargs)
+
+ +
+
+async def usergroups_users_update(self,
*,
usergroup: str,
users: str | Sequence[str],
include_count: bool | None = None,
team_id: str | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def usergroups_users_update(
+    self,
+    *,
+    usergroup: str,
+    users: Union[str, Sequence[str]],
+    include_count: Optional[bool] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Update the list of users for a User Group
+    https://docs.slack.dev/reference/methods/usergroups.users.update
+    """
+    kwargs.update(
+        {
+            "usergroup": usergroup,
+            "include_count": include_count,
+            "team_id": team_id,
+        }
+    )
+    if isinstance(users, (list, tuple)):
+        kwargs.update({"users": ",".join(users)})
+    else:
+        kwargs.update({"users": users})
+    return await self.api_call("usergroups.users.update", params=kwargs)
+
+

Update the list of users for a User Group +https://docs.slack.dev/reference/methods/usergroups.users.update

+
+
+async def users_conversations(self,
*,
cursor: str | None = None,
exclude_archived: bool | None = None,
limit: int | None = None,
team_id: str | None = None,
types: str | Sequence[str] | None = None,
user: str | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def users_conversations(
+    self,
+    *,
+    cursor: Optional[str] = None,
+    exclude_archived: Optional[bool] = None,
+    limit: Optional[int] = None,
+    team_id: Optional[str] = None,
+    types: Optional[Union[str, Sequence[str]]] = None,
+    user: Optional[str] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """List conversations the calling user may access.
+    https://docs.slack.dev/reference/methods/users.conversations
+    """
+    kwargs.update(
+        {
+            "cursor": cursor,
+            "exclude_archived": exclude_archived,
+            "limit": limit,
+            "team_id": team_id,
+            "user": user,
+        }
+    )
+    if isinstance(types, (list, tuple)):
+        kwargs.update({"types": ",".join(types)})
+    else:
+        kwargs.update({"types": types})
+    return await self.api_call("users.conversations", http_verb="GET", params=kwargs)
+
+

List conversations the calling user may access. +https://docs.slack.dev/reference/methods/users.conversations

+
+
+async def users_deletePhoto(self, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def users_deletePhoto(
+    self,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Delete the user profile photo
+    https://docs.slack.dev/reference/methods/users.deletePhoto
+    """
+    return await self.api_call("users.deletePhoto", http_verb="GET", params=kwargs)
+
+ +
+
+async def users_discoverableContacts_lookup(self, email: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def users_discoverableContacts_lookup(
+    self,
+    email: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Lookup an email address to see if someone is on Slack
+    https://docs.slack.dev/reference/methods/users.discoverableContacts.lookup
+    """
+    kwargs.update({"email": email})
+    return await self.api_call("users.discoverableContacts.lookup", params=kwargs)
+
+

Lookup an email address to see if someone is on Slack +https://docs.slack.dev/reference/methods/users.discoverableContacts.lookup

+
+
+async def users_getPresence(self, *, user: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def users_getPresence(
+    self,
+    *,
+    user: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Gets user presence information.
+    https://docs.slack.dev/reference/methods/users.getPresence
+    """
+    kwargs.update({"user": user})
+    return await self.api_call("users.getPresence", http_verb="GET", params=kwargs)
+
+ +
+
+async def users_identity(self, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def users_identity(
+    self,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Get a user's identity.
+    https://docs.slack.dev/reference/methods/users.identity
+    """
+    return await self.api_call("users.identity", http_verb="GET", params=kwargs)
+
+ +
+
+async def users_info(self, *, user: str, include_locale: bool | None = None, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def users_info(
+    self,
+    *,
+    user: str,
+    include_locale: Optional[bool] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Gets information about a user.
+    https://docs.slack.dev/reference/methods/users.info
+    """
+    kwargs.update({"user": user, "include_locale": include_locale})
+    return await self.api_call("users.info", http_verb="GET", params=kwargs)
+
+ +
+
+async def users_list(self,
*,
cursor: str | None = None,
include_locale: bool | None = None,
limit: int | None = None,
team_id: str | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def users_list(
+    self,
+    *,
+    cursor: Optional[str] = None,
+    include_locale: Optional[bool] = None,
+    limit: Optional[int] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Lists all users in a Slack team.
+    https://docs.slack.dev/reference/methods/users.list
+    """
+    kwargs.update(
+        {
+            "cursor": cursor,
+            "include_locale": include_locale,
+            "limit": limit,
+            "team_id": team_id,
+        }
+    )
+    return await self.api_call("users.list", http_verb="GET", params=kwargs)
+
+

Lists all users in a Slack team. +https://docs.slack.dev/reference/methods/users.list

+
+
+async def users_lookupByEmail(self, *, email: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def users_lookupByEmail(
+    self,
+    *,
+    email: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Find a user with an email address.
+    https://docs.slack.dev/reference/methods/users.lookupByEmail
+    """
+    kwargs.update({"email": email})
+    return await self.api_call("users.lookupByEmail", http_verb="GET", params=kwargs)
+
+ +
+
+async def users_profile_get(self, *, user: str | None = None, include_labels: bool | None = None, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def users_profile_get(
+    self,
+    *,
+    user: Optional[str] = None,
+    include_labels: Optional[bool] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Retrieves a user's profile information.
+    https://docs.slack.dev/reference/methods/users.profile.get
+    """
+    kwargs.update({"user": user, "include_labels": include_labels})
+    return await self.api_call("users.profile.get", http_verb="GET", params=kwargs)
+
+

Retrieves a user's profile information. +https://docs.slack.dev/reference/methods/users.profile.get

+
+
+async def users_profile_set(self,
*,
name: str | None = None,
value: str | None = None,
user: str | None = None,
profile: Dict | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def users_profile_set(
+    self,
+    *,
+    name: Optional[str] = None,
+    value: Optional[str] = None,
+    user: Optional[str] = None,
+    profile: Optional[Dict] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Set the profile information for a user.
+    https://docs.slack.dev/reference/methods/users.profile.set
+    """
+    kwargs.update(
+        {
+            "name": name,
+            "profile": profile,
+            "user": user,
+            "value": value,
+        }
+    )
+    kwargs = _remove_none_values(kwargs)
+    # NOTE: Intentionally using json for the "profile" parameter
+    return await self.api_call("users.profile.set", json=kwargs)
+
+

Set the profile information for a user. +https://docs.slack.dev/reference/methods/users.profile.set

+
+
+async def users_setPhoto(self,
*,
image: str | io.IOBase,
crop_w: str | int | None = None,
crop_x: str | int | None = None,
crop_y: str | int | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def users_setPhoto(
+    self,
+    *,
+    image: Union[str, IOBase],
+    crop_w: Optional[Union[int, str]] = None,
+    crop_x: Optional[Union[int, str]] = None,
+    crop_y: Optional[Union[int, str]] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Set the user profile photo
+    https://docs.slack.dev/reference/methods/users.setPhoto
+    """
+    kwargs.update({"crop_w": crop_w, "crop_x": crop_x, "crop_y": crop_y})
+    return await self.api_call("users.setPhoto", files={"image": image}, data=kwargs)
+
+ +
+
+async def users_setPresence(self, *, presence: str, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def users_setPresence(
+    self,
+    *,
+    presence: str,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Manually sets user presence.
+    https://docs.slack.dev/reference/methods/users.setPresence
+    """
+    kwargs.update({"presence": presence})
+    return await self.api_call("users.setPresence", params=kwargs)
+
+ +
+
+async def views_open(self,
*,
trigger_id: str | None = None,
interactivity_pointer: str | None = None,
view: dict | View,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def views_open(
+    self,
+    *,
+    trigger_id: Optional[str] = None,
+    interactivity_pointer: Optional[str] = None,
+    view: Union[dict, View],
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Open a view for a user.
+    https://docs.slack.dev/reference/methods/views.open
+    See https://docs.slack.dev/surfaces/modals/ for details.
+    """
+    kwargs.update({"trigger_id": trigger_id, "interactivity_pointer": interactivity_pointer})
+    if isinstance(view, View):
+        kwargs.update({"view": view.to_dict()})
+    else:
+        kwargs.update({"view": view})
+    kwargs = _remove_none_values(kwargs)
+    # NOTE: Intentionally using json for the "view" parameter
+    return await self.api_call("views.open", json=kwargs)
+
+ +
+
+async def views_publish(self,
*,
user_id: str,
view: dict | View,
hash: str | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def views_publish(
+    self,
+    *,
+    user_id: str,
+    view: Union[dict, View],
+    hash: Optional[str] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Publish a static view for a User.
+    Create or update the view that comprises an
+    app's Home tab (https://docs.slack.dev/surfaces/app-home/)
+    https://docs.slack.dev/reference/methods/views.publish
+    """
+    kwargs.update({"user_id": user_id, "hash": hash})
+    if isinstance(view, View):
+        kwargs.update({"view": view.to_dict()})
+    else:
+        kwargs.update({"view": view})
+    kwargs = _remove_none_values(kwargs)
+    # NOTE: Intentionally using json for the "view" parameter
+    return await self.api_call("views.publish", json=kwargs)
+
+

Publish a static view for a User. +Create or update the view that comprises an +app's Home tab (https://docs.slack.dev/surfaces/app-home/) +https://docs.slack.dev/reference/methods/views.publish

+
+
+async def views_push(self,
*,
trigger_id: str | None = None,
interactivity_pointer: str | None = None,
view: dict | View,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def views_push(
+    self,
+    *,
+    trigger_id: Optional[str] = None,
+    interactivity_pointer: Optional[str] = None,
+    view: Union[dict, View],
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Push a view onto the stack of a root view.
+    Push a new view onto the existing view stack by passing a view
+    payload and a valid trigger_id generated from an interaction
+    within the existing modal.
+    Read the modals documentation (https://docs.slack.dev/surfaces/modals/)
+    to learn more about the lifecycle and intricacies of views.
+    https://docs.slack.dev/reference/methods/views.push
+    """
+    kwargs.update({"trigger_id": trigger_id, "interactivity_pointer": interactivity_pointer})
+    if isinstance(view, View):
+        kwargs.update({"view": view.to_dict()})
+    else:
+        kwargs.update({"view": view})
+    kwargs = _remove_none_values(kwargs)
+    # NOTE: Intentionally using json for the "view" parameter
+    return await self.api_call("views.push", json=kwargs)
+
+

Push a view onto the stack of a root view. +Push a new view onto the existing view stack by passing a view +payload and a valid trigger_id generated from an interaction +within the existing modal. +Read the modals documentation (https://docs.slack.dev/surfaces/modals/) +to learn more about the lifecycle and intricacies of views. +https://docs.slack.dev/reference/methods/views.push

+
+
+async def views_update(self,
*,
view: dict | View,
external_id: str | None = None,
view_id: str | None = None,
hash: str | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def views_update(
+    self,
+    *,
+    view: Union[dict, View],
+    external_id: Optional[str] = None,
+    view_id: Optional[str] = None,
+    hash: Optional[str] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Update an existing view.
+    Update a view by passing a new view definition along with the
+    view_id returned in views.open or the external_id.
+    See the modals documentation (https://docs.slack.dev/surfaces/modals/#updating_views)
+    to learn more about updating views and avoiding race conditions with the hash argument.
+    https://docs.slack.dev/reference/methods/views.update
+    """
+    if isinstance(view, View):
+        kwargs.update({"view": view.to_dict()})
+    else:
+        kwargs.update({"view": view})
+    if external_id:
+        kwargs.update({"external_id": external_id})
+    elif view_id:
+        kwargs.update({"view_id": view_id})
+    else:
+        raise e.SlackRequestError("Either view_id or external_id is required.")
+    kwargs.update({"hash": hash})
+    kwargs = _remove_none_values(kwargs)
+    # NOTE: Intentionally using json for the "view" parameter
+    return await self.api_call("views.update", json=kwargs)
+
+

Update an existing view. +Update a view by passing a new view definition along with the +view_id returned in views.open or the external_id. +See the modals documentation (https://docs.slack.dev/surfaces/modals/#updating_views) +to learn more about updating views and avoiding race conditions with the hash argument. +https://docs.slack.dev/reference/methods/views.update

+
+ +
+
+ +Expand source code + +
async def workflows_featured_add(
+    self,
+    *,
+    channel_id: str,
+    trigger_ids: Union[str, Sequence[str]],
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Add featured workflows to a channel.
+    https://docs.slack.dev/reference/methods/workflows.featured.add
+    """
+    kwargs.update({"channel_id": channel_id})
+    if isinstance(trigger_ids, (list, tuple)):
+        kwargs.update({"trigger_ids": ",".join(trigger_ids)})
+    else:
+        kwargs.update({"trigger_ids": trigger_ids})
+    return await self.api_call("workflows.featured.add", params=kwargs)
+
+ +
+ +
+
+ +Expand source code + +
async def workflows_featured_list(
+    self,
+    *,
+    channel_ids: Union[str, Sequence[str]],
+    **kwargs,
+) -> AsyncSlackResponse:
+    """List the featured workflows for specified channels.
+    https://docs.slack.dev/reference/methods/workflows.featured.list
+    """
+    if isinstance(channel_ids, (list, tuple)):
+        kwargs.update({"channel_ids": ",".join(channel_ids)})
+    else:
+        kwargs.update({"channel_ids": channel_ids})
+    return await self.api_call("workflows.featured.list", params=kwargs)
+
+

List the featured workflows for specified channels. +https://docs.slack.dev/reference/methods/workflows.featured.list

+
+ +
+
+ +Expand source code + +
async def workflows_featured_remove(
+    self,
+    *,
+    channel_id: str,
+    trigger_ids: Union[str, Sequence[str]],
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Remove featured workflows from a channel.
+    https://docs.slack.dev/reference/methods/workflows.featured.remove
+    """
+    kwargs.update({"channel_id": channel_id})
+    if isinstance(trigger_ids, (list, tuple)):
+        kwargs.update({"trigger_ids": ",".join(trigger_ids)})
+    else:
+        kwargs.update({"trigger_ids": trigger_ids})
+    return await self.api_call("workflows.featured.remove", params=kwargs)
+
+ +
+ +
+
+ +Expand source code + +
async def workflows_featured_set(
+    self,
+    *,
+    channel_id: str,
+    trigger_ids: Union[str, Sequence[str]],
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Set featured workflows for a channel.
+    https://docs.slack.dev/reference/methods/workflows.featured.set
+    """
+    kwargs.update({"channel_id": channel_id})
+    if isinstance(trigger_ids, (list, tuple)):
+        kwargs.update({"trigger_ids": ",".join(trigger_ids)})
+    else:
+        kwargs.update({"trigger_ids": trigger_ids})
+    return await self.api_call("workflows.featured.set", params=kwargs)
+
+ +
+
+async def workflows_stepCompleted(self, *, workflow_step_execute_id: str, outputs: dict | None = None, **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def workflows_stepCompleted(
+    self,
+    *,
+    workflow_step_execute_id: str,
+    outputs: Optional[dict] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Indicate a successful outcome of a workflow step's execution.
+    https://docs.slack.dev/reference/methods/workflows.stepCompleted
+    """
+    kwargs.update({"workflow_step_execute_id": workflow_step_execute_id})
+    if outputs is not None:
+        kwargs.update({"outputs": outputs})
+    kwargs = _remove_none_values(kwargs)
+    # NOTE: Intentionally using json for the "outputs" parameter
+    return await self.api_call("workflows.stepCompleted", json=kwargs)
+
+

Indicate a successful outcome of a workflow step's execution. +https://docs.slack.dev/reference/methods/workflows.stepCompleted

+
+
+async def workflows_stepFailed(self, *, workflow_step_execute_id: str, error: Dict[str, str], **kwargs) ‑> AsyncSlackResponse +
+
+
+ +Expand source code + +
async def workflows_stepFailed(
+    self,
+    *,
+    workflow_step_execute_id: str,
+    error: Dict[str, str],
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Indicate an unsuccessful outcome of a workflow step's execution.
+    https://docs.slack.dev/reference/methods/workflows.stepFailed
+    """
+    kwargs.update(
+        {
+            "workflow_step_execute_id": workflow_step_execute_id,
+            "error": error,
+        }
+    )
+    kwargs = _remove_none_values(kwargs)
+    # NOTE: Intentionally using json for the "error" parameter
+    return await self.api_call("workflows.stepFailed", json=kwargs)
+
+

Indicate an unsuccessful outcome of a workflow step's execution. +https://docs.slack.dev/reference/methods/workflows.stepFailed

+
+
+async def workflows_updateStep(self,
*,
workflow_step_edit_id: str,
inputs: Dict[str, Any] | None = None,
outputs: List[Dict[str, str]] | None = None,
**kwargs) ‑> AsyncSlackResponse
+
+
+
+ +Expand source code + +
async def workflows_updateStep(
+    self,
+    *,
+    workflow_step_edit_id: str,
+    inputs: Optional[Dict[str, Any]] = None,
+    outputs: Optional[List[Dict[str, str]]] = None,
+    **kwargs,
+) -> AsyncSlackResponse:
+    """Update the configuration for a workflow extension step.
+    https://docs.slack.dev/reference/methods/workflows.updateStep
+    """
+    kwargs.update({"workflow_step_edit_id": workflow_step_edit_id})
+    if inputs is not None:
+        kwargs.update({"inputs": inputs})
+    if outputs is not None:
+        kwargs.update({"outputs": outputs})
+    kwargs = _remove_none_values(kwargs)
+    # NOTE: Intentionally using json for the "inputs" / "outputs" parameters
+    return await self.api_call("workflows.updateStep", json=kwargs)
+
+

Update the configuration for a workflow extension step. +https://docs.slack.dev/reference/methods/workflows.updateStep

+
+
+

Inherited members

+ +
+
+
+
+ +
+ + + diff --git a/docs/reference/web/async_internal_utils.html b/docs/reference/web/async_internal_utils.html new file mode 100644 index 000000000..12eb2ba7f --- /dev/null +++ b/docs/reference/web/async_internal_utils.html @@ -0,0 +1,66 @@ + + + + + + +slack_sdk.web.async_internal_utils API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.web.async_internal_utils

+
+
+
+
+
+
+
+
+
+
+
+
+ +
+ + + diff --git a/docs/reference/web/async_slack_response.html b/docs/reference/web/async_slack_response.html new file mode 100644 index 000000000..e081b1ab8 --- /dev/null +++ b/docs/reference/web/async_slack_response.html @@ -0,0 +1,391 @@ + + + + + + +slack_sdk.web.async_slack_response API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.web.async_slack_response

+
+
+

A Python module for interacting and consuming responses from Slack.

+
+
+
+
+
+
+
+
+

Classes

+
+
+class AsyncSlackResponse +(*,
client,
http_verb: str,
api_url: str,
req_args: dict,
data: dict | bytes,
headers: dict,
status_code: int)
+
+
+
+ +Expand source code + +
class AsyncSlackResponse:
+    """An iterable container of response data.
+
+    Attributes:
+        data (dict): The json-encoded content of the response. Along
+            with the headers and status code information.
+
+    Methods:
+        validate: Check if the response from Slack was successful.
+        get: Retrieves any key from the response data.
+        next: Retrieves the next portion of results,
+            if 'next_cursor' is present.
+
+    Example:
+    ```python
+    import os
+    import slack
+
+    client = slack.AsyncWebClient(token=os.environ['SLACK_API_TOKEN'])
+
+    response1 = await client.auth_revoke(test='true')
+    assert not response1['revoked']
+
+    response2 = await client.auth_test()
+    assert response2.get('ok', False)
+
+    users = []
+    async for page in await client.users_list(limit=2):
+        users = users + page['members']
+    ```
+
+    Note:
+        Some responses return collections of information
+        like channel and user lists. If they do it's likely
+        that you'll only receive a portion of results. This
+        object allows you to iterate over the response which
+        makes subsequent API requests until your code hits
+        'break' or there are no more results to be found.
+
+        Any attributes or methods prefixed with _underscores are
+        intended to be "private" internal use only. They may be changed or
+        removed at anytime.
+    """
+
+    def __init__(
+        self,
+        *,
+        client,  # AsyncWebClient
+        http_verb: str,
+        api_url: str,
+        req_args: dict,
+        data: Union[dict, bytes],  # data can be binary data
+        headers: dict,
+        status_code: int,
+    ):
+        self.http_verb = http_verb
+        self.api_url = api_url
+        self.req_args = req_args
+        self.data = data
+        self.headers = headers
+        self.status_code = status_code
+        self._initial_data = data
+        self._iteration = None  # for __iter__ & __next__
+        self._client = client
+        self._logger = logging.getLogger(__name__)
+
+    def __str__(self):
+        """Return the Response data if object is converted to a string."""
+        if isinstance(self.data, bytes):
+            raise ValueError("As the response.data is binary data, this operation is unsupported")
+        return f"{self.data}"
+
+    def __contains__(self, key: str) -> bool:
+        return self.get(key) is not None
+
+    def __getitem__(self, key):
+        """Retrieves any key from the data store.
+
+        Note:
+            This is implemented so users can reference the
+            SlackResponse object like a dictionary.
+            e.g. response["ok"]
+
+        Returns:
+            The value from data or None.
+        """
+        if isinstance(self.data, bytes):
+            raise ValueError("As the response.data is binary data, this operation is unsupported")
+        if self.data is None:
+            raise ValueError("As the response.data is empty, this operation is unsupported")
+        return self.data.get(key, None)
+
+    def __aiter__(self):
+        """Enables the ability to iterate over the response.
+        It's required async-for the iterator protocol.
+
+        Note:
+            This enables Slack cursor-based pagination.
+
+        Returns:
+            (AsyncSlackResponse) self
+        """
+        if isinstance(self.data, bytes):
+            raise ValueError("As the response.data is binary data, this operation is unsupported")
+        self._iteration = 0
+        self.data = self._initial_data
+        return self
+
+    async def __anext__(self):
+        """Retrieves the next portion of results, if 'next_cursor' is present.
+
+        Note:
+            Some responses return collections of information
+            like channel and user lists. If they do it's likely
+            that you'll only receive a portion of results. This
+            method allows you to iterate over the response until
+            your code hits 'break' or there are no more results
+            to be found.
+
+        Returns:
+            (AsyncSlackResponse) self
+                With the new response data now attached to this object.
+
+        Raises:
+            SlackApiError: If the request to the Slack API failed.
+            StopAsyncIteration: If 'next_cursor' is not present or empty.
+        """
+        if isinstance(self.data, bytes):
+            raise ValueError("As the response.data is binary data, this operation is unsupported")
+        self._iteration += 1
+        if self._iteration == 1:
+            return self
+        if _next_cursor_is_present(self.data):
+            params = self.req_args.get("params", {})
+            if params is None:
+                params = {}
+            next_cursor = self.data.get("response_metadata", {}).get("next_cursor") or self.data.get("next_cursor")
+            params.update({"cursor": next_cursor})
+            self.req_args.update({"params": params})
+
+            response = await self._client._request(
+                http_verb=self.http_verb,
+                api_url=self.api_url,
+                req_args=self.req_args,
+            )
+
+            self.data = response["data"]
+            self.headers = response["headers"]
+            self.status_code = response["status_code"]
+            return self.validate()
+        else:
+            raise StopAsyncIteration
+
+    @overload
+    def get(self, key: str, default: None = None) -> Optional[Any]:
+        ...
+
+    @overload
+    def get(self, key: str, default: T) -> T:
+        ...
+
+    def get(self, key, default=None):
+        """Retrieves any key from the response data.
+
+        Note:
+            This is implemented so users can reference the
+            SlackResponse object like a dictionary.
+            e.g. response.get("ok", False)
+
+        Returns:
+            The value from data or the specified default.
+        """
+        if isinstance(self.data, bytes):
+            raise ValueError("As the response.data is binary data, this operation is unsupported")
+        if self.data is None:
+            return None
+        return self.data.get(key, default)
+
+    def validate(self):
+        """Check if the response from Slack was successful.
+
+        Returns:
+            (AsyncSlackResponse)
+                This method returns it's own object. e.g. 'self'
+
+        Raises:
+            SlackApiError: The request to the Slack API failed.
+        """
+        if self.status_code == 200 and self.data and (isinstance(self.data, bytes) or self.data.get("ok", False)):
+            return self
+        msg = f"The request to the Slack API failed. (url: {self.api_url}, status: {self.status_code})"
+        raise e.SlackApiError(message=msg, response=self)
+
+

An iterable container of response data.

+

Attributes

+
+
data : dict
+
The json-encoded content of the response. Along +with the headers and status code information.
+
+

Methods

+

validate: Check if the response from Slack was successful. +get: Retrieves any key from the response data. +next: Retrieves the next portion of results, +if 'next_cursor' is present.

+

Example:

+
import os
+import slack
+
+client = slack.AsyncWebClient(token=os.environ['SLACK_API_TOKEN'])
+
+response1 = await client.auth_revoke(test='true')
+assert not response1['revoked']
+
+response2 = await client.auth_test()
+assert response2.get('ok', False)
+
+users = []
+async for page in await client.users_list(limit=2):
+    users = users + page['members']
+
+

Note

+

Some responses return collections of information +like channel and user lists. If they do it's likely +that you'll only receive a portion of results. This +object allows you to iterate over the response which +makes subsequent API requests until your code hits +'break' or there are no more results to be found.

+

Any attributes or methods prefixed with _underscores are +intended to be "private" internal use only. They may be changed or +removed at anytime.

+

Methods

+
+
+def get(self, key, default=None) +
+
+
+ +Expand source code + +
def get(self, key, default=None):
+    """Retrieves any key from the response data.
+
+    Note:
+        This is implemented so users can reference the
+        SlackResponse object like a dictionary.
+        e.g. response.get("ok", False)
+
+    Returns:
+        The value from data or the specified default.
+    """
+    if isinstance(self.data, bytes):
+        raise ValueError("As the response.data is binary data, this operation is unsupported")
+    if self.data is None:
+        return None
+    return self.data.get(key, default)
+
+

Retrieves any key from the response data.

+

Note

+

This is implemented so users can reference the +SlackResponse object like a dictionary. +e.g. response.get("ok", False)

+

Returns

+

The value from data or the specified default.

+
+
+def validate(self) +
+
+
+ +Expand source code + +
def validate(self):
+    """Check if the response from Slack was successful.
+
+    Returns:
+        (AsyncSlackResponse)
+            This method returns it's own object. e.g. 'self'
+
+    Raises:
+        SlackApiError: The request to the Slack API failed.
+    """
+    if self.status_code == 200 and self.data and (isinstance(self.data, bytes) or self.data.get("ok", False)):
+        return self
+    msg = f"The request to the Slack API failed. (url: {self.api_url}, status: {self.status_code})"
+    raise e.SlackApiError(message=msg, response=self)
+
+

Check if the response from Slack was successful.

+

Returns

+

(AsyncSlackResponse) +This method returns it's own object. e.g. 'self'

+

Raises

+
+
SlackApiError
+
The request to the Slack API failed.
+
+
+
+
+
+
+
+ +
+ + + diff --git a/docs/reference/web/base_client.html b/docs/reference/web/base_client.html new file mode 100644 index 000000000..affbba13c --- /dev/null +++ b/docs/reference/web/base_client.html @@ -0,0 +1,949 @@ + + + + + + +slack_sdk.web.base_client API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.web.base_client

+
+
+

A Python module for interacting with Slack's Web API.

+
+
+
+
+
+
+
+
+

Classes

+
+
+class BaseClient +(token: str | None = None,
base_url: str = 'https://slack.com/api/',
timeout: int = 30,
ssl: ssl.SSLContext | None = None,
proxy: str | None = None,
headers: dict | None = None,
user_agent_prefix: str | None = None,
user_agent_suffix: str | None = None,
team_id: str | None = None,
logger: logging.Logger | None = None,
retry_handlers: List[RetryHandler] | None = None)
+
+
+
+ +Expand source code + +
class BaseClient:
+    BASE_URL = "https://slack.com/api/"
+
+    def __init__(
+        self,
+        token: Optional[str] = None,
+        base_url: str = BASE_URL,
+        timeout: int = 30,
+        ssl: Optional[SSLContext] = None,
+        proxy: Optional[str] = None,
+        headers: Optional[dict] = None,
+        user_agent_prefix: Optional[str] = None,
+        user_agent_suffix: Optional[str] = None,
+        # for Org-Wide App installation
+        team_id: Optional[str] = None,
+        logger: Optional[logging.Logger] = None,
+        retry_handlers: Optional[List[RetryHandler]] = None,
+    ):
+        self.token = None if token is None else token.strip()
+        """A string specifying an `xoxp-*` or `xoxb-*` token."""
+        if not base_url.endswith("/"):
+            base_url += "/"
+        self.base_url = base_url
+        """A string representing the Slack API base URL.
+        Default is `'https://slack.com/api/'`."""
+        self.timeout = timeout
+        """The maximum number of seconds the client will wait
+        to connect and receive a response from Slack.
+        Default is 30 seconds."""
+        self.ssl = ssl
+        """An [`ssl.SSLContext`](https://docs.python.org/3/library/ssl.html#ssl.SSLContext)
+        instance, helpful for specifying your own custom
+        certificate chain."""
+        self.proxy = proxy
+        """String representing a fully-qualified URL to a proxy through which
+        to route all requests to the Slack API. Even if this parameter
+        is not specified, if any of the following environment variables are
+        present, they will be loaded into this parameter: `HTTPS_PROXY`,
+        `https_proxy`, `HTTP_PROXY` or `http_proxy`."""
+        self.headers = headers or {}
+        """`dict` representing additional request headers to attach to all requests."""
+        self.headers["User-Agent"] = get_user_agent(user_agent_prefix, user_agent_suffix)
+        self.default_params = {}
+        if team_id is not None:
+            self.default_params["team_id"] = team_id
+        self._logger = logger if logger is not None else logging.getLogger(__name__)
+
+        self.retry_handlers = retry_handlers if retry_handlers is not None else default_retry_handlers()
+
+        if self.proxy is None or len(self.proxy.strip()) == 0:
+            env_variable = load_http_proxy_from_env(self._logger)
+            if env_variable is not None:
+                self.proxy = env_variable
+
+    # -------------------------
+    # accessors
+
+    @property
+    def logger(self) -> logging.Logger:
+        """The logger this client uses."""
+        return self._logger
+
+    # -------------------------
+    # api call
+
+    def api_call(
+        self,
+        api_method: str,
+        *,
+        http_verb: str = "POST",
+        files: Optional[dict] = None,
+        data: Optional[dict] = None,
+        params: Optional[dict] = None,
+        json: Optional[dict] = None,
+        headers: Optional[dict] = None,
+        auth: Optional[dict] = None,
+    ) -> SlackResponse:
+        """Create a request and execute the API call to Slack.
+
+        Args:
+            api_method (str): The target Slack API method.
+                e.g. 'chat.postMessage'
+            http_verb (str): HTTP Verb. e.g. 'POST'
+            files (dict): Files to multipart upload.
+                e.g. {image OR file: file_object OR file_path}
+            data: The body to attach to the request. If a dictionary is
+                provided, form-encoding will take place.
+                e.g. {'key1': 'value1', 'key2': 'value2'}
+            params (dict): The URL parameters to append to the URL.
+                e.g. {'key1': 'value1', 'key2': 'value2'}
+            json (dict): JSON for the body to attach to the request
+                (if files or data is not specified).
+                e.g. {'key1': 'value1', 'key2': 'value2'}
+            headers (dict): Additional request headers
+            auth (dict): A dictionary that consists of client_id and client_secret
+
+        Returns:
+            (SlackResponse)
+                The server's response to an HTTP request. Data
+                from the response can be accessed like a dict.
+                If the response included 'next_cursor' it can
+                be iterated on to execute subsequent requests.
+
+        Raises:
+            SlackApiError: The following Slack API call failed:
+                'chat.postMessage'.
+            SlackRequestError: Json data can only be submitted as
+                POST requests.
+        """
+
+        api_url = _get_url(self.base_url, api_method)
+        headers = headers or {}
+        headers.update(self.headers)
+        req_args = _build_req_args(
+            token=self.token,
+            http_verb=http_verb,
+            files=files,  # type: ignore[arg-type]
+            data=data,  # type: ignore[arg-type]
+            default_params=self.default_params,
+            params=params,  # type: ignore[arg-type]
+            json=json,  # type: ignore[arg-type]
+            headers=headers,
+            auth=auth,  # type: ignore[arg-type]
+            ssl=self.ssl,
+            proxy=self.proxy,
+        )
+
+        show_deprecation_warning_if_any(api_method)
+        return self._sync_send(api_url=api_url, req_args=req_args)
+
+    # =================================================================
+    # urllib based WebClient
+    # =================================================================
+
+    def _sync_send(self, api_url, req_args) -> SlackResponse:
+        params = req_args["params"] if "params" in req_args else None
+        data = req_args["data"] if "data" in req_args else None
+        files = req_args["files"] if "files" in req_args else None
+        _json = req_args["json"] if "json" in req_args else None
+        headers = req_args["headers"] if "headers" in req_args else None
+        token = params.get("token") if params and "token" in params else None
+        auth = req_args["auth"] if "auth" in req_args else None  # Basic Auth for oauth.v2.access / oauth.access
+        if auth is not None:
+            headers = {}
+            if isinstance(auth, str):
+                headers["Authorization"] = auth
+            elif isinstance(auth, dict):
+                client_id, client_secret = auth["client_id"], auth["client_secret"]
+                value = b64encode(f"{client_id}:{client_secret}".encode("utf-8")).decode("ascii")
+                headers["Authorization"] = f"Basic {value}"
+            else:
+                self._logger.warning(f"As the auth: {auth}: {type(auth)} is unsupported, skipped")
+
+        body_params = {}
+        if params:
+            body_params.update(params)
+        if data:
+            body_params.update(data)
+
+        return self._urllib_api_call(
+            token=token,
+            url=api_url,
+            query_params={},
+            body_params=body_params,
+            files=files,  # type: ignore[arg-type]
+            json_body=_json,  # type: ignore[arg-type]
+            additional_headers=headers,  # type: ignore[arg-type]
+        )
+
+    def _request_for_pagination(self, api_url: str, req_args: Dict[str, Dict[str, Any]]) -> Dict[str, Any]:
+        """This method is supposed to be used only for SlackResponse pagination
+
+        You can paginate using Python's for iterator as below:
+
+          for response in client.conversations_list(limit=100):
+              # do something with each response here
+        """
+        response = self._perform_urllib_http_request(url=api_url, args=req_args)
+        return {
+            "status_code": int(response["status"]),
+            "headers": dict(response["headers"]),
+            "data": json.loads(response["body"]),
+        }
+
+    def _urllib_api_call(
+        self,
+        *,
+        token: Optional[str] = None,
+        url: str,
+        query_params: Dict[str, str],
+        json_body: Dict,
+        body_params: Dict[str, str],
+        files: Dict[str, io.BytesIO],
+        additional_headers: Dict[str, str],
+    ) -> SlackResponse:
+        """Performs a Slack API request and returns the result.
+
+        Args:
+            token: Slack API Token (either bot token or user token)
+            url: Complete URL (e.g., https://slack.com/api/chat.postMessage)
+            query_params: Query string
+            json_body: JSON data structure (it's still a dict at this point),
+                if you give this argument, body_params and files will be skipped
+            body_params: Form body params
+            files: Files to upload
+            additional_headers: Request headers to append
+
+        Returns:
+            API response
+        """
+        files_to_close: List[BinaryIO] = []
+        try:
+            # True/False -> "1"/"0"
+            query_params = convert_bool_to_0_or_1(query_params)  # type: ignore[assignment]
+            body_params = convert_bool_to_0_or_1(body_params)  # type: ignore[assignment]
+
+            if self._logger.level <= logging.DEBUG:
+
+                def convert_params(values: dict) -> dict:
+                    if not values or not isinstance(values, dict):
+                        return {}
+                    return {k: ("(bytes)" if isinstance(v, bytes) else v) for k, v in values.items()}
+
+                headers = {k: "(redacted)" if k.lower() == "authorization" else v for k, v in additional_headers.items()}
+                self._logger.debug(
+                    f"Sending a request - url: {url}, "
+                    f"query_params: {convert_params(query_params)}, "
+                    f"body_params: {convert_params(body_params)}, "
+                    f"files: {convert_params(files)}, "
+                    f"json_body: {json_body}, "
+                    f"headers: {headers}"
+                )
+
+            request_data = {}
+            if files is not None and isinstance(files, dict) and len(files) > 0:
+                if body_params:
+                    for k, v in body_params.items():
+                        request_data.update({k: v})
+
+                for k, v in files.items():  # type: ignore[assignment]
+                    if isinstance(v, str):
+                        f: BinaryIO = open(v.encode("utf-8", "ignore"), "rb")
+                        files_to_close.append(f)
+                        request_data.update({k: f})  # type: ignore[dict-item]
+                    elif isinstance(v, (bytearray, bytes)):
+                        request_data.update({k: io.BytesIO(v)})
+                    else:
+                        request_data.update({k: v})
+
+            request_headers = self._build_urllib_request_headers(
+                token=token or self.token,  # type: ignore[arg-type]
+                has_json=json is not None,
+                has_files=files is not None,
+                additional_headers=additional_headers,
+            )
+            request_args = {
+                "headers": request_headers,
+                "data": request_data,
+                "params": body_params,
+                "files": files,
+                "json": json_body,
+            }
+            if query_params:
+                q = urlencode(query_params)
+                url = f"{url}&{q}" if "?" in url else f"{url}?{q}"
+
+            response = self._perform_urllib_http_request(url=url, args=request_args)  # type: ignore[arg-type]
+            response_body = response.get("body", None)
+            response_body_data: Optional[Union[dict, bytes]] = response_body
+            if response_body is not None and not isinstance(response_body, bytes):
+                try:
+                    response_body_data = json.loads(response["body"])
+                except json.decoder.JSONDecodeError:
+                    message = _build_unexpected_body_error_message(response.get("body", ""))
+                    self._logger.error(f"Failed to decode Slack API response: {message}")
+                    response_body_data = {"ok": False, "error": message}
+
+            all_params: Dict[str, Any] = copy.copy(body_params) if body_params is not None else {}
+            if query_params:
+                all_params.update(query_params)
+            request_args["params"] = all_params  # for backward-compatibility
+
+            return SlackResponse(
+                client=self,
+                http_verb="POST",  # you can use POST method for all the Web APIs
+                api_url=url,
+                req_args=request_args,
+                data=response_body_data,  # type: ignore[arg-type]
+                headers=dict(response["headers"]),
+                status_code=response["status"],
+            ).validate()
+        finally:
+            for f in files_to_close:
+                if not f.closed:
+                    f.close()
+
+    def _perform_urllib_http_request(self, *, url: str, args: Dict[str, Dict[str, Any]]) -> Dict[str, Any]:
+        """Performs an HTTP request and parses the response.
+
+        Args:
+            url: Complete URL (e.g., https://slack.com/api/chat.postMessage)
+            args: args has "headers", "data", "params", and "json"
+                "headers": Dict[str, str]
+                "data": Dict[str, Any]
+                "params": Dict[str, str],
+                "json": Dict[str, Any],
+
+        Returns:
+            dict {status: int, headers: Headers, body: str}
+        """
+        headers = args["headers"]
+        body: Optional[Union[bytes, str]] = None
+        if args["json"]:
+            body = json.dumps(args["json"])
+            headers["Content-Type"] = "application/json;charset=utf-8"
+        elif args["data"]:
+            boundary = f"--------------{uuid.uuid4()}"
+            sep_boundary = b"\r\n--" + boundary.encode("ascii")
+            end_boundary = sep_boundary + b"--\r\n"
+            body_builder = io.BytesIO()
+            data = args["data"]
+            for key, value in data.items():
+                readable = getattr(value, "readable", None)
+                if readable and value.readable():
+                    filename = "Uploaded file"
+                    name_attr = getattr(value, "name", None)
+                    if name_attr:
+                        filename = name_attr.decode("utf-8") if isinstance(name_attr, bytes) else name_attr
+                    if "filename" in data:
+                        filename = data["filename"]
+                    mimetype = mimetypes.guess_type(filename)[0] or "application/octet-stream"
+                    title = (
+                        f'\r\nContent-Disposition: form-data; name="{key}"; filename="{filename}"\r\n'
+                        + f"Content-Type: {mimetype}\r\n"
+                    )
+                    value = value.read()
+                else:
+                    title = f'\r\nContent-Disposition: form-data; name="{key}"\r\n'
+                    value = str(value).encode("utf-8")
+                body_builder.write(sep_boundary)
+                body_builder.write(title.encode("utf-8"))
+                body_builder.write(b"\r\n")
+                body_builder.write(value)
+
+            body_builder.write(end_boundary)
+            body = body_builder.getvalue()
+            headers["Content-Type"] = f"multipart/form-data; boundary={boundary}"
+            headers["Content-Length"] = len(body)
+        elif args["params"]:
+            body = urlencode(args["params"])
+            headers["Content-Type"] = "application/x-www-form-urlencoded"
+
+        if isinstance(body, str):
+            body = body.encode("utf-8")
+
+        # NOTE: Intentionally ignore the `http_verb` here
+        # Slack APIs accepts any API method requests with POST methods
+        req = Request(method="POST", url=url, data=body, headers=headers)
+        resp = None
+        last_error = None
+
+        retry_state = RetryState()
+        counter_for_safety = 0
+        while counter_for_safety < 100:
+            counter_for_safety += 1
+            # If this is a retry, the next try started here. We can reset the flag.
+            retry_state.next_attempt_requested = False
+
+            try:
+                resp = self._perform_urllib_http_request_internal(url, req)
+                # The resp is a 200 OK response
+                if len(self.retry_handlers) > 0:
+                    retry_request = RetryHttpRequest.from_urllib_http_request(req)
+                    body_string = resp["body"] if isinstance(resp["body"], str) else None
+                    body_bytes = body_string.encode("utf-8") if body_string is not None else resp["body"]
+                    if body_string is not None and body_string.startswith("{"):
+                        body = json.loads(body_string)
+                    else:
+                        body = {}  # type: ignore[assignment]
+                    retry_response = RetryHttpResponse(
+                        status_code=resp["status"],
+                        headers=resp["headers"],
+                        body=body,  # type: ignore[arg-type]
+                        data=body_bytes,
+                    )
+                    for handler in self.retry_handlers:
+                        if handler.can_retry(state=retry_state, request=retry_request, response=retry_response):
+                            if self._logger.level <= logging.DEBUG:
+                                self._logger.info(
+                                    f"A retry handler found: {type(handler).__name__} for {req.method} {req.full_url}"
+                                )
+                            handler.prepare_for_next_attempt(
+                                state=retry_state, request=retry_request, response=retry_response
+                            )
+                            break
+                if retry_state.next_attempt_requested is False:
+                    return resp
+
+            except HTTPError as e:
+                # As adding new values to HTTPError#headers can be ignored, building a new dict object here
+                response_headers = dict(e.headers.items())
+                resp = {"status": e.code, "headers": response_headers}
+                if e.code == 429:
+                    # for compatibility with aiohttp
+                    if "retry-after" not in response_headers and "Retry-After" in response_headers:
+                        response_headers["retry-after"] = response_headers["Retry-After"]
+                    if "Retry-After" not in response_headers and "retry-after" in response_headers:
+                        response_headers["Retry-After"] = response_headers["retry-after"]
+
+                # read the response body here
+                charset = e.headers.get_content_charset() or "utf-8"
+                response_body: str = e.read().decode(charset)
+                resp["body"] = response_body
+
+                # Try to find a retry handler for this error
+                retry_request = RetryHttpRequest.from_urllib_http_request(req)
+                retry_response = RetryHttpResponse(
+                    status_code=e.code,
+                    headers={k: [v] for k, v in response_headers.items()},
+                    data=response_body.encode("utf-8") if response_body is not None else None,
+                )
+                for handler in self.retry_handlers:
+                    if handler.can_retry(
+                        state=retry_state,
+                        request=retry_request,
+                        response=retry_response,
+                        error=e,
+                    ):
+                        if self._logger.level <= logging.DEBUG:
+                            self._logger.info(
+                                f"A retry handler found: {type(handler).__name__} for {req.method} {req.full_url} - {e}"
+                            )
+                        handler.prepare_for_next_attempt(
+                            state=retry_state,
+                            request=retry_request,
+                            response=retry_response,
+                            error=e,
+                        )
+                        break
+
+                if retry_state.next_attempt_requested is False:
+                    return resp
+
+            except Exception as err:
+                last_error = err
+                self._logger.error(f"Failed to send a request to Slack API server: {err}")
+
+                # Try to find a retry handler for this error
+                retry_request = RetryHttpRequest.from_urllib_http_request(req)
+                for handler in self.retry_handlers:
+                    if handler.can_retry(
+                        state=retry_state,
+                        request=retry_request,
+                        response=None,
+                        error=err,
+                    ):
+                        if self._logger.level <= logging.DEBUG:
+                            self._logger.info(
+                                f"A retry handler found: {type(handler).__name__} for {req.method} {req.full_url} - {err}"
+                            )
+                        handler.prepare_for_next_attempt(
+                            state=retry_state,
+                            request=retry_request,
+                            response=None,
+                            error=err,
+                        )
+                        self._logger.info(f"Going to retry the same request: {req.method} {req.full_url}")
+                        break
+
+                if retry_state.next_attempt_requested is False:
+                    raise err
+
+        if resp is not None:
+            return resp
+        raise last_error  # type: ignore[misc]
+
+    def _perform_urllib_http_request_internal(
+        self,
+        url: str,
+        req: Request,
+    ) -> Dict[str, Any]:
+        # urllib not only opens http:// or https:// URLs, but also ftp:// and file://.
+        # With this it might be possible to open local files on the executing machine
+        # which might be a security risk if the URL to open can be manipulated by an external user.
+        # (BAN-B310)
+        if url.lower().startswith("http"):
+            opener: Optional[OpenerDirector] = None
+            if self.proxy is not None:
+                if isinstance(self.proxy, str):
+                    opener = urllib.request.build_opener(
+                        ProxyHandler({"http": self.proxy, "https": self.proxy}),
+                        HTTPSHandler(context=self.ssl),
+                    )
+                else:
+                    raise SlackRequestError(f"Invalid proxy detected: {self.proxy} must be a str value")
+
+            if opener:
+                resp = opener.open(req, timeout=self.timeout)
+            else:
+                resp = urlopen(req, context=self.ssl, timeout=self.timeout)
+            if resp.headers.get_content_type() == "application/gzip":
+                # admin.analytics.getFile
+                body: bytes = resp.read()
+                if self._logger.level <= logging.DEBUG:
+                    self._logger.debug(
+                        "Received the following response - "
+                        f"status: {resp.code}, "
+                        f"headers: {dict(resp.headers)}, "
+                        f"body: (binary)"
+                    )
+                return {"status": resp.code, "headers": resp.headers, "body": body}
+
+            charset = resp.headers.get_content_charset() or "utf-8"
+            decoded_body: str = resp.read().decode(charset)  # read the response body here
+            if self._logger.level <= logging.DEBUG:
+                self._logger.debug(
+                    "Received the following response - "
+                    f"status: {resp.code}, "
+                    f"headers: {dict(resp.headers)}, "
+                    f"body: {decoded_body}"
+                )
+            return {"status": resp.code, "headers": resp.headers, "body": decoded_body}
+        raise SlackRequestError(f"Invalid URL detected: {url}")
+
+    def _build_urllib_request_headers(
+        self, token: str, has_json: bool, has_files: bool, additional_headers: dict
+    ) -> Dict[str, str]:
+        headers = {"Content-Type": "application/x-www-form-urlencoded"}
+        headers.update(self.headers)
+        if token:
+            headers.update({"Authorization": "Bearer {}".format(token)})
+        if additional_headers:
+            headers.update(additional_headers)
+        if has_json:
+            headers.update({"Content-Type": "application/json;charset=utf-8"})
+        if has_files:
+            # will be set afterward
+            headers.pop("Content-Type", None)
+        return headers
+
+    def _upload_file(
+        self,
+        *,
+        url: str,
+        data: bytes,
+        logger: logging.Logger,
+        timeout: int,
+        proxy: Optional[str],
+        ssl: Optional[SSLContext],
+    ) -> FileUploadV2Result:
+        """Upload a file using the issued upload URL"""
+        result = _upload_file_via_v2_url(
+            url=url,
+            data=data,
+            logger=logger,
+            timeout=timeout,
+            proxy=proxy,
+            ssl=ssl,
+        )
+        return FileUploadV2Result(
+            status=result.get("status"),  # type: ignore[arg-type]
+            body=result.get("body"),  # type: ignore[arg-type]
+        )
+
+    # =================================================================
+
+    @staticmethod
+    def validate_slack_signature(*, signing_secret: str, data: str, timestamp: str, signature: str) -> bool:
+        """
+        Slack creates a unique string for your app and shares it with you. Verify
+        requests from Slack with confidence by verifying signatures using your
+        signing secret.
+
+        On each HTTP request that Slack sends, we add an X-Slack-Signature HTTP
+        header. The signature is created by combining the signing secret with the
+        body of the request we're sending using a standard HMAC-SHA256 keyed hash.
+
+        https://docs.slack.dev/authentication/verifying-requests-from-slack/#how_to_make_a_request_signature_in_4_easy_steps__an_overview
+
+        Args:
+            signing_secret: Your application's signing secret, available in the
+                Slack API dashboard
+            data: The raw body of the incoming request - no headers, just the body.
+            timestamp: from the 'X-Slack-Request-Timestamp' header
+            signature: from the 'X-Slack-Signature' header - the calculated signature
+                should match this.
+
+        Returns:
+            True if signatures matches
+        """
+        warnings.warn(
+            "As this method is deprecated since slackclient 2.6.0, "
+            "use `from slack.signature import SignatureVerifier` instead",
+            DeprecationWarning,
+        )
+        format_req = str.encode(f"v0:{timestamp}:{data}")
+        encoded_secret = str.encode(signing_secret)
+        request_hash = hmac.new(encoded_secret, format_req, hashlib.sha256).hexdigest()
+        calculated_signature = f"v0={request_hash}"
+        return hmac.compare_digest(calculated_signature, signature)
+
+
+

Subclasses

+ +

Class variables

+
+
var BASE_URL
+
+

The type of the None singleton.

+
+
+

Static methods

+
+
+def validate_slack_signature(*, signing_secret: str, data: str, timestamp: str, signature: str) ‑> bool +
+
+
+ +Expand source code + +
@staticmethod
+def validate_slack_signature(*, signing_secret: str, data: str, timestamp: str, signature: str) -> bool:
+    """
+    Slack creates a unique string for your app and shares it with you. Verify
+    requests from Slack with confidence by verifying signatures using your
+    signing secret.
+
+    On each HTTP request that Slack sends, we add an X-Slack-Signature HTTP
+    header. The signature is created by combining the signing secret with the
+    body of the request we're sending using a standard HMAC-SHA256 keyed hash.
+
+    https://docs.slack.dev/authentication/verifying-requests-from-slack/#how_to_make_a_request_signature_in_4_easy_steps__an_overview
+
+    Args:
+        signing_secret: Your application's signing secret, available in the
+            Slack API dashboard
+        data: The raw body of the incoming request - no headers, just the body.
+        timestamp: from the 'X-Slack-Request-Timestamp' header
+        signature: from the 'X-Slack-Signature' header - the calculated signature
+            should match this.
+
+    Returns:
+        True if signatures matches
+    """
+    warnings.warn(
+        "As this method is deprecated since slackclient 2.6.0, "
+        "use `from slack.signature import SignatureVerifier` instead",
+        DeprecationWarning,
+    )
+    format_req = str.encode(f"v0:{timestamp}:{data}")
+    encoded_secret = str.encode(signing_secret)
+    request_hash = hmac.new(encoded_secret, format_req, hashlib.sha256).hexdigest()
+    calculated_signature = f"v0={request_hash}"
+    return hmac.compare_digest(calculated_signature, signature)
+
+

Slack creates a unique string for your app and shares it with you. Verify +requests from Slack with confidence by verifying signatures using your +signing secret.

+

On each HTTP request that Slack sends, we add an X-Slack-Signature HTTP +header. The signature is created by combining the signing secret with the +body of the request we're sending using a standard HMAC-SHA256 keyed hash.

+

https://docs.slack.dev/authentication/verifying-requests-from-slack/#how_to_make_a_request_signature_in_4_easy_steps__an_overview

+

Args

+
+
signing_secret
+
Your application's signing secret, available in the +Slack API dashboard
+
data
+
The raw body of the incoming request - no headers, just the body.
+
timestamp
+
from the 'X-Slack-Request-Timestamp' header
+
signature
+
from the 'X-Slack-Signature' header - the calculated signature +should match this.
+
+

Returns

+

True if signatures matches

+
+
+

Instance variables

+
+
var base_url
+
+

A string representing the Slack API base URL. +Default is 'https://slack.com/api/'.

+
+
var headers
+
+

dict representing additional request headers to attach to all requests.

+
+
prop logger : logging.Logger
+
+
+ +Expand source code + +
@property
+def logger(self) -> logging.Logger:
+    """The logger this client uses."""
+    return self._logger
+
+

The logger this client uses.

+
+
var proxy
+
+

String representing a fully-qualified URL to a proxy through which +to route all requests to the Slack API. Even if this parameter +is not specified, if any of the following environment variables are +present, they will be loaded into this parameter: HTTPS_PROXY, +https_proxy, HTTP_PROXY or http_proxy.

+
+
var ssl
+
+

An ssl.SSLContext +instance, helpful for specifying your own custom +certificate chain.

+
+
var timeout
+
+

The maximum number of seconds the client will wait +to connect and receive a response from Slack. +Default is 30 seconds.

+
+
var token
+
+

A string specifying an xoxp-* or xoxb-* token.

+
+
+

Methods

+
+
+def api_call(self,
api_method: str,
*,
http_verb: str = 'POST',
files: dict | None = None,
data: dict | None = None,
params: dict | None = None,
json: dict | None = None,
headers: dict | None = None,
auth: dict | None = None) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def api_call(
+    self,
+    api_method: str,
+    *,
+    http_verb: str = "POST",
+    files: Optional[dict] = None,
+    data: Optional[dict] = None,
+    params: Optional[dict] = None,
+    json: Optional[dict] = None,
+    headers: Optional[dict] = None,
+    auth: Optional[dict] = None,
+) -> SlackResponse:
+    """Create a request and execute the API call to Slack.
+
+    Args:
+        api_method (str): The target Slack API method.
+            e.g. 'chat.postMessage'
+        http_verb (str): HTTP Verb. e.g. 'POST'
+        files (dict): Files to multipart upload.
+            e.g. {image OR file: file_object OR file_path}
+        data: The body to attach to the request. If a dictionary is
+            provided, form-encoding will take place.
+            e.g. {'key1': 'value1', 'key2': 'value2'}
+        params (dict): The URL parameters to append to the URL.
+            e.g. {'key1': 'value1', 'key2': 'value2'}
+        json (dict): JSON for the body to attach to the request
+            (if files or data is not specified).
+            e.g. {'key1': 'value1', 'key2': 'value2'}
+        headers (dict): Additional request headers
+        auth (dict): A dictionary that consists of client_id and client_secret
+
+    Returns:
+        (SlackResponse)
+            The server's response to an HTTP request. Data
+            from the response can be accessed like a dict.
+            If the response included 'next_cursor' it can
+            be iterated on to execute subsequent requests.
+
+    Raises:
+        SlackApiError: The following Slack API call failed:
+            'chat.postMessage'.
+        SlackRequestError: Json data can only be submitted as
+            POST requests.
+    """
+
+    api_url = _get_url(self.base_url, api_method)
+    headers = headers or {}
+    headers.update(self.headers)
+    req_args = _build_req_args(
+        token=self.token,
+        http_verb=http_verb,
+        files=files,  # type: ignore[arg-type]
+        data=data,  # type: ignore[arg-type]
+        default_params=self.default_params,
+        params=params,  # type: ignore[arg-type]
+        json=json,  # type: ignore[arg-type]
+        headers=headers,
+        auth=auth,  # type: ignore[arg-type]
+        ssl=self.ssl,
+        proxy=self.proxy,
+    )
+
+    show_deprecation_warning_if_any(api_method)
+    return self._sync_send(api_url=api_url, req_args=req_args)
+
+

Create a request and execute the API call to Slack.

+

Args

+
+
api_method : str
+
The target Slack API method. +e.g. 'chat.postMessage'
+
http_verb : str
+
HTTP Verb. e.g. 'POST'
+
files : dict
+
Files to multipart upload. +e.g. {image OR file: file_object OR file_path}
+
data
+
The body to attach to the request. If a dictionary is +provided, form-encoding will take place. +e.g. {'key1': 'value1', 'key2': 'value2'}
+
params : dict
+
The URL parameters to append to the URL. +e.g. {'key1': 'value1', 'key2': 'value2'}
+
json : dict
+
JSON for the body to attach to the request +(if files or data is not specified). +e.g. {'key1': 'value1', 'key2': 'value2'}
+
headers : dict
+
Additional request headers
+
auth : dict
+
A dictionary that consists of client_id and client_secret
+
+

Returns

+

(SlackResponse) +The server's response to an HTTP request. Data +from the response can be accessed like a dict. +If the response included 'next_cursor' it can +be iterated on to execute subsequent requests.

+

Raises

+
+
SlackApiError
+
The following Slack API call failed: +'chat.postMessage'.
+
SlackRequestError
+
Json data can only be submitted as +POST requests.
+
+
+
+
+
+
+
+ +
+ + + diff --git a/docs/reference/web/chat_stream.html b/docs/reference/web/chat_stream.html new file mode 100644 index 000000000..94d96e5eb --- /dev/null +++ b/docs/reference/web/chat_stream.html @@ -0,0 +1,506 @@ + + + + + + +slack_sdk.web.chat_stream API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.web.chat_stream

+
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class ChatStream +(client: WebClient,
*,
channel: str,
logger: logging.Logger,
thread_ts: str,
buffer_size: int,
recipient_team_id: str | None = None,
recipient_user_id: str | None = None,
**kwargs)
+
+
+
+ +Expand source code + +
class ChatStream:
+    """A helper class for streaming markdown text into a conversation using the chat streaming APIs.
+
+    This class provides a convenient interface for the chat.startStream, chat.appendStream, and chat.stopStream API
+    methods, with automatic buffering and state management.
+    """
+
+    def __init__(
+        self,
+        client: "WebClient",
+        *,
+        channel: str,
+        logger: logging.Logger,
+        thread_ts: str,
+        buffer_size: int,
+        recipient_team_id: Optional[str] = None,
+        recipient_user_id: Optional[str] = None,
+        **kwargs,
+    ):
+        """Initialize a new ChatStream instance.
+
+        The __init__ method creates a unique ChatStream instance that keeps track of one chat stream.
+
+        Args:
+            client: The WebClient instance to use for API calls.
+            channel: An encoded ID that represents a channel, private group, or DM.
+            logger: A logging channel for outputs.
+            thread_ts: Provide another message's ts value to reply to. Streamed messages should always be replies to a user
+              request.
+            recipient_team_id: The encoded ID of the team the user receiving the streaming text belongs to. Required when
+              streaming to channels.
+            recipient_user_id: The encoded ID of the user to receive the streaming text. Required when streaming to channels.
+            buffer_size: The length of markdown_text to buffer in-memory before calling a method. Increasing this value
+              decreases the number of method calls made for the same amount of text, which is useful to avoid rate limits.
+            **kwargs: Additional arguments passed to the underlying API calls.
+        """
+        self._client = client
+        self._logger = logger
+        self._token: Optional[str] = kwargs.pop("token", None)
+        self._stream_args = {
+            "channel": channel,
+            "thread_ts": thread_ts,
+            "recipient_team_id": recipient_team_id,
+            "recipient_user_id": recipient_user_id,
+            **kwargs,
+        }
+        self._buffer = ""
+        self._state = "starting"
+        self._stream_ts: Optional[str] = None
+        self._buffer_size = buffer_size
+
+    def append(
+        self,
+        *,
+        markdown_text: str,
+        **kwargs,
+    ) -> Optional[SlackResponse]:
+        """Append to the stream.
+
+        The "append" method appends to the chat stream being used. This method can be called multiple times. After the stream
+        is stopped this method cannot be called.
+
+        Args:
+            markdown_text: Accepts message text formatted in markdown. Limit this field to 12,000 characters. This text is
+              what will be appended to the message received so far.
+            **kwargs: Additional arguments passed to the underlying API calls.
+
+        Returns:
+            SlackResponse if the buffer was flushed, None if buffering.
+
+        Raises:
+            SlackRequestError: If the stream is already completed.
+
+        Example:
+            ```python
+            streamer = client.chat_stream(
+                channel="C0123456789",
+                thread_ts="1700000001.123456",
+                recipient_team_id="T0123456789",
+                recipient_user_id="U0123456789",
+            )
+            streamer.append(markdown_text="**hello wo")
+            streamer.append(markdown_text="rld!**")
+            streamer.stop()
+            ```
+        """
+        if self._state == "completed":
+            raise e.SlackRequestError(f"Cannot append to stream: stream state is {self._state}")
+        if kwargs.get("token"):
+            self._token = kwargs.pop("token")
+        self._buffer += markdown_text
+        if len(self._buffer) >= self._buffer_size:
+            return self._flush_buffer(**kwargs)
+        details = {
+            "buffer_length": len(self._buffer),
+            "buffer_size": self._buffer_size,
+            "channel": self._stream_args.get("channel"),
+            "recipient_team_id": self._stream_args.get("recipient_team_id"),
+            "recipient_user_id": self._stream_args.get("recipient_user_id"),
+            "thread_ts": self._stream_args.get("thread_ts"),
+        }
+        self._logger.debug(f"ChatStream appended to buffer: {json.dumps(details)}")
+        return None
+
+    def stop(
+        self,
+        *,
+        markdown_text: Optional[str] = None,
+        blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None,
+        metadata: Optional[Union[Dict, Metadata]] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Stop the stream and finalize the message.
+
+        Args:
+            blocks: A list of blocks that will be rendered at the bottom of the finalized message.
+            markdown_text: Accepts message text formatted in markdown. Limit this field to 12,000 characters. This text is
+              what will be appended to the message received so far.
+            metadata: JSON object with event_type and event_payload fields, presented as a URL-encoded string. Metadata you
+              post to Slack is accessible to any app or user who is a member of that workspace.
+            **kwargs: Additional arguments passed to the underlying API calls.
+
+        Returns:
+            SlackResponse from the chat.stopStream API call.
+
+        Raises:
+            SlackRequestError: If the stream is already completed.
+
+        Example:
+            ```python
+            streamer = client.chat_stream(
+                channel="C0123456789",
+                thread_ts="1700000001.123456",
+                recipient_team_id="T0123456789",
+                recipient_user_id="U0123456789",
+            )
+            streamer.append(markdown_text="**hello wo")
+            streamer.append(markdown_text="rld!**")
+            streamer.stop()
+            ```
+        """
+        if self._state == "completed":
+            raise e.SlackRequestError(f"Cannot stop stream: stream state is {self._state}")
+        if kwargs.get("token"):
+            self._token = kwargs.pop("token")
+        if markdown_text:
+            self._buffer += markdown_text
+        if not self._stream_ts:
+            response = self._client.chat_startStream(
+                **self._stream_args,
+                token=self._token,
+            )
+            if not response.get("ts"):
+                raise e.SlackRequestError("Failed to stop stream: stream not started")
+            self._stream_ts = str(response["ts"])
+            self._state = "in_progress"
+        response = self._client.chat_stopStream(
+            token=self._token,
+            channel=self._stream_args["channel"],
+            ts=self._stream_ts,
+            blocks=blocks,
+            markdown_text=self._buffer,
+            metadata=metadata,
+            **kwargs,
+        )
+        self._state = "completed"
+        return response
+
+    def _flush_buffer(self, **kwargs) -> SlackResponse:
+        """Flush the internal buffer by making appropriate API calls."""
+        if not self._stream_ts:
+            response = self._client.chat_startStream(
+                **self._stream_args,
+                token=self._token,
+                **kwargs,
+                markdown_text=self._buffer,
+            )
+            self._stream_ts = response.get("ts")
+            self._state = "in_progress"
+        else:
+            response = self._client.chat_appendStream(
+                token=self._token,
+                channel=self._stream_args["channel"],
+                ts=self._stream_ts,
+                **kwargs,
+                markdown_text=self._buffer,
+            )
+        self._buffer = ""
+        return response
+
+

A helper class for streaming markdown text into a conversation using the chat streaming APIs.

+

This class provides a convenient interface for the chat.startStream, chat.appendStream, and chat.stopStream API +methods, with automatic buffering and state management.

+

Initialize a new ChatStream instance.

+

The init method creates a unique ChatStream instance that keeps track of one chat stream.

+

Args

+
+
client
+
The WebClient instance to use for API calls.
+
channel
+
An encoded ID that represents a channel, private group, or DM.
+
logger
+
A logging channel for outputs.
+
thread_ts
+
Provide another message's ts value to reply to. Streamed messages should always be replies to a user +request.
+
recipient_team_id
+
The encoded ID of the team the user receiving the streaming text belongs to. Required when +streaming to channels.
+
recipient_user_id
+
The encoded ID of the user to receive the streaming text. Required when streaming to channels.
+
buffer_size
+
The length of markdown_text to buffer in-memory before calling a method. Increasing this value +decreases the number of method calls made for the same amount of text, which is useful to avoid rate limits.
+
**kwargs
+
Additional arguments passed to the underlying API calls.
+
+

Methods

+
+
+def append(self, *, markdown_text: str, **kwargs) ‑> SlackResponse | None +
+
+
+ +Expand source code + +
def append(
+    self,
+    *,
+    markdown_text: str,
+    **kwargs,
+) -> Optional[SlackResponse]:
+    """Append to the stream.
+
+    The "append" method appends to the chat stream being used. This method can be called multiple times. After the stream
+    is stopped this method cannot be called.
+
+    Args:
+        markdown_text: Accepts message text formatted in markdown. Limit this field to 12,000 characters. This text is
+          what will be appended to the message received so far.
+        **kwargs: Additional arguments passed to the underlying API calls.
+
+    Returns:
+        SlackResponse if the buffer was flushed, None if buffering.
+
+    Raises:
+        SlackRequestError: If the stream is already completed.
+
+    Example:
+        ```python
+        streamer = client.chat_stream(
+            channel="C0123456789",
+            thread_ts="1700000001.123456",
+            recipient_team_id="T0123456789",
+            recipient_user_id="U0123456789",
+        )
+        streamer.append(markdown_text="**hello wo")
+        streamer.append(markdown_text="rld!**")
+        streamer.stop()
+        ```
+    """
+    if self._state == "completed":
+        raise e.SlackRequestError(f"Cannot append to stream: stream state is {self._state}")
+    if kwargs.get("token"):
+        self._token = kwargs.pop("token")
+    self._buffer += markdown_text
+    if len(self._buffer) >= self._buffer_size:
+        return self._flush_buffer(**kwargs)
+    details = {
+        "buffer_length": len(self._buffer),
+        "buffer_size": self._buffer_size,
+        "channel": self._stream_args.get("channel"),
+        "recipient_team_id": self._stream_args.get("recipient_team_id"),
+        "recipient_user_id": self._stream_args.get("recipient_user_id"),
+        "thread_ts": self._stream_args.get("thread_ts"),
+    }
+    self._logger.debug(f"ChatStream appended to buffer: {json.dumps(details)}")
+    return None
+
+

Append to the stream.

+

The "append" method appends to the chat stream being used. This method can be called multiple times. After the stream +is stopped this method cannot be called.

+

Args

+
+
markdown_text
+
Accepts message text formatted in markdown. Limit this field to 12,000 characters. This text is +what will be appended to the message received so far.
+
**kwargs
+
Additional arguments passed to the underlying API calls.
+
+

Returns

+

SlackResponse if the buffer was flushed, None if buffering.

+

Raises

+
+
SlackRequestError
+
If the stream is already completed.
+
+

Example

+
streamer = client.chat_stream(
+    channel="C0123456789",
+    thread_ts="1700000001.123456",
+    recipient_team_id="T0123456789",
+    recipient_user_id="U0123456789",
+)
+streamer.append(markdown_text="**hello wo")
+streamer.append(markdown_text="rld!**")
+streamer.stop()
+
+
+
+def stop(self,
*,
markdown_text: str | None = None,
blocks: str | Sequence[Dict | Block] | None = None,
metadata: Dict | Metadata | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def stop(
+    self,
+    *,
+    markdown_text: Optional[str] = None,
+    blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None,
+    metadata: Optional[Union[Dict, Metadata]] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Stop the stream and finalize the message.
+
+    Args:
+        blocks: A list of blocks that will be rendered at the bottom of the finalized message.
+        markdown_text: Accepts message text formatted in markdown. Limit this field to 12,000 characters. This text is
+          what will be appended to the message received so far.
+        metadata: JSON object with event_type and event_payload fields, presented as a URL-encoded string. Metadata you
+          post to Slack is accessible to any app or user who is a member of that workspace.
+        **kwargs: Additional arguments passed to the underlying API calls.
+
+    Returns:
+        SlackResponse from the chat.stopStream API call.
+
+    Raises:
+        SlackRequestError: If the stream is already completed.
+
+    Example:
+        ```python
+        streamer = client.chat_stream(
+            channel="C0123456789",
+            thread_ts="1700000001.123456",
+            recipient_team_id="T0123456789",
+            recipient_user_id="U0123456789",
+        )
+        streamer.append(markdown_text="**hello wo")
+        streamer.append(markdown_text="rld!**")
+        streamer.stop()
+        ```
+    """
+    if self._state == "completed":
+        raise e.SlackRequestError(f"Cannot stop stream: stream state is {self._state}")
+    if kwargs.get("token"):
+        self._token = kwargs.pop("token")
+    if markdown_text:
+        self._buffer += markdown_text
+    if not self._stream_ts:
+        response = self._client.chat_startStream(
+            **self._stream_args,
+            token=self._token,
+        )
+        if not response.get("ts"):
+            raise e.SlackRequestError("Failed to stop stream: stream not started")
+        self._stream_ts = str(response["ts"])
+        self._state = "in_progress"
+    response = self._client.chat_stopStream(
+        token=self._token,
+        channel=self._stream_args["channel"],
+        ts=self._stream_ts,
+        blocks=blocks,
+        markdown_text=self._buffer,
+        metadata=metadata,
+        **kwargs,
+    )
+    self._state = "completed"
+    return response
+
+

Stop the stream and finalize the message.

+

Args

+
+
blocks
+
A list of blocks that will be rendered at the bottom of the finalized message.
+
markdown_text
+
Accepts message text formatted in markdown. Limit this field to 12,000 characters. This text is +what will be appended to the message received so far.
+
metadata
+
JSON object with event_type and event_payload fields, presented as a URL-encoded string. Metadata you +post to Slack is accessible to any app or user who is a member of that workspace.
+
**kwargs
+
Additional arguments passed to the underlying API calls.
+
+

Returns

+

SlackResponse from the chat.stopStream API call.

+

Raises

+
+
SlackRequestError
+
If the stream is already completed.
+
+

Example

+
streamer = client.chat_stream(
+    channel="C0123456789",
+    thread_ts="1700000001.123456",
+    recipient_team_id="T0123456789",
+    recipient_user_id="U0123456789",
+)
+streamer.append(markdown_text="**hello wo")
+streamer.append(markdown_text="rld!**")
+streamer.stop()
+
+
+
+
+
+
+
+ +
+ + + diff --git a/docs/reference/web/client.html b/docs/reference/web/client.html new file mode 100644 index 000000000..6fded11e1 --- /dev/null +++ b/docs/reference/web/client.html @@ -0,0 +1,15787 @@ + + + + + + +slack_sdk.web.client API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.web.client

+
+
+

A Python module for interacting with Slack's Web API.

+
+
+
+
+
+
+
+
+

Classes

+
+
+class WebClient +(token: str | None = None,
base_url: str = 'https://slack.com/api/',
timeout: int = 30,
ssl: ssl.SSLContext | None = None,
proxy: str | None = None,
headers: dict | None = None,
user_agent_prefix: str | None = None,
user_agent_suffix: str | None = None,
team_id: str | None = None,
logger: logging.Logger | None = None,
retry_handlers: List[RetryHandler] | None = None)
+
+
+
+ +Expand source code + +
class WebClient(BaseClient):
+    """A WebClient allows apps to communicate with the Slack Platform's Web API.
+
+    https://docs.slack.dev/reference/methods
+
+    The Slack Web API is an interface for querying information from
+    and enacting change in a Slack workspace.
+
+    This client handles constructing and sending HTTP requests to Slack
+    as well as parsing any responses received into a `SlackResponse`.
+
+    Attributes:
+        token (str): A string specifying an `xoxp-*` or `xoxb-*` token.
+        base_url (str): A string representing the Slack API base URL.
+            Default is `'https://slack.com/api/'`
+        timeout (int): The maximum number of seconds the client will wait
+            to connect and receive a response from Slack.
+            Default is 30 seconds.
+        ssl (SSLContext): An [`ssl.SSLContext`][1] instance, helpful for specifying
+            your own custom certificate chain.
+        proxy (str): String representing a fully-qualified URL to a proxy through
+            which to route all requests to the Slack API. Even if this parameter
+            is not specified, if any of the following environment variables are
+            present, they will be loaded into this parameter: `HTTPS_PROXY`,
+            `https_proxy`, `HTTP_PROXY` or `http_proxy`.
+        headers (dict): Additional request headers to attach to all requests.
+
+    Methods:
+        `api_call`: Constructs a request and executes the API call to Slack.
+
+    Example of recommended usage:
+    ```python
+        import os
+        from slack_sdk import WebClient
+
+        client = WebClient(token=os.environ['SLACK_API_TOKEN'])
+        response = client.chat_postMessage(
+            channel='#random',
+            text="Hello world!")
+        assert response["ok"]
+        assert response["message"]["text"] == "Hello world!"
+    ```
+
+    Example manually creating an API request:
+    ```python
+        import os
+        from slack_sdk import WebClient
+
+        client = WebClient(token=os.environ['SLACK_API_TOKEN'])
+        response = client.api_call(
+            api_method='chat.postMessage',
+            json={'channel': '#random','text': "Hello world!"}
+        )
+        assert response["ok"]
+        assert response["message"]["text"] == "Hello world!"
+    ```
+
+    Note:
+        Any attributes or methods prefixed with _underscores are
+        intended to be "private" internal use only. They may be changed or
+        removed at anytime.
+
+    [1]: https://docs.python.org/3/library/ssl.html#ssl.SSLContext
+    """
+
+    def admin_analytics_getFile(
+        self,
+        *,
+        type: str,
+        date: Optional[str] = None,
+        metadata_only: Optional[bool] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Retrieve analytics data for a given date, presented as a compressed JSON file
+        https://docs.slack.dev/reference/methods/admin.analytics.getFile
+        """
+        kwargs.update({"type": type})
+        if date is not None:
+            kwargs.update({"date": date})
+        if metadata_only is not None:
+            kwargs.update({"metadata_only": metadata_only})
+        return self.api_call("admin.analytics.getFile", params=kwargs)
+
+    def admin_apps_approve(
+        self,
+        *,
+        app_id: Optional[str] = None,
+        request_id: Optional[str] = None,
+        enterprise_id: Optional[str] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Approve an app for installation on a workspace.
+        Either app_id or request_id is required.
+        These IDs can be obtained either directly via the app_requested event,
+        or by the admin.apps.requests.list method.
+        https://docs.slack.dev/reference/methods/admin.apps.approve
+        """
+        if app_id:
+            kwargs.update({"app_id": app_id})
+        elif request_id:
+            kwargs.update({"request_id": request_id})
+        else:
+            raise e.SlackRequestError("The app_id or request_id argument must be specified.")
+
+        kwargs.update(
+            {
+                "enterprise_id": enterprise_id,
+                "team_id": team_id,
+            }
+        )
+        return self.api_call("admin.apps.approve", params=kwargs)
+
+    def admin_apps_approved_list(
+        self,
+        *,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        enterprise_id: Optional[str] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """List approved apps for an org or workspace.
+        https://docs.slack.dev/reference/methods/admin.apps.approved.list
+        """
+        kwargs.update(
+            {
+                "cursor": cursor,
+                "limit": limit,
+                "enterprise_id": enterprise_id,
+                "team_id": team_id,
+            }
+        )
+        return self.api_call("admin.apps.approved.list", http_verb="GET", params=kwargs)
+
+    def admin_apps_clearResolution(
+        self,
+        *,
+        app_id: str,
+        enterprise_id: Optional[str] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Clear an app resolution
+        https://docs.slack.dev/reference/methods/admin.apps.clearResolution
+        """
+        kwargs.update(
+            {
+                "app_id": app_id,
+                "enterprise_id": enterprise_id,
+                "team_id": team_id,
+            }
+        )
+        return self.api_call("admin.apps.clearResolution", http_verb="POST", params=kwargs)
+
+    def admin_apps_requests_cancel(
+        self,
+        *,
+        request_id: str,
+        enterprise_id: Optional[str] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """List app requests for a team/workspace.
+        https://docs.slack.dev/reference/methods/admin.apps.requests.cancel
+        """
+        kwargs.update(
+            {
+                "request_id": request_id,
+                "enterprise_id": enterprise_id,
+                "team_id": team_id,
+            }
+        )
+        return self.api_call("admin.apps.requests.cancel", http_verb="POST", params=kwargs)
+
+    def admin_apps_requests_list(
+        self,
+        *,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """List app requests for a team/workspace.
+        https://docs.slack.dev/reference/methods/admin.apps.requests.list
+        """
+        kwargs.update(
+            {
+                "cursor": cursor,
+                "limit": limit,
+                "team_id": team_id,
+            }
+        )
+        return self.api_call("admin.apps.requests.list", http_verb="GET", params=kwargs)
+
+    def admin_apps_restrict(
+        self,
+        *,
+        app_id: Optional[str] = None,
+        request_id: Optional[str] = None,
+        enterprise_id: Optional[str] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Restrict an app for installation on a workspace.
+        Exactly one of the team_id or enterprise_id arguments is required, not both.
+        Either app_id or request_id is required. These IDs can be obtained either directly
+        via the app_requested event, or by the admin.apps.requests.list method.
+        https://docs.slack.dev/reference/methods/admin.apps.restrict
+        """
+        if app_id:
+            kwargs.update({"app_id": app_id})
+        elif request_id:
+            kwargs.update({"request_id": request_id})
+        else:
+            raise e.SlackRequestError("The app_id or request_id argument must be specified.")
+
+        kwargs.update(
+            {
+                "enterprise_id": enterprise_id,
+                "team_id": team_id,
+            }
+        )
+        return self.api_call("admin.apps.restrict", params=kwargs)
+
+    def admin_apps_restricted_list(
+        self,
+        *,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        enterprise_id: Optional[str] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """List restricted apps for an org or workspace.
+        https://docs.slack.dev/reference/methods/admin.apps.restricted.list
+        """
+        kwargs.update(
+            {
+                "cursor": cursor,
+                "limit": limit,
+                "enterprise_id": enterprise_id,
+                "team_id": team_id,
+            }
+        )
+        return self.api_call("admin.apps.restricted.list", http_verb="GET", params=kwargs)
+
+    def admin_apps_uninstall(
+        self,
+        *,
+        app_id: str,
+        enterprise_id: Optional[str] = None,
+        team_ids: Optional[Union[str, Sequence[str]]] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Uninstall an app from one or many workspaces, or an entire enterprise organization.
+        With an org-level token, enterprise_id or team_ids is required.
+        https://docs.slack.dev/reference/methods/admin.apps.uninstall
+        """
+        kwargs.update({"app_id": app_id})
+        if enterprise_id is not None:
+            kwargs.update({"enterprise_id": enterprise_id})
+        if team_ids is not None:
+            if isinstance(team_ids, (list, tuple)):
+                kwargs.update({"team_ids": ",".join(team_ids)})
+            else:
+                kwargs.update({"team_ids": team_ids})
+        return self.api_call("admin.apps.uninstall", http_verb="POST", params=kwargs)
+
+    def admin_apps_activities_list(
+        self,
+        *,
+        app_id: Optional[str] = None,
+        component_id: Optional[str] = None,
+        component_type: Optional[str] = None,
+        log_event_type: Optional[str] = None,
+        max_date_created: Optional[int] = None,
+        min_date_created: Optional[int] = None,
+        min_log_level: Optional[str] = None,
+        sort_direction: Optional[str] = None,
+        source: Optional[str] = None,
+        team_id: Optional[str] = None,
+        trace_id: Optional[str] = None,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Get logs for a specified team/org
+        https://docs.slack.dev/reference/methods/admin.apps.activities.list
+        """
+        kwargs.update(
+            {
+                "app_id": app_id,
+                "component_id": component_id,
+                "component_type": component_type,
+                "log_event_type": log_event_type,
+                "max_date_created": max_date_created,
+                "min_date_created": min_date_created,
+                "min_log_level": min_log_level,
+                "sort_direction": sort_direction,
+                "source": source,
+                "team_id": team_id,
+                "trace_id": trace_id,
+                "cursor": cursor,
+                "limit": limit,
+            }
+        )
+        return self.api_call("admin.apps.activities.list", params=kwargs)
+
+    def admin_apps_config_lookup(
+        self,
+        *,
+        app_ids: Union[str, Sequence[str]],
+        **kwargs,
+    ) -> SlackResponse:
+        """Look up the app config for connectors by their IDs
+        https://docs.slack.dev/reference/methods/admin.apps.config.lookup
+        """
+        if isinstance(app_ids, (list, tuple)):
+            kwargs.update({"app_ids": ",".join(app_ids)})
+        else:
+            kwargs.update({"app_ids": app_ids})
+        return self.api_call("admin.apps.config.lookup", params=kwargs)
+
+    def admin_apps_config_set(
+        self,
+        *,
+        app_id: str,
+        domain_restrictions: Optional[Dict[str, Any]] = None,
+        workflow_auth_strategy: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Set the app config for a connector
+        https://docs.slack.dev/reference/methods/admin.apps.config.set
+        """
+        kwargs.update(
+            {
+                "app_id": app_id,
+                "workflow_auth_strategy": workflow_auth_strategy,
+            }
+        )
+        if domain_restrictions is not None:
+            kwargs.update({"domain_restrictions": json.dumps(domain_restrictions)})
+        return self.api_call("admin.apps.config.set", params=kwargs)
+
+    def admin_auth_policy_getEntities(
+        self,
+        *,
+        policy_name: str,
+        cursor: Optional[str] = None,
+        entity_type: Optional[str] = None,
+        limit: Optional[int] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Fetch all the entities assigned to a particular authentication policy by name.
+        https://docs.slack.dev/reference/methods/admin.auth.policy.getEntities
+        """
+        kwargs.update({"policy_name": policy_name})
+        if cursor is not None:
+            kwargs.update({"cursor": cursor})
+        if entity_type is not None:
+            kwargs.update({"entity_type": entity_type})
+        if limit is not None:
+            kwargs.update({"limit": limit})
+        return self.api_call("admin.auth.policy.getEntities", http_verb="POST", params=kwargs)
+
+    def admin_auth_policy_assignEntities(
+        self,
+        *,
+        entity_ids: Union[str, Sequence[str]],
+        policy_name: str,
+        entity_type: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Assign entities to a particular authentication policy.
+        https://docs.slack.dev/reference/methods/admin.auth.policy.assignEntities
+        """
+        if isinstance(entity_ids, (list, tuple)):
+            kwargs.update({"entity_ids": ",".join(entity_ids)})
+        else:
+            kwargs.update({"entity_ids": entity_ids})
+        kwargs.update({"policy_name": policy_name})
+        kwargs.update({"entity_type": entity_type})
+        return self.api_call("admin.auth.policy.assignEntities", http_verb="POST", params=kwargs)
+
+    def admin_auth_policy_removeEntities(
+        self,
+        *,
+        entity_ids: Union[str, Sequence[str]],
+        policy_name: str,
+        entity_type: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Remove specified entities from a specified authentication policy.
+        https://docs.slack.dev/reference/methods/admin.auth.policy.removeEntities
+        """
+        if isinstance(entity_ids, (list, tuple)):
+            kwargs.update({"entity_ids": ",".join(entity_ids)})
+        else:
+            kwargs.update({"entity_ids": entity_ids})
+        kwargs.update({"policy_name": policy_name})
+        kwargs.update({"entity_type": entity_type})
+        return self.api_call("admin.auth.policy.removeEntities", http_verb="POST", params=kwargs)
+
+    def admin_conversations_createForObjects(
+        self,
+        *,
+        object_id: str,
+        salesforce_org_id: str,
+        invite_object_team: Optional[bool] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Create a Salesforce channel for the corresponding object provided.
+        https://docs.slack.dev/reference/methods/admin.conversations.createForObjects
+        """
+        kwargs.update(
+            {"object_id": object_id, "salesforce_org_id": salesforce_org_id, "invite_object_team": invite_object_team}
+        )
+        return self.api_call("admin.conversations.createForObjects", params=kwargs)
+
+    def admin_conversations_linkObjects(
+        self,
+        *,
+        channel: str,
+        record_id: str,
+        salesforce_org_id: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Link a Salesforce record to a channel.
+        https://docs.slack.dev/reference/methods/admin.conversations.linkObjects
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "record_id": record_id,
+                "salesforce_org_id": salesforce_org_id,
+            }
+        )
+        return self.api_call("admin.conversations.linkObjects", params=kwargs)
+
+    def admin_conversations_unlinkObjects(
+        self,
+        *,
+        channel: str,
+        new_name: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Unlink a Salesforce record from a channel.
+        https://docs.slack.dev/reference/methods/admin.conversations.unlinkObjects
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "new_name": new_name,
+            }
+        )
+        return self.api_call("admin.conversations.unlinkObjects", params=kwargs)
+
+    def admin_barriers_create(
+        self,
+        *,
+        barriered_from_usergroup_ids: Union[str, Sequence[str]],
+        primary_usergroup_id: str,
+        restricted_subjects: Union[str, Sequence[str]],
+        **kwargs,
+    ) -> SlackResponse:
+        """Create an Information Barrier
+        https://docs.slack.dev/reference/methods/admin.barriers.create
+        """
+        kwargs.update({"primary_usergroup_id": primary_usergroup_id})
+        if isinstance(barriered_from_usergroup_ids, (list, tuple)):
+            kwargs.update({"barriered_from_usergroup_ids": ",".join(barriered_from_usergroup_ids)})
+        else:
+            kwargs.update({"barriered_from_usergroup_ids": barriered_from_usergroup_ids})
+        if isinstance(restricted_subjects, (list, tuple)):
+            kwargs.update({"restricted_subjects": ",".join(restricted_subjects)})
+        else:
+            kwargs.update({"restricted_subjects": restricted_subjects})
+        return self.api_call("admin.barriers.create", http_verb="POST", params=kwargs)
+
+    def admin_barriers_delete(
+        self,
+        *,
+        barrier_id: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Delete an existing Information Barrier
+        https://docs.slack.dev/reference/methods/admin.barriers.delete
+        """
+        kwargs.update({"barrier_id": barrier_id})
+        return self.api_call("admin.barriers.delete", http_verb="POST", params=kwargs)
+
+    def admin_barriers_update(
+        self,
+        *,
+        barrier_id: str,
+        barriered_from_usergroup_ids: Union[str, Sequence[str]],
+        primary_usergroup_id: str,
+        restricted_subjects: Union[str, Sequence[str]],
+        **kwargs,
+    ) -> SlackResponse:
+        """Update an existing Information Barrier
+        https://docs.slack.dev/reference/methods/admin.barriers.update
+        """
+        kwargs.update({"barrier_id": barrier_id, "primary_usergroup_id": primary_usergroup_id})
+        if isinstance(barriered_from_usergroup_ids, (list, tuple)):
+            kwargs.update({"barriered_from_usergroup_ids": ",".join(barriered_from_usergroup_ids)})
+        else:
+            kwargs.update({"barriered_from_usergroup_ids": barriered_from_usergroup_ids})
+        if isinstance(restricted_subjects, (list, tuple)):
+            kwargs.update({"restricted_subjects": ",".join(restricted_subjects)})
+        else:
+            kwargs.update({"restricted_subjects": restricted_subjects})
+        return self.api_call("admin.barriers.update", http_verb="POST", params=kwargs)
+
+    def admin_barriers_list(
+        self,
+        *,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Get all Information Barriers for your organization
+        https://docs.slack.dev/reference/methods/admin.barriers.list"""
+        kwargs.update(
+            {
+                "cursor": cursor,
+                "limit": limit,
+            }
+        )
+        return self.api_call("admin.barriers.list", http_verb="GET", params=kwargs)
+
+    def admin_conversations_create(
+        self,
+        *,
+        is_private: bool,
+        name: str,
+        description: Optional[str] = None,
+        org_wide: Optional[bool] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Create a public or private channel-based conversation.
+        https://docs.slack.dev/reference/methods/admin.conversations.create
+        """
+        kwargs.update(
+            {
+                "is_private": is_private,
+                "name": name,
+                "description": description,
+                "org_wide": org_wide,
+                "team_id": team_id,
+            }
+        )
+        return self.api_call("admin.conversations.create", params=kwargs)
+
+    def admin_conversations_delete(
+        self,
+        *,
+        channel_id: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Delete a public or private channel.
+        https://docs.slack.dev/reference/methods/admin.conversations.delete
+        """
+        kwargs.update({"channel_id": channel_id})
+        return self.api_call("admin.conversations.delete", params=kwargs)
+
+    def admin_conversations_invite(
+        self,
+        *,
+        channel_id: str,
+        user_ids: Union[str, Sequence[str]],
+        **kwargs,
+    ) -> SlackResponse:
+        """Invite a user to a public or private channel.
+        https://docs.slack.dev/reference/methods/admin.conversations.invite
+        """
+        kwargs.update({"channel_id": channel_id})
+        if isinstance(user_ids, (list, tuple)):
+            kwargs.update({"user_ids": ",".join(user_ids)})
+        else:
+            kwargs.update({"user_ids": user_ids})
+        # NOTE: the endpoint is unable to handle Content-Type: application/json as of Sep 3, 2020.
+        return self.api_call("admin.conversations.invite", params=kwargs)
+
+    def admin_conversations_archive(
+        self,
+        *,
+        channel_id: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Archive a public or private channel.
+        https://docs.slack.dev/reference/methods/admin.conversations.archive
+        """
+        kwargs.update({"channel_id": channel_id})
+        return self.api_call("admin.conversations.archive", params=kwargs)
+
+    def admin_conversations_unarchive(
+        self,
+        *,
+        channel_id: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Unarchive a public or private channel.
+        https://docs.slack.dev/reference/methods/admin.conversations.archive
+        """
+        kwargs.update({"channel_id": channel_id})
+        return self.api_call("admin.conversations.unarchive", params=kwargs)
+
+    def admin_conversations_rename(
+        self,
+        *,
+        channel_id: str,
+        name: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Rename a public or private channel.
+        https://docs.slack.dev/reference/methods/admin.conversations.rename
+        """
+        kwargs.update({"channel_id": channel_id, "name": name})
+        return self.api_call("admin.conversations.rename", params=kwargs)
+
+    def admin_conversations_search(
+        self,
+        *,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        query: Optional[str] = None,
+        search_channel_types: Optional[Union[str, Sequence[str]]] = None,
+        sort: Optional[str] = None,
+        sort_dir: Optional[str] = None,
+        team_ids: Optional[Union[str, Sequence[str]]] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Search for public or private channels in an Enterprise organization.
+        https://docs.slack.dev/reference/methods/admin.conversations.search
+        """
+        kwargs.update(
+            {
+                "cursor": cursor,
+                "limit": limit,
+                "query": query,
+                "sort": sort,
+                "sort_dir": sort_dir,
+            }
+        )
+
+        if isinstance(search_channel_types, (list, tuple)):
+            kwargs.update({"search_channel_types": ",".join(search_channel_types)})
+        else:
+            kwargs.update({"search_channel_types": search_channel_types})
+
+        if isinstance(team_ids, (list, tuple)):
+            kwargs.update({"team_ids": ",".join(team_ids)})
+        else:
+            kwargs.update({"team_ids": team_ids})
+
+        return self.api_call("admin.conversations.search", params=kwargs)
+
+    def admin_conversations_convertToPrivate(
+        self,
+        *,
+        channel_id: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Convert a public channel to a private channel.
+        https://docs.slack.dev/reference/methods/admin.conversations.convertToPrivate
+        """
+        kwargs.update({"channel_id": channel_id})
+        return self.api_call("admin.conversations.convertToPrivate", params=kwargs)
+
+    def admin_conversations_convertToPublic(
+        self,
+        *,
+        channel_id: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Convert a privte channel to a public channel.
+        https://docs.slack.dev/reference/methods/admin.conversations.convertToPublic
+        """
+        kwargs.update({"channel_id": channel_id})
+        return self.api_call("admin.conversations.convertToPublic", params=kwargs)
+
+    def admin_conversations_setConversationPrefs(
+        self,
+        *,
+        channel_id: str,
+        prefs: Union[str, Dict[str, str]],
+        **kwargs,
+    ) -> SlackResponse:
+        """Set the posting permissions for a public or private channel.
+        https://docs.slack.dev/reference/methods/admin.conversations.setConversationPrefs
+        """
+        kwargs.update({"channel_id": channel_id})
+        if isinstance(prefs, dict):
+            kwargs.update({"prefs": json.dumps(prefs)})
+        else:
+            kwargs.update({"prefs": prefs})
+        return self.api_call("admin.conversations.setConversationPrefs", params=kwargs)
+
+    def admin_conversations_getConversationPrefs(
+        self,
+        *,
+        channel_id: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Get conversation preferences for a public or private channel.
+        https://docs.slack.dev/reference/methods/admin.conversations.getConversationPrefs
+        """
+        kwargs.update({"channel_id": channel_id})
+        return self.api_call("admin.conversations.getConversationPrefs", params=kwargs)
+
+    def admin_conversations_disconnectShared(
+        self,
+        *,
+        channel_id: str,
+        leaving_team_ids: Optional[Union[str, Sequence[str]]] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Disconnect a connected channel from one or more workspaces.
+        https://docs.slack.dev/reference/methods/admin.conversations.disconnectShared
+        """
+        kwargs.update({"channel_id": channel_id})
+        if isinstance(leaving_team_ids, (list, tuple)):
+            kwargs.update({"leaving_team_ids": ",".join(leaving_team_ids)})
+        else:
+            kwargs.update({"leaving_team_ids": leaving_team_ids})
+        return self.api_call("admin.conversations.disconnectShared", params=kwargs)
+
+    def admin_conversations_lookup(
+        self,
+        *,
+        last_message_activity_before: int,
+        team_ids: Union[str, Sequence[str]],
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        max_member_count: Optional[int] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Returns channels on the given team using the filters.
+        https://docs.slack.dev/reference/methods/admin.conversations.lookup
+        """
+        kwargs.update(
+            {
+                "last_message_activity_before": last_message_activity_before,
+                "cursor": cursor,
+                "limit": limit,
+                "max_member_count": max_member_count,
+            }
+        )
+        if isinstance(team_ids, (list, tuple)):
+            kwargs.update({"team_ids": ",".join(team_ids)})
+        else:
+            kwargs.update({"team_ids": team_ids})
+        return self.api_call("admin.conversations.lookup", params=kwargs)
+
+    def admin_conversations_ekm_listOriginalConnectedChannelInfo(
+        self,
+        *,
+        channel_ids: Optional[Union[str, Sequence[str]]] = None,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        team_ids: Optional[Union[str, Sequence[str]]] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """List all disconnected channels—i.e.,
+        channels that were once connected to other workspaces and then disconnected—and
+        the corresponding original channel IDs for key revocation with EKM.
+        https://docs.slack.dev/reference/methods/admin.conversations.ekm.listOriginalConnectedChannelInfo
+        """
+        kwargs.update(
+            {
+                "cursor": cursor,
+                "limit": limit,
+            }
+        )
+        if isinstance(channel_ids, (list, tuple)):
+            kwargs.update({"channel_ids": ",".join(channel_ids)})
+        else:
+            kwargs.update({"channel_ids": channel_ids})
+        if isinstance(team_ids, (list, tuple)):
+            kwargs.update({"team_ids": ",".join(team_ids)})
+        else:
+            kwargs.update({"team_ids": team_ids})
+        return self.api_call("admin.conversations.ekm.listOriginalConnectedChannelInfo", params=kwargs)
+
+    def admin_conversations_restrictAccess_addGroup(
+        self,
+        *,
+        channel_id: str,
+        group_id: str,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Add an allowlist of IDP groups for accessing a channel.
+        https://docs.slack.dev/reference/methods/admin.conversations.restrictAccess.addGroup
+        """
+        kwargs.update(
+            {
+                "channel_id": channel_id,
+                "group_id": group_id,
+                "team_id": team_id,
+            }
+        )
+        return self.api_call(
+            "admin.conversations.restrictAccess.addGroup",
+            http_verb="GET",
+            params=kwargs,
+        )
+
+    def admin_conversations_restrictAccess_listGroups(
+        self,
+        *,
+        channel_id: str,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """List all IDP Groups linked to a channel.
+        https://docs.slack.dev/reference/methods/admin.conversations.restrictAccess.listGroups
+        """
+        kwargs.update(
+            {
+                "channel_id": channel_id,
+                "team_id": team_id,
+            }
+        )
+        return self.api_call(
+            "admin.conversations.restrictAccess.listGroups",
+            http_verb="GET",
+            params=kwargs,
+        )
+
+    def admin_conversations_restrictAccess_removeGroup(
+        self,
+        *,
+        channel_id: str,
+        group_id: str,
+        team_id: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Remove a linked IDP group linked from a private channel.
+        https://docs.slack.dev/reference/methods/admin.conversations.restrictAccess.removeGroup
+        """
+        kwargs.update(
+            {
+                "channel_id": channel_id,
+                "group_id": group_id,
+                "team_id": team_id,
+            }
+        )
+        return self.api_call(
+            "admin.conversations.restrictAccess.removeGroup",
+            http_verb="GET",
+            params=kwargs,
+        )
+
+    def admin_conversations_setTeams(
+        self,
+        *,
+        channel_id: str,
+        org_channel: Optional[bool] = None,
+        target_team_ids: Optional[Union[str, Sequence[str]]] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Set the workspaces in an Enterprise grid org that connect to a public or private channel.
+        https://docs.slack.dev/reference/methods/admin.conversations.setTeams
+        """
+        kwargs.update(
+            {
+                "channel_id": channel_id,
+                "org_channel": org_channel,
+                "team_id": team_id,
+            }
+        )
+        if isinstance(target_team_ids, (list, tuple)):
+            kwargs.update({"target_team_ids": ",".join(target_team_ids)})
+        else:
+            kwargs.update({"target_team_ids": target_team_ids})
+        return self.api_call("admin.conversations.setTeams", params=kwargs)
+
+    def admin_conversations_getTeams(
+        self,
+        *,
+        channel_id: str,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Set the workspaces in an Enterprise grid org that connect to a channel.
+        https://docs.slack.dev/reference/methods/admin.conversations.getTeams
+        """
+        kwargs.update(
+            {
+                "channel_id": channel_id,
+                "cursor": cursor,
+                "limit": limit,
+            }
+        )
+        return self.api_call("admin.conversations.getTeams", params=kwargs)
+
+    def admin_conversations_getCustomRetention(
+        self,
+        *,
+        channel_id: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Get a channel's retention policy
+        https://docs.slack.dev/reference/methods/admin.conversations.getCustomRetention
+        """
+        kwargs.update({"channel_id": channel_id})
+        return self.api_call("admin.conversations.getCustomRetention", params=kwargs)
+
+    def admin_conversations_removeCustomRetention(
+        self,
+        *,
+        channel_id: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Remove a channel's retention policy
+        https://docs.slack.dev/reference/methods/admin.conversations.removeCustomRetention
+        """
+        kwargs.update({"channel_id": channel_id})
+        return self.api_call("admin.conversations.removeCustomRetention", params=kwargs)
+
+    def admin_conversations_setCustomRetention(
+        self,
+        *,
+        channel_id: str,
+        duration_days: int,
+        **kwargs,
+    ) -> SlackResponse:
+        """Set a channel's retention policy
+        https://docs.slack.dev/reference/methods/admin.conversations.setCustomRetention
+        """
+        kwargs.update({"channel_id": channel_id, "duration_days": duration_days})
+        return self.api_call("admin.conversations.setCustomRetention", params=kwargs)
+
+    def admin_conversations_bulkArchive(
+        self,
+        *,
+        channel_ids: Union[Sequence[str], str],
+        **kwargs,
+    ) -> SlackResponse:
+        """Archive public or private channels in bulk.
+        https://docs.slack.dev/reference/methods/admin.conversations.bulkArchive
+        """
+        kwargs.update({"channel_ids": ",".join(channel_ids) if isinstance(channel_ids, (list, tuple)) else channel_ids})
+        return self.api_call("admin.conversations.bulkArchive", params=kwargs)
+
+    def admin_conversations_bulkDelete(
+        self,
+        *,
+        channel_ids: Union[Sequence[str], str],
+        **kwargs,
+    ) -> SlackResponse:
+        """Delete public or private channels in bulk.
+        https://slack.com/api/admin.conversations.bulkDelete
+        """
+        kwargs.update({"channel_ids": ",".join(channel_ids) if isinstance(channel_ids, (list, tuple)) else channel_ids})
+        return self.api_call("admin.conversations.bulkDelete", params=kwargs)
+
+    def admin_conversations_bulkMove(
+        self,
+        *,
+        channel_ids: Union[Sequence[str], str],
+        target_team_id: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Move public or private channels in bulk.
+        https://docs.slack.dev/reference/methods/admin.conversations.bulkMove
+        """
+        kwargs.update(
+            {
+                "target_team_id": target_team_id,
+                "channel_ids": ",".join(channel_ids) if isinstance(channel_ids, (list, tuple)) else channel_ids,
+            }
+        )
+        return self.api_call("admin.conversations.bulkMove", params=kwargs)
+
+    def admin_emoji_add(
+        self,
+        *,
+        name: str,
+        url: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Add an emoji.
+        https://docs.slack.dev/reference/methods/admin.emoji.add
+        """
+        kwargs.update({"name": name, "url": url})
+        return self.api_call("admin.emoji.add", http_verb="GET", params=kwargs)
+
+    def admin_emoji_addAlias(
+        self,
+        *,
+        alias_for: str,
+        name: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Add an emoji alias.
+        https://docs.slack.dev/reference/methods/admin.emoji.addAlias
+        """
+        kwargs.update({"alias_for": alias_for, "name": name})
+        return self.api_call("admin.emoji.addAlias", http_verb="GET", params=kwargs)
+
+    def admin_emoji_list(
+        self,
+        *,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """List emoji for an Enterprise Grid organization.
+        https://docs.slack.dev/reference/methods/admin.emoji.list
+        """
+        kwargs.update({"cursor": cursor, "limit": limit})
+        return self.api_call("admin.emoji.list", http_verb="GET", params=kwargs)
+
+    def admin_emoji_remove(
+        self,
+        *,
+        name: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Remove an emoji across an Enterprise Grid organization.
+        https://docs.slack.dev/reference/methods/admin.emoji.remove
+        """
+        kwargs.update({"name": name})
+        return self.api_call("admin.emoji.remove", http_verb="GET", params=kwargs)
+
+    def admin_emoji_rename(
+        self,
+        *,
+        name: str,
+        new_name: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Rename an emoji.
+        https://docs.slack.dev/reference/methods/admin.emoji.rename
+        """
+        kwargs.update({"name": name, "new_name": new_name})
+        return self.api_call("admin.emoji.rename", http_verb="GET", params=kwargs)
+
+    def admin_functions_list(
+        self,
+        *,
+        app_ids: Union[str, Sequence[str]],
+        team_id: Optional[str] = None,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Look up functions by a set of apps
+        https://docs.slack.dev/reference/methods/admin.functions.list
+        """
+        if isinstance(app_ids, (list, tuple)):
+            kwargs.update({"app_ids": ",".join(app_ids)})
+        else:
+            kwargs.update({"app_ids": app_ids})
+        kwargs.update(
+            {
+                "team_id": team_id,
+                "cursor": cursor,
+                "limit": limit,
+            }
+        )
+        return self.api_call("admin.functions.list", params=kwargs)
+
+    def admin_functions_permissions_lookup(
+        self,
+        *,
+        function_ids: Union[str, Sequence[str]],
+        **kwargs,
+    ) -> SlackResponse:
+        """Lookup the visibility of multiple Slack functions
+        and include the users if it is limited to particular named entities.
+        https://docs.slack.dev/reference/methods/admin.functions.permissions.lookup
+        """
+        if isinstance(function_ids, (list, tuple)):
+            kwargs.update({"function_ids": ",".join(function_ids)})
+        else:
+            kwargs.update({"function_ids": function_ids})
+        return self.api_call("admin.functions.permissions.lookup", params=kwargs)
+
+    def admin_functions_permissions_set(
+        self,
+        *,
+        function_id: str,
+        visibility: str,
+        user_ids: Optional[Union[str, Sequence[str]]] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Set the visibility of a Slack function
+        and define the users or workspaces if it is set to named_entities
+        https://docs.slack.dev/reference/methods/admin.functions.permissions.set
+        """
+        kwargs.update(
+            {
+                "function_id": function_id,
+                "visibility": visibility,
+            }
+        )
+        if user_ids is not None:
+            if isinstance(user_ids, (list, tuple)):
+                kwargs.update({"user_ids": ",".join(user_ids)})
+            else:
+                kwargs.update({"user_ids": user_ids})
+        return self.api_call("admin.functions.permissions.set", params=kwargs)
+
+    def admin_roles_addAssignments(
+        self,
+        *,
+        role_id: str,
+        entity_ids: Union[str, Sequence[str]],
+        user_ids: Union[str, Sequence[str]],
+        **kwargs,
+    ) -> SlackResponse:
+        """Adds members to the specified role with the specified scopes
+        https://docs.slack.dev/reference/methods/admin.roles.addAssignments
+        """
+        kwargs.update({"role_id": role_id})
+        if isinstance(entity_ids, (list, tuple)):
+            kwargs.update({"entity_ids": ",".join(entity_ids)})
+        else:
+            kwargs.update({"entity_ids": entity_ids})
+        if isinstance(user_ids, (list, tuple)):
+            kwargs.update({"user_ids": ",".join(user_ids)})
+        else:
+            kwargs.update({"user_ids": user_ids})
+        return self.api_call("admin.roles.addAssignments", params=kwargs)
+
+    def admin_roles_listAssignments(
+        self,
+        *,
+        role_ids: Optional[Union[str, Sequence[str]]] = None,
+        entity_ids: Optional[Union[str, Sequence[str]]] = None,
+        cursor: Optional[str] = None,
+        limit: Optional[Union[str, int]] = None,
+        sort_dir: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Lists assignments for all roles across entities.
+            Options to scope results by any combination of roles or entities
+        https://docs.slack.dev/reference/methods/admin.roles.listAssignments
+        """
+        kwargs.update({"cursor": cursor, "limit": limit, "sort_dir": sort_dir})
+        if isinstance(entity_ids, (list, tuple)):
+            kwargs.update({"entity_ids": ",".join(entity_ids)})
+        else:
+            kwargs.update({"entity_ids": entity_ids})
+        if isinstance(role_ids, (list, tuple)):
+            kwargs.update({"role_ids": ",".join(role_ids)})
+        else:
+            kwargs.update({"role_ids": role_ids})
+        return self.api_call("admin.roles.listAssignments", params=kwargs)
+
+    def admin_roles_removeAssignments(
+        self,
+        *,
+        role_id: str,
+        entity_ids: Union[str, Sequence[str]],
+        user_ids: Union[str, Sequence[str]],
+        **kwargs,
+    ) -> SlackResponse:
+        """Removes a set of users from a role for the given scopes and entities
+        https://docs.slack.dev/reference/methods/admin.roles.removeAssignments
+        """
+        kwargs.update({"role_id": role_id})
+        if isinstance(entity_ids, (list, tuple)):
+            kwargs.update({"entity_ids": ",".join(entity_ids)})
+        else:
+            kwargs.update({"entity_ids": entity_ids})
+        if isinstance(user_ids, (list, tuple)):
+            kwargs.update({"user_ids": ",".join(user_ids)})
+        else:
+            kwargs.update({"user_ids": user_ids})
+        return self.api_call("admin.roles.removeAssignments", params=kwargs)
+
+    def admin_users_session_reset(
+        self,
+        *,
+        user_id: str,
+        mobile_only: Optional[bool] = None,
+        web_only: Optional[bool] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Wipes all valid sessions on all devices for a given user.
+        https://docs.slack.dev/reference/methods/admin.users.session.reset
+        """
+        kwargs.update(
+            {
+                "user_id": user_id,
+                "mobile_only": mobile_only,
+                "web_only": web_only,
+            }
+        )
+        return self.api_call("admin.users.session.reset", params=kwargs)
+
+    def admin_users_session_resetBulk(
+        self,
+        *,
+        user_ids: Union[str, Sequence[str]],
+        mobile_only: Optional[bool] = None,
+        web_only: Optional[bool] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Enqueues an asynchronous job to wipe all valid sessions on all devices for a given list of users
+        https://docs.slack.dev/reference/methods/admin.users.session.resetBulk
+        """
+        if isinstance(user_ids, (list, tuple)):
+            kwargs.update({"user_ids": ",".join(user_ids)})
+        else:
+            kwargs.update({"user_ids": user_ids})
+        kwargs.update(
+            {
+                "mobile_only": mobile_only,
+                "web_only": web_only,
+            }
+        )
+        return self.api_call("admin.users.session.resetBulk", params=kwargs)
+
+    def admin_users_session_invalidate(
+        self,
+        *,
+        session_id: str,
+        team_id: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Invalidate a single session for a user by session_id.
+        https://docs.slack.dev/reference/methods/admin.users.session.invalidate
+        """
+        kwargs.update({"session_id": session_id, "team_id": team_id})
+        return self.api_call("admin.users.session.invalidate", params=kwargs)
+
+    def admin_users_session_list(
+        self,
+        *,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        team_id: Optional[str] = None,
+        user_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Lists all active user sessions for an organization
+        https://docs.slack.dev/reference/methods/admin.users.session.list
+        """
+        kwargs.update(
+            {
+                "cursor": cursor,
+                "limit": limit,
+                "team_id": team_id,
+                "user_id": user_id,
+            }
+        )
+        return self.api_call("admin.users.session.list", params=kwargs)
+
+    def admin_teams_settings_setDefaultChannels(
+        self,
+        *,
+        team_id: str,
+        channel_ids: Union[str, Sequence[str]],
+        **kwargs,
+    ) -> SlackResponse:
+        """Set the default channels of a workspace.
+        https://docs.slack.dev/reference/methods/admin.teams.settings.setDefaultChannels
+        """
+        kwargs.update({"team_id": team_id})
+        if isinstance(channel_ids, (list, tuple)):
+            kwargs.update({"channel_ids": ",".join(channel_ids)})
+        else:
+            kwargs.update({"channel_ids": channel_ids})
+        return self.api_call("admin.teams.settings.setDefaultChannels", http_verb="GET", params=kwargs)
+
+    def admin_users_session_getSettings(
+        self,
+        *,
+        user_ids: Union[str, Sequence[str]],
+        **kwargs,
+    ) -> SlackResponse:
+        """Get user-specific session settings—the session duration
+        and what happens when the client closes—given a list of users.
+        https://docs.slack.dev/reference/methods/admin.users.session.getSettings
+        """
+        if isinstance(user_ids, (list, tuple)):
+            kwargs.update({"user_ids": ",".join(user_ids)})
+        else:
+            kwargs.update({"user_ids": user_ids})
+        return self.api_call("admin.users.session.getSettings", params=kwargs)
+
+    def admin_users_session_setSettings(
+        self,
+        *,
+        user_ids: Union[str, Sequence[str]],
+        desktop_app_browser_quit: Optional[bool] = None,
+        duration: Optional[int] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Configure the user-level session settings—the session duration
+        and what happens when the client closes—for one or more users.
+        https://docs.slack.dev/reference/methods/admin.users.session.setSettings
+        """
+        if isinstance(user_ids, (list, tuple)):
+            kwargs.update({"user_ids": ",".join(user_ids)})
+        else:
+            kwargs.update({"user_ids": user_ids})
+        kwargs.update(
+            {
+                "desktop_app_browser_quit": desktop_app_browser_quit,
+                "duration": duration,
+            }
+        )
+        return self.api_call("admin.users.session.setSettings", params=kwargs)
+
+    def admin_users_session_clearSettings(
+        self,
+        *,
+        user_ids: Union[str, Sequence[str]],
+        **kwargs,
+    ) -> SlackResponse:
+        """Clear user-specific session settings—the session duration
+        and what happens when the client closes—for a list of users.
+        https://docs.slack.dev/reference/methods/admin.users.session.clearSettings
+        """
+        if isinstance(user_ids, (list, tuple)):
+            kwargs.update({"user_ids": ",".join(user_ids)})
+        else:
+            kwargs.update({"user_ids": user_ids})
+        return self.api_call("admin.users.session.clearSettings", params=kwargs)
+
+    def admin_users_unsupportedVersions_export(
+        self,
+        *,
+        date_end_of_support: Optional[Union[str, int]] = None,
+        date_sessions_started: Optional[Union[str, int]] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Ask Slackbot to send you an export listing all workspace members using unsupported software,
+        presented as a zipped CSV file.
+        https://docs.slack.dev/reference/methods/admin.users.unsupportedVersions.export
+        """
+        kwargs.update(
+            {
+                "date_end_of_support": date_end_of_support,
+                "date_sessions_started": date_sessions_started,
+            }
+        )
+        return self.api_call("admin.users.unsupportedVersions.export", params=kwargs)
+
+    def admin_inviteRequests_approve(
+        self,
+        *,
+        invite_request_id: str,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Approve a workspace invite request.
+        https://docs.slack.dev/reference/methods/admin.inviteRequests.approve
+        """
+        kwargs.update({"invite_request_id": invite_request_id, "team_id": team_id})
+        return self.api_call("admin.inviteRequests.approve", params=kwargs)
+
+    def admin_inviteRequests_approved_list(
+        self,
+        *,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """List all approved workspace invite requests.
+        https://docs.slack.dev/reference/methods/admin.inviteRequests.approved.list
+        """
+        kwargs.update(
+            {
+                "cursor": cursor,
+                "limit": limit,
+                "team_id": team_id,
+            }
+        )
+        return self.api_call("admin.inviteRequests.approved.list", params=kwargs)
+
+    def admin_inviteRequests_denied_list(
+        self,
+        *,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """List all denied workspace invite requests.
+        https://docs.slack.dev/reference/methods/admin.inviteRequests.denied.list
+        """
+        kwargs.update(
+            {
+                "cursor": cursor,
+                "limit": limit,
+                "team_id": team_id,
+            }
+        )
+        return self.api_call("admin.inviteRequests.denied.list", params=kwargs)
+
+    def admin_inviteRequests_deny(
+        self,
+        *,
+        invite_request_id: str,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Deny a workspace invite request.
+        https://docs.slack.dev/reference/methods/admin.inviteRequests.deny
+        """
+        kwargs.update({"invite_request_id": invite_request_id, "team_id": team_id})
+        return self.api_call("admin.inviteRequests.deny", params=kwargs)
+
+    def admin_inviteRequests_list(
+        self,
+        **kwargs,
+    ) -> SlackResponse:
+        """List all pending workspace invite requests."""
+        return self.api_call("admin.inviteRequests.list", params=kwargs)
+
+    def admin_teams_admins_list(
+        self,
+        *,
+        team_id: str,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """List all of the admins on a given workspace.
+        https://docs.slack.dev/reference/methods/admin.inviteRequests.list
+        """
+        kwargs.update(
+            {
+                "cursor": cursor,
+                "limit": limit,
+                "team_id": team_id,
+            }
+        )
+        return self.api_call("admin.teams.admins.list", http_verb="GET", params=kwargs)
+
+    def admin_teams_create(
+        self,
+        *,
+        team_domain: str,
+        team_name: str,
+        team_description: Optional[str] = None,
+        team_discoverability: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Create an Enterprise team.
+        https://docs.slack.dev/reference/methods/admin.teams.create
+        """
+        kwargs.update(
+            {
+                "team_domain": team_domain,
+                "team_name": team_name,
+                "team_description": team_description,
+                "team_discoverability": team_discoverability,
+            }
+        )
+        return self.api_call("admin.teams.create", params=kwargs)
+
+    def admin_teams_list(
+        self,
+        *,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """List all teams on an Enterprise organization.
+        https://docs.slack.dev/reference/methods/admin.teams.list
+        """
+        kwargs.update({"cursor": cursor, "limit": limit})
+        return self.api_call("admin.teams.list", params=kwargs)
+
+    def admin_teams_owners_list(
+        self,
+        *,
+        team_id: str,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """List all of the admins on a given workspace.
+        https://docs.slack.dev/reference/methods/admin.teams.owners.list
+        """
+        kwargs.update({"team_id": team_id, "cursor": cursor, "limit": limit})
+        return self.api_call("admin.teams.owners.list", http_verb="GET", params=kwargs)
+
+    def admin_teams_settings_info(
+        self,
+        *,
+        team_id: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Fetch information about settings in a workspace
+        https://docs.slack.dev/reference/methods/admin.teams.settings.info
+        """
+        kwargs.update({"team_id": team_id})
+        return self.api_call("admin.teams.settings.info", params=kwargs)
+
+    def admin_teams_settings_setDescription(
+        self,
+        *,
+        team_id: str,
+        description: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Set the description of a given workspace.
+        https://docs.slack.dev/reference/methods/admin.teams.settings.setDescription
+        """
+        kwargs.update({"team_id": team_id, "description": description})
+        return self.api_call("admin.teams.settings.setDescription", params=kwargs)
+
+    def admin_teams_settings_setDiscoverability(
+        self,
+        *,
+        team_id: str,
+        discoverability: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Sets the icon of a workspace.
+        https://docs.slack.dev/reference/methods/admin.teams.settings.setDiscoverability
+        """
+        kwargs.update({"team_id": team_id, "discoverability": discoverability})
+        return self.api_call("admin.teams.settings.setDiscoverability", params=kwargs)
+
+    def admin_teams_settings_setIcon(
+        self,
+        *,
+        team_id: str,
+        image_url: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Sets the icon of a workspace.
+        https://docs.slack.dev/reference/methods/admin.teams.settings.setIcon
+        """
+        kwargs.update({"team_id": team_id, "image_url": image_url})
+        return self.api_call("admin.teams.settings.setIcon", http_verb="GET", params=kwargs)
+
+    def admin_teams_settings_setName(
+        self,
+        *,
+        team_id: str,
+        name: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Sets the icon of a workspace.
+        https://docs.slack.dev/reference/methods/admin.teams.settings.setName
+        """
+        kwargs.update({"team_id": team_id, "name": name})
+        return self.api_call("admin.teams.settings.setName", params=kwargs)
+
+    def admin_usergroups_addChannels(
+        self,
+        *,
+        channel_ids: Union[str, Sequence[str]],
+        usergroup_id: str,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Add one or more default channels to an IDP group.
+        https://docs.slack.dev/reference/methods/admin.usergroups.addChannels
+        """
+        kwargs.update({"team_id": team_id, "usergroup_id": usergroup_id})
+        if isinstance(channel_ids, (list, tuple)):
+            kwargs.update({"channel_ids": ",".join(channel_ids)})
+        else:
+            kwargs.update({"channel_ids": channel_ids})
+        return self.api_call("admin.usergroups.addChannels", params=kwargs)
+
+    def admin_usergroups_addTeams(
+        self,
+        *,
+        usergroup_id: str,
+        team_ids: Union[str, Sequence[str]],
+        auto_provision: Optional[bool] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Associate one or more default workspaces with an organization-wide IDP group.
+        https://docs.slack.dev/reference/methods/admin.usergroups.addTeams
+        """
+        kwargs.update({"usergroup_id": usergroup_id, "auto_provision": auto_provision})
+        if isinstance(team_ids, (list, tuple)):
+            kwargs.update({"team_ids": ",".join(team_ids)})
+        else:
+            kwargs.update({"team_ids": team_ids})
+        return self.api_call("admin.usergroups.addTeams", params=kwargs)
+
+    def admin_usergroups_listChannels(
+        self,
+        *,
+        usergroup_id: str,
+        include_num_members: Optional[bool] = None,
+        team_id: Optional[bool] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Add one or more default channels to an IDP group.
+        https://docs.slack.dev/reference/methods/admin.usergroups.listChannels
+        """
+        kwargs.update(
+            {
+                "usergroup_id": usergroup_id,
+                "include_num_members": include_num_members,
+                "team_id": team_id,
+            }
+        )
+        return self.api_call("admin.usergroups.listChannels", params=kwargs)
+
+    def admin_usergroups_removeChannels(
+        self,
+        *,
+        usergroup_id: str,
+        channel_ids: Union[str, Sequence[str]],
+        **kwargs,
+    ) -> SlackResponse:
+        """Add one or more default channels to an IDP group.
+        https://docs.slack.dev/reference/methods/admin.usergroups.removeChannels
+        """
+        kwargs.update({"usergroup_id": usergroup_id})
+        if isinstance(channel_ids, (list, tuple)):
+            kwargs.update({"channel_ids": ",".join(channel_ids)})
+        else:
+            kwargs.update({"channel_ids": channel_ids})
+        return self.api_call("admin.usergroups.removeChannels", params=kwargs)
+
+    def admin_users_assign(
+        self,
+        *,
+        team_id: str,
+        user_id: str,
+        channel_ids: Optional[Union[str, Sequence[str]]] = None,
+        is_restricted: Optional[bool] = None,
+        is_ultra_restricted: Optional[bool] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Add an Enterprise user to a workspace.
+        https://docs.slack.dev/reference/methods/admin.users.assign
+        """
+        kwargs.update(
+            {
+                "team_id": team_id,
+                "user_id": user_id,
+                "is_restricted": is_restricted,
+                "is_ultra_restricted": is_ultra_restricted,
+            }
+        )
+        if isinstance(channel_ids, (list, tuple)):
+            kwargs.update({"channel_ids": ",".join(channel_ids)})
+        else:
+            kwargs.update({"channel_ids": channel_ids})
+        return self.api_call("admin.users.assign", params=kwargs)
+
+    def admin_users_invite(
+        self,
+        *,
+        team_id: str,
+        email: str,
+        channel_ids: Union[str, Sequence[str]],
+        custom_message: Optional[str] = None,
+        email_password_policy_enabled: Optional[bool] = None,
+        guest_expiration_ts: Optional[Union[str, float]] = None,
+        is_restricted: Optional[bool] = None,
+        is_ultra_restricted: Optional[bool] = None,
+        real_name: Optional[str] = None,
+        resend: Optional[bool] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Invite a user to a workspace.
+        https://docs.slack.dev/reference/methods/admin.users.invite
+        """
+        kwargs.update(
+            {
+                "team_id": team_id,
+                "email": email,
+                "custom_message": custom_message,
+                "email_password_policy_enabled": email_password_policy_enabled,
+                "guest_expiration_ts": str(guest_expiration_ts) if guest_expiration_ts is not None else None,
+                "is_restricted": is_restricted,
+                "is_ultra_restricted": is_ultra_restricted,
+                "real_name": real_name,
+                "resend": resend,
+            }
+        )
+        if isinstance(channel_ids, (list, tuple)):
+            kwargs.update({"channel_ids": ",".join(channel_ids)})
+        else:
+            kwargs.update({"channel_ids": channel_ids})
+        return self.api_call("admin.users.invite", params=kwargs)
+
+    def admin_users_list(
+        self,
+        *,
+        team_id: Optional[str] = None,
+        include_deactivated_user_workspaces: Optional[bool] = None,
+        is_active: Optional[bool] = None,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """List users on a workspace
+        https://docs.slack.dev/reference/methods/admin.users.list
+        """
+        kwargs.update(
+            {
+                "team_id": team_id,
+                "include_deactivated_user_workspaces": include_deactivated_user_workspaces,
+                "is_active": is_active,
+                "cursor": cursor,
+                "limit": limit,
+            }
+        )
+        return self.api_call("admin.users.list", params=kwargs)
+
+    def admin_users_remove(
+        self,
+        *,
+        team_id: str,
+        user_id: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Remove a user from a workspace.
+        https://docs.slack.dev/reference/methods/admin.users.remove
+        """
+        kwargs.update({"team_id": team_id, "user_id": user_id})
+        return self.api_call("admin.users.remove", params=kwargs)
+
+    def admin_users_setAdmin(
+        self,
+        *,
+        team_id: str,
+        user_id: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Set an existing guest, regular user, or owner to be an admin user.
+        https://docs.slack.dev/reference/methods/admin.users.setAdmin
+        """
+        kwargs.update({"team_id": team_id, "user_id": user_id})
+        return self.api_call("admin.users.setAdmin", params=kwargs)
+
+    def admin_users_setExpiration(
+        self,
+        *,
+        expiration_ts: int,
+        user_id: str,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Set an expiration for a guest user.
+        https://docs.slack.dev/reference/methods/admin.users.setExpiration
+        """
+        kwargs.update({"expiration_ts": expiration_ts, "team_id": team_id, "user_id": user_id})
+        return self.api_call("admin.users.setExpiration", params=kwargs)
+
+    def admin_users_setOwner(
+        self,
+        *,
+        team_id: str,
+        user_id: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Set an existing guest, regular user, or admin user to be a workspace owner.
+        https://docs.slack.dev/reference/methods/admin.users.setOwner
+        """
+        kwargs.update({"team_id": team_id, "user_id": user_id})
+        return self.api_call("admin.users.setOwner", params=kwargs)
+
+    def admin_users_setRegular(
+        self,
+        *,
+        team_id: str,
+        user_id: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Set an existing guest user, admin user, or owner to be a regular user.
+        https://docs.slack.dev/reference/methods/admin.users.setRegular
+        """
+        kwargs.update({"team_id": team_id, "user_id": user_id})
+        return self.api_call("admin.users.setRegular", params=kwargs)
+
+    def admin_workflows_search(
+        self,
+        *,
+        app_id: Optional[str] = None,
+        collaborator_ids: Optional[Union[str, Sequence[str]]] = None,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        no_collaborators: Optional[bool] = None,
+        num_trigger_ids: Optional[int] = None,
+        query: Optional[str] = None,
+        sort: Optional[str] = None,
+        sort_dir: Optional[str] = None,
+        source: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Search workflows within the team or enterprise
+        https://docs.slack.dev/reference/methods/admin.workflows.search
+        """
+        if collaborator_ids is not None:
+            if isinstance(collaborator_ids, (list, tuple)):
+                kwargs.update({"collaborator_ids": ",".join(collaborator_ids)})
+            else:
+                kwargs.update({"collaborator_ids": collaborator_ids})
+        kwargs.update(
+            {
+                "app_id": app_id,
+                "cursor": cursor,
+                "limit": limit,
+                "no_collaborators": no_collaborators,
+                "num_trigger_ids": num_trigger_ids,
+                "query": query,
+                "sort": sort,
+                "sort_dir": sort_dir,
+                "source": source,
+            }
+        )
+        return self.api_call("admin.workflows.search", params=kwargs)
+
+    def admin_workflows_permissions_lookup(
+        self,
+        *,
+        workflow_ids: Union[str, Sequence[str]],
+        max_workflow_triggers: Optional[int] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Look up the permissions for a set of workflows
+        https://docs.slack.dev/reference/methods/admin.workflows.permissions.lookup
+        """
+        if isinstance(workflow_ids, (list, tuple)):
+            kwargs.update({"workflow_ids": ",".join(workflow_ids)})
+        else:
+            kwargs.update({"workflow_ids": workflow_ids})
+        kwargs.update(
+            {
+                "max_workflow_triggers": max_workflow_triggers,
+            }
+        )
+        return self.api_call("admin.workflows.permissions.lookup", params=kwargs)
+
+    def admin_workflows_collaborators_add(
+        self,
+        *,
+        collaborator_ids: Union[str, Sequence[str]],
+        workflow_ids: Union[str, Sequence[str]],
+        **kwargs,
+    ) -> SlackResponse:
+        """Add collaborators to workflows within the team or enterprise
+        https://docs.slack.dev/reference/methods/admin.workflows.collaborators.add
+        """
+        if isinstance(collaborator_ids, (list, tuple)):
+            kwargs.update({"collaborator_ids": ",".join(collaborator_ids)})
+        else:
+            kwargs.update({"collaborator_ids": collaborator_ids})
+        if isinstance(workflow_ids, (list, tuple)):
+            kwargs.update({"workflow_ids": ",".join(workflow_ids)})
+        else:
+            kwargs.update({"workflow_ids": workflow_ids})
+        return self.api_call("admin.workflows.collaborators.add", params=kwargs)
+
+    def admin_workflows_collaborators_remove(
+        self,
+        *,
+        collaborator_ids: Union[str, Sequence[str]],
+        workflow_ids: Union[str, Sequence[str]],
+        **kwargs,
+    ) -> SlackResponse:
+        """Remove collaborators from workflows within the team or enterprise
+        https://docs.slack.dev/reference/methods/admin.workflows.collaborators.remove
+        """
+        if isinstance(collaborator_ids, (list, tuple)):
+            kwargs.update({"collaborator_ids": ",".join(collaborator_ids)})
+        else:
+            kwargs.update({"collaborator_ids": collaborator_ids})
+        if isinstance(workflow_ids, (list, tuple)):
+            kwargs.update({"workflow_ids": ",".join(workflow_ids)})
+        else:
+            kwargs.update({"workflow_ids": workflow_ids})
+        return self.api_call("admin.workflows.collaborators.remove", params=kwargs)
+
+    def admin_workflows_unpublish(
+        self,
+        *,
+        workflow_ids: Union[str, Sequence[str]],
+        **kwargs,
+    ) -> SlackResponse:
+        """Unpublish workflows within the team or enterprise
+        https://docs.slack.dev/reference/methods/admin.workflows.unpublish
+        """
+        if isinstance(workflow_ids, (list, tuple)):
+            kwargs.update({"workflow_ids": ",".join(workflow_ids)})
+        else:
+            kwargs.update({"workflow_ids": workflow_ids})
+        return self.api_call("admin.workflows.unpublish", params=kwargs)
+
+    def api_test(
+        self,
+        *,
+        error: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Checks API calling code.
+        https://docs.slack.dev/reference/methods/api.test
+        """
+        kwargs.update({"error": error})
+        return self.api_call("api.test", params=kwargs)
+
+    def apps_connections_open(
+        self,
+        *,
+        app_token: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Generate a temporary Socket Mode WebSocket URL that your app can connect to
+        in order to receive events and interactive payloads
+        https://docs.slack.dev/reference/methods/apps.connections.open
+        """
+        kwargs.update({"token": app_token})
+        return self.api_call("apps.connections.open", http_verb="POST", params=kwargs)
+
+    def apps_event_authorizations_list(
+        self,
+        *,
+        event_context: str,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Get a list of authorizations for the given event context.
+        Each authorization represents an app installation that the event is visible to.
+        https://docs.slack.dev/reference/methods/apps.event.authorizations.list
+        """
+        kwargs.update({"event_context": event_context, "cursor": cursor, "limit": limit})
+        return self.api_call("apps.event.authorizations.list", params=kwargs)
+
+    def apps_uninstall(
+        self,
+        *,
+        client_id: str,
+        client_secret: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Uninstalls your app from a workspace.
+        https://docs.slack.dev/reference/methods/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://docs.slack.dev/reference/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://docs.slack.dev/reference/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://docs.slack.dev/reference/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://docs.slack.dev/reference/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://docs.slack.dev/reference/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://docs.slack.dev/reference/methods/tooling.tokens.rotate
+        """
+        kwargs.update({"refresh_token": refresh_token})
+        return self.api_call("tooling.tokens.rotate", params=kwargs)
+
+    def assistant_threads_setStatus(
+        self,
+        *,
+        channel_id: str,
+        thread_ts: str,
+        status: str,
+        loading_messages: Optional[List[str]] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Set the status for an AI assistant thread.
+        https://docs.slack.dev/reference/methods/assistant.threads.setStatus
+        """
+        kwargs.update(
+            {"channel_id": channel_id, "thread_ts": thread_ts, "status": status, "loading_messages": loading_messages}
+        )
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("assistant.threads.setStatus", json=kwargs)
+
+    def assistant_threads_setTitle(
+        self,
+        *,
+        channel_id: str,
+        thread_ts: str,
+        title: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Set the title for the given assistant thread.
+        https://docs.slack.dev/reference/methods/assistant.threads.setTitle
+        """
+        kwargs.update({"channel_id": channel_id, "thread_ts": thread_ts, "title": title})
+        return self.api_call("assistant.threads.setTitle", params=kwargs)
+
+    def assistant_threads_setSuggestedPrompts(
+        self,
+        *,
+        channel_id: str,
+        thread_ts: str,
+        title: Optional[str] = None,
+        prompts: List[Dict[str, str]],
+        **kwargs,
+    ) -> SlackResponse:
+        """Set suggested prompts for the given assistant thread.
+        https://docs.slack.dev/reference/methods/assistant.threads.setSuggestedPrompts
+        """
+        kwargs.update({"channel_id": channel_id, "thread_ts": thread_ts, "prompts": prompts})
+        if title is not None:
+            kwargs.update({"title": title})
+        return self.api_call("assistant.threads.setSuggestedPrompts", json=kwargs)
+
+    def auth_revoke(
+        self,
+        *,
+        test: Optional[bool] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Revokes a token.
+        https://docs.slack.dev/reference/methods/auth.revoke
+        """
+        kwargs.update({"test": test})
+        return self.api_call("auth.revoke", http_verb="GET", params=kwargs)
+
+    def auth_test(
+        self,
+        **kwargs,
+    ) -> SlackResponse:
+        """Checks authentication & identity.
+        https://docs.slack.dev/reference/methods/auth.test
+        """
+        return self.api_call("auth.test", params=kwargs)
+
+    def auth_teams_list(
+        self,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        include_icon: Optional[bool] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """List the workspaces a token can access.
+        https://docs.slack.dev/reference/methods/auth.teams.list
+        """
+        kwargs.update({"cursor": cursor, "limit": limit, "include_icon": include_icon})
+        return self.api_call("auth.teams.list", params=kwargs)
+
+    def bookmarks_add(
+        self,
+        *,
+        channel_id: str,
+        title: str,
+        type: str,
+        emoji: Optional[str] = None,
+        entity_id: Optional[str] = None,
+        link: Optional[str] = None,  # include when type is 'link'
+        parent_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Add bookmark to a channel.
+        https://docs.slack.dev/reference/methods/bookmarks.add
+        """
+        kwargs.update(
+            {
+                "channel_id": channel_id,
+                "title": title,
+                "type": type,
+                "emoji": emoji,
+                "entity_id": entity_id,
+                "link": link,
+                "parent_id": parent_id,
+            }
+        )
+        return self.api_call("bookmarks.add", http_verb="POST", params=kwargs)
+
+    def bookmarks_edit(
+        self,
+        *,
+        bookmark_id: str,
+        channel_id: str,
+        emoji: Optional[str] = None,
+        link: Optional[str] = None,
+        title: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Edit bookmark.
+        https://docs.slack.dev/reference/methods/bookmarks.edit
+        """
+        kwargs.update(
+            {
+                "bookmark_id": bookmark_id,
+                "channel_id": channel_id,
+                "emoji": emoji,
+                "link": link,
+                "title": title,
+            }
+        )
+        return self.api_call("bookmarks.edit", http_verb="POST", params=kwargs)
+
+    def bookmarks_list(
+        self,
+        *,
+        channel_id: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """List bookmark for the channel.
+        https://docs.slack.dev/reference/methods/bookmarks.list
+        """
+        kwargs.update({"channel_id": channel_id})
+        return self.api_call("bookmarks.list", http_verb="POST", params=kwargs)
+
+    def bookmarks_remove(
+        self,
+        *,
+        bookmark_id: str,
+        channel_id: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Remove bookmark from the channel.
+        https://docs.slack.dev/reference/methods/bookmarks.remove
+        """
+        kwargs.update({"bookmark_id": bookmark_id, "channel_id": channel_id})
+        return self.api_call("bookmarks.remove", http_verb="POST", params=kwargs)
+
+    def bots_info(
+        self,
+        *,
+        bot: Optional[str] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Gets information about a bot user.
+        https://docs.slack.dev/reference/methods/bots.info
+        """
+        kwargs.update({"bot": bot, "team_id": team_id})
+        return self.api_call("bots.info", http_verb="GET", params=kwargs)
+
+    def calls_add(
+        self,
+        *,
+        external_unique_id: str,
+        join_url: str,
+        created_by: Optional[str] = None,
+        date_start: Optional[int] = None,
+        desktop_app_join_url: Optional[str] = None,
+        external_display_id: Optional[str] = None,
+        title: Optional[str] = None,
+        users: Optional[Union[str, Sequence[Dict[str, str]]]] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Registers a new Call.
+        https://docs.slack.dev/reference/methods/calls.add
+        """
+        kwargs.update(
+            {
+                "external_unique_id": external_unique_id,
+                "join_url": join_url,
+                "created_by": created_by,
+                "date_start": date_start,
+                "desktop_app_join_url": desktop_app_join_url,
+                "external_display_id": external_display_id,
+                "title": title,
+            }
+        )
+        _update_call_participants(
+            kwargs,
+            users if users is not None else kwargs.get("users"),  # type: ignore[arg-type]
+        )
+        return self.api_call("calls.add", http_verb="POST", params=kwargs)
+
+    def calls_end(
+        self,
+        *,
+        id: str,
+        duration: Optional[int] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Ends a Call.
+        https://docs.slack.dev/reference/methods/calls.end
+        """
+        kwargs.update({"id": id, "duration": duration})
+        return self.api_call("calls.end", http_verb="POST", params=kwargs)
+
+    def calls_info(
+        self,
+        *,
+        id: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Returns information about a Call.
+        https://docs.slack.dev/reference/methods/calls.info
+        """
+        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, Sequence[Dict[str, str]]],
+        **kwargs,
+    ) -> SlackResponse:
+        """Registers new participants added to a Call.
+        https://docs.slack.dev/reference/methods/calls.participants.add
+        """
+        kwargs.update({"id": id})
+        _update_call_participants(kwargs, users)
+        return self.api_call("calls.participants.add", http_verb="POST", params=kwargs)
+
+    def calls_participants_remove(
+        self,
+        *,
+        id: str,
+        users: Union[str, Sequence[Dict[str, str]]],
+        **kwargs,
+    ) -> SlackResponse:
+        """Registers participants removed from a Call.
+        https://docs.slack.dev/reference/methods/calls.participants.remove
+        """
+        kwargs.update({"id": id})
+        _update_call_participants(kwargs, users)
+        return self.api_call("calls.participants.remove", http_verb="POST", params=kwargs)
+
+    def calls_update(
+        self,
+        *,
+        id: str,
+        desktop_app_join_url: Optional[str] = None,
+        join_url: Optional[str] = None,
+        title: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Updates information about a Call.
+        https://docs.slack.dev/reference/methods/calls.update
+        """
+        kwargs.update(
+            {
+                "id": id,
+                "desktop_app_join_url": desktop_app_join_url,
+                "join_url": join_url,
+                "title": title,
+            }
+        )
+        return self.api_call("calls.update", http_verb="POST", params=kwargs)
+
+    def canvases_create(
+        self,
+        *,
+        title: Optional[str] = None,
+        document_content: Dict[str, str],
+        **kwargs,
+    ) -> SlackResponse:
+        """Create Canvas for a user
+        https://docs.slack.dev/reference/methods/canvases.create
+        """
+        kwargs.update({"title": title, "document_content": document_content})
+        return self.api_call("canvases.create", json=kwargs)
+
+    def canvases_edit(
+        self,
+        *,
+        canvas_id: str,
+        changes: Sequence[Dict[str, Any]],
+        **kwargs,
+    ) -> SlackResponse:
+        """Update an existing canvas
+        https://docs.slack.dev/reference/methods/canvases.edit
+        """
+        kwargs.update({"canvas_id": canvas_id, "changes": changes})
+        return self.api_call("canvases.edit", json=kwargs)
+
+    def canvases_delete(
+        self,
+        *,
+        canvas_id: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Deletes a canvas
+        https://docs.slack.dev/reference/methods/canvases.delete
+        """
+        kwargs.update({"canvas_id": canvas_id})
+        return self.api_call("canvases.delete", params=kwargs)
+
+    def canvases_access_set(
+        self,
+        *,
+        canvas_id: str,
+        access_level: str,
+        channel_ids: Optional[Union[Sequence[str], str]] = None,
+        user_ids: Optional[Union[Sequence[str], str]] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Sets the access level to a canvas for specified entities
+        https://docs.slack.dev/reference/methods/canvases.access.set
+        """
+        kwargs.update({"canvas_id": canvas_id, "access_level": access_level})
+        if channel_ids is not None:
+            if isinstance(channel_ids, (list, tuple)):
+                kwargs.update({"channel_ids": ",".join(channel_ids)})
+            else:
+                kwargs.update({"channel_ids": channel_ids})
+        if user_ids is not None:
+            if isinstance(user_ids, (list, tuple)):
+                kwargs.update({"user_ids": ",".join(user_ids)})
+            else:
+                kwargs.update({"user_ids": user_ids})
+
+        return self.api_call("canvases.access.set", params=kwargs)
+
+    def canvases_access_delete(
+        self,
+        *,
+        canvas_id: str,
+        channel_ids: Optional[Union[Sequence[str], str]] = None,
+        user_ids: Optional[Union[Sequence[str], str]] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Create a Channel Canvas for a channel
+        https://docs.slack.dev/reference/methods/canvases.access.delete
+        """
+        kwargs.update({"canvas_id": canvas_id})
+        if channel_ids is not None:
+            if isinstance(channel_ids, (list, tuple)):
+                kwargs.update({"channel_ids": ",".join(channel_ids)})
+            else:
+                kwargs.update({"channel_ids": channel_ids})
+        if user_ids is not None:
+            if isinstance(user_ids, (list, tuple)):
+                kwargs.update({"user_ids": ",".join(user_ids)})
+            else:
+                kwargs.update({"user_ids": user_ids})
+        return self.api_call("canvases.access.delete", params=kwargs)
+
+    def canvases_sections_lookup(
+        self,
+        *,
+        canvas_id: str,
+        criteria: Dict[str, Any],
+        **kwargs,
+    ) -> SlackResponse:
+        """Find sections matching the provided criteria
+        https://docs.slack.dev/reference/methods/canvases.sections.lookup
+        """
+        kwargs.update({"canvas_id": canvas_id, "criteria": json.dumps(criteria)})
+        return self.api_call("canvases.sections.lookup", params=kwargs)
+
+    # --------------------------
+    # Deprecated: channels.*
+    # You can use conversations.* APIs instead.
+    # https://docs.slack.dev/changelog/2020-01-deprecating-antecedents-to-the-conversations-api/
+    # --------------------------
+
+    def channels_archive(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Archives a channel."""
+        kwargs.update({"channel": channel})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("channels.archive", json=kwargs)
+
+    def channels_create(
+        self,
+        *,
+        name: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Creates a channel."""
+        kwargs.update({"name": name})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("channels.create", json=kwargs)
+
+    def channels_history(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Fetches history of messages and events from a channel."""
+        kwargs.update({"channel": channel})
+        return self.api_call("channels.history", http_verb="GET", params=kwargs)
+
+    def channels_info(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Gets information about a channel."""
+        kwargs.update({"channel": channel})
+        return self.api_call("channels.info", http_verb="GET", params=kwargs)
+
+    def channels_invite(
+        self,
+        *,
+        channel: str,
+        user: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Invites a user to a channel."""
+        kwargs.update({"channel": channel, "user": user})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("channels.invite", json=kwargs)
+
+    def channels_join(
+        self,
+        *,
+        name: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Joins a channel, creating it if needed."""
+        kwargs.update({"name": name})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("channels.join", json=kwargs)
+
+    def channels_kick(
+        self,
+        *,
+        channel: str,
+        user: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Removes a user from a channel."""
+        kwargs.update({"channel": channel, "user": user})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("channels.kick", json=kwargs)
+
+    def channels_leave(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Leaves a channel."""
+        kwargs.update({"channel": channel})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("channels.leave", json=kwargs)
+
+    def channels_list(
+        self,
+        **kwargs,
+    ) -> SlackResponse:
+        """Lists all channels in a Slack team."""
+        return self.api_call("channels.list", http_verb="GET", params=kwargs)
+
+    def channels_mark(
+        self,
+        *,
+        channel: str,
+        ts: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Sets the read cursor in a channel."""
+        kwargs.update({"channel": channel, "ts": ts})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("channels.mark", json=kwargs)
+
+    def channels_rename(
+        self,
+        *,
+        channel: str,
+        name: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Renames a channel."""
+        kwargs.update({"channel": channel, "name": name})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("channels.rename", json=kwargs)
+
+    def channels_replies(
+        self,
+        *,
+        channel: str,
+        thread_ts: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Retrieve a thread of messages posted to a channel"""
+        kwargs.update({"channel": channel, "thread_ts": thread_ts})
+        return self.api_call("channels.replies", http_verb="GET", params=kwargs)
+
+    def channels_setPurpose(
+        self,
+        *,
+        channel: str,
+        purpose: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Sets the purpose for a channel."""
+        kwargs.update({"channel": channel, "purpose": purpose})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("channels.setPurpose", json=kwargs)
+
+    def channels_setTopic(
+        self,
+        *,
+        channel: str,
+        topic: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Sets the topic for a channel."""
+        kwargs.update({"channel": channel, "topic": topic})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("channels.setTopic", json=kwargs)
+
+    def channels_unarchive(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Unarchives a channel."""
+        kwargs.update({"channel": channel})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("channels.unarchive", json=kwargs)
+
+    # --------------------------
+
+    def chat_appendStream(
+        self,
+        *,
+        channel: str,
+        ts: str,
+        markdown_text: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Appends text to an existing streaming conversation.
+        https://docs.slack.dev/reference/methods/chat.appendStream
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "ts": ts,
+                "markdown_text": markdown_text,
+            }
+        )
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("chat.appendStream", json=kwargs)
+
+    def chat_delete(
+        self,
+        *,
+        channel: str,
+        ts: str,
+        as_user: Optional[bool] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Deletes a message.
+        https://docs.slack.dev/reference/methods/chat.delete
+        """
+        kwargs.update({"channel": channel, "ts": ts, "as_user": as_user})
+        return self.api_call("chat.delete", params=kwargs)
+
+    def chat_deleteScheduledMessage(
+        self,
+        *,
+        channel: str,
+        scheduled_message_id: str,
+        as_user: Optional[bool] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Deletes a scheduled message.
+        https://docs.slack.dev/reference/methods/chat.deleteScheduledMessage
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "scheduled_message_id": scheduled_message_id,
+                "as_user": as_user,
+            }
+        )
+        return self.api_call("chat.deleteScheduledMessage", params=kwargs)
+
+    def chat_getPermalink(
+        self,
+        *,
+        channel: str,
+        message_ts: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Retrieve a permalink URL for a specific extant message
+        https://docs.slack.dev/reference/methods/chat.getPermalink
+        """
+        kwargs.update({"channel": channel, "message_ts": message_ts})
+        return self.api_call("chat.getPermalink", http_verb="GET", params=kwargs)
+
+    def chat_meMessage(
+        self,
+        *,
+        channel: str,
+        text: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Share a me message into a channel.
+        https://docs.slack.dev/reference/methods/chat.meMessage
+        """
+        kwargs.update({"channel": channel, "text": text})
+        return self.api_call("chat.meMessage", params=kwargs)
+
+    def chat_postEphemeral(
+        self,
+        *,
+        channel: str,
+        user: str,
+        text: Optional[str] = None,
+        as_user: Optional[bool] = None,
+        attachments: Optional[Union[str, Sequence[Union[Dict, Attachment]]]] = None,
+        blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None,
+        thread_ts: Optional[str] = None,
+        icon_emoji: Optional[str] = None,
+        icon_url: Optional[str] = None,
+        link_names: Optional[bool] = None,
+        username: Optional[str] = None,
+        parse: Optional[str] = None,
+        markdown_text: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Sends an ephemeral message to a user in a channel.
+        https://docs.slack.dev/reference/methods/chat.postEphemeral
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "user": user,
+                "text": text,
+                "as_user": as_user,
+                "attachments": attachments,
+                "blocks": blocks,
+                "thread_ts": thread_ts,
+                "icon_emoji": icon_emoji,
+                "icon_url": icon_url,
+                "link_names": link_names,
+                "username": username,
+                "parse": parse,
+                "markdown_text": markdown_text,
+            }
+        )
+        _parse_web_class_objects(kwargs)
+        kwargs = _remove_none_values(kwargs)
+        _warn_if_message_text_content_is_missing("chat.postEphemeral", kwargs)
+        # NOTE: intentionally using json over params for the API methods using blocks/attachments
+        return self.api_call("chat.postEphemeral", json=kwargs)
+
+    def chat_postMessage(
+        self,
+        *,
+        channel: str,
+        text: Optional[str] = None,
+        as_user: Optional[bool] = None,
+        attachments: Optional[Union[str, Sequence[Union[Dict, Attachment]]]] = None,
+        blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None,
+        thread_ts: Optional[str] = None,
+        reply_broadcast: Optional[bool] = None,
+        unfurl_links: Optional[bool] = None,
+        unfurl_media: Optional[bool] = None,
+        container_id: Optional[str] = None,
+        icon_emoji: Optional[str] = None,
+        icon_url: Optional[str] = None,
+        mrkdwn: Optional[bool] = None,
+        link_names: Optional[bool] = None,
+        username: Optional[str] = None,
+        parse: Optional[str] = None,  # none, full
+        metadata: Optional[Union[Dict, Metadata, EventAndEntityMetadata]] = None,
+        markdown_text: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Sends a message to a channel.
+        https://docs.slack.dev/reference/methods/chat.postMessage
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "text": text,
+                "as_user": as_user,
+                "attachments": attachments,
+                "blocks": blocks,
+                "thread_ts": thread_ts,
+                "reply_broadcast": reply_broadcast,
+                "unfurl_links": unfurl_links,
+                "unfurl_media": unfurl_media,
+                "container_id": container_id,
+                "icon_emoji": icon_emoji,
+                "icon_url": icon_url,
+                "mrkdwn": mrkdwn,
+                "link_names": link_names,
+                "username": username,
+                "parse": parse,
+                "metadata": metadata,
+                "markdown_text": markdown_text,
+            }
+        )
+        _parse_web_class_objects(kwargs)
+        kwargs = _remove_none_values(kwargs)
+        _warn_if_message_text_content_is_missing("chat.postMessage", kwargs)
+        # NOTE: intentionally using json over params for the API methods using blocks/attachments
+        return self.api_call("chat.postMessage", json=kwargs)
+
+    def chat_scheduleMessage(
+        self,
+        *,
+        channel: str,
+        post_at: Union[str, int],
+        text: Optional[str] = None,
+        as_user: Optional[bool] = None,
+        attachments: Optional[Union[str, Sequence[Union[Dict, Attachment]]]] = None,
+        blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None,
+        thread_ts: Optional[str] = None,
+        parse: Optional[str] = None,
+        reply_broadcast: Optional[bool] = None,
+        unfurl_links: Optional[bool] = None,
+        unfurl_media: Optional[bool] = None,
+        link_names: Optional[bool] = None,
+        metadata: Optional[Union[Dict, Metadata]] = None,
+        markdown_text: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Schedules a message.
+        https://docs.slack.dev/reference/methods/chat.scheduleMessage
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "post_at": post_at,
+                "text": text,
+                "as_user": as_user,
+                "attachments": attachments,
+                "blocks": blocks,
+                "thread_ts": thread_ts,
+                "reply_broadcast": reply_broadcast,
+                "parse": parse,
+                "unfurl_links": unfurl_links,
+                "unfurl_media": unfurl_media,
+                "link_names": link_names,
+                "metadata": metadata,
+                "markdown_text": markdown_text,
+            }
+        )
+        _parse_web_class_objects(kwargs)
+        kwargs = _remove_none_values(kwargs)
+        _warn_if_message_text_content_is_missing("chat.scheduleMessage", kwargs)
+        # NOTE: intentionally using json over params for the API methods using blocks/attachments
+        return self.api_call("chat.scheduleMessage", json=kwargs)
+
+    def chat_scheduledMessages_list(
+        self,
+        *,
+        channel: Optional[str] = None,
+        cursor: Optional[str] = None,
+        latest: Optional[str] = None,
+        limit: Optional[int] = None,
+        oldest: Optional[str] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Lists all scheduled messages.
+        https://docs.slack.dev/reference/methods/chat.scheduledMessages.list
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "cursor": cursor,
+                "latest": latest,
+                "limit": limit,
+                "oldest": oldest,
+                "team_id": team_id,
+            }
+        )
+        return self.api_call("chat.scheduledMessages.list", params=kwargs)
+
+    def chat_startStream(
+        self,
+        *,
+        channel: str,
+        thread_ts: str,
+        markdown_text: Optional[str] = None,
+        recipient_team_id: Optional[str] = None,
+        recipient_user_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Starts a new streaming conversation.
+        https://docs.slack.dev/reference/methods/chat.startStream
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "thread_ts": thread_ts,
+                "markdown_text": markdown_text,
+                "recipient_team_id": recipient_team_id,
+                "recipient_user_id": recipient_user_id,
+            }
+        )
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("chat.startStream", json=kwargs)
+
+    def chat_stopStream(
+        self,
+        *,
+        channel: str,
+        ts: str,
+        markdown_text: Optional[str] = None,
+        blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None,
+        metadata: Optional[Union[Dict, Metadata]] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Stops a streaming conversation.
+        https://docs.slack.dev/reference/methods/chat.stopStream
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "ts": ts,
+                "markdown_text": markdown_text,
+                "blocks": blocks,
+                "metadata": metadata,
+            }
+        )
+        _parse_web_class_objects(kwargs)
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("chat.stopStream", json=kwargs)
+
+    def chat_stream(
+        self,
+        *,
+        buffer_size: int = 256,
+        channel: str,
+        thread_ts: str,
+        recipient_team_id: Optional[str] = None,
+        recipient_user_id: Optional[str] = None,
+        **kwargs,
+    ) -> ChatStream:
+        """Stream markdown text into a conversation.
+
+        This method starts a new chat stream in a conversation that can be appended to. After appending an entire message,
+        the stream can be stopped with concluding arguments such as "blocks" for gathering feedback.
+
+        The following methods are used:
+
+        - chat.startStream: Starts a new streaming conversation.
+          [Reference](https://docs.slack.dev/reference/methods/chat.startStream).
+        - chat.appendStream: Appends text to an existing streaming conversation.
+          [Reference](https://docs.slack.dev/reference/methods/chat.appendStream).
+        - chat.stopStream: Stops a streaming conversation.
+          [Reference](https://docs.slack.dev/reference/methods/chat.stopStream).
+
+        Args:
+            buffer_size: The length of markdown_text to buffer in-memory before calling a stream method. Increasing this
+              value decreases the number of method calls made for the same amount of text, which is useful to avoid rate
+              limits. Default: 256.
+            channel: An encoded ID that represents a channel, private group, or DM.
+            thread_ts: Provide another message's ts value to reply to. Streamed messages should always be replies to a user
+              request.
+            recipient_team_id: The encoded ID of the team the user receiving the streaming text belongs to. Required when
+              streaming to channels.
+            recipient_user_id: The encoded ID of the user to receive the streaming text. Required when streaming to channels.
+            **kwargs: Additional arguments passed to the underlying API calls.
+
+        Returns:
+            ChatStream instance for managing the stream
+
+        Example:
+            ```python
+            streamer = client.chat_stream(
+                channel="C0123456789",
+                thread_ts="1700000001.123456",
+                recipient_team_id="T0123456789",
+                recipient_user_id="U0123456789",
+            )
+            streamer.append(markdown_text="**hello wo")
+            streamer.append(markdown_text="rld!**")
+            streamer.stop()
+            ```
+        """
+        return ChatStream(
+            self,
+            logger=self._logger,
+            channel=channel,
+            thread_ts=thread_ts,
+            recipient_team_id=recipient_team_id,
+            recipient_user_id=recipient_user_id,
+            buffer_size=buffer_size,
+            **kwargs,
+        )
+
+    def chat_unfurl(
+        self,
+        *,
+        channel: Optional[str] = None,
+        ts: Optional[str] = None,
+        source: Optional[str] = None,
+        unfurl_id: Optional[str] = None,
+        unfurls: Optional[Dict[str, Dict]] = None,  # or user_auth_*
+        metadata: Optional[Union[Dict, EventAndEntityMetadata]] = None,
+        user_auth_blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None,
+        user_auth_message: Optional[str] = None,
+        user_auth_required: Optional[bool] = None,
+        user_auth_url: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Provide custom unfurl behavior for user-posted URLs.
+        https://docs.slack.dev/reference/methods/chat.unfurl
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "ts": ts,
+                "source": source,
+                "unfurl_id": unfurl_id,
+                "unfurls": unfurls,
+                "metadata": metadata,
+                "user_auth_blocks": user_auth_blocks,
+                "user_auth_message": user_auth_message,
+                "user_auth_required": user_auth_required,
+                "user_auth_url": user_auth_url,
+            }
+        )
+        _parse_web_class_objects(kwargs)  # for user_auth_blocks
+        kwargs = _remove_none_values(kwargs)
+        # NOTE: intentionally using json over params for API methods using blocks/attachments
+        return self.api_call("chat.unfurl", json=kwargs)
+
+    def chat_update(
+        self,
+        *,
+        channel: str,
+        ts: str,
+        text: Optional[str] = None,
+        attachments: Optional[Union[str, Sequence[Union[Dict, Attachment]]]] = None,
+        blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None,
+        as_user: Optional[bool] = None,
+        file_ids: Optional[Union[str, Sequence[str]]] = None,
+        link_names: Optional[bool] = None,
+        parse: Optional[str] = None,  # none, full
+        reply_broadcast: Optional[bool] = None,
+        metadata: Optional[Union[Dict, Metadata]] = None,
+        markdown_text: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Updates a message in a channel.
+        https://docs.slack.dev/reference/methods/chat.update
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "ts": ts,
+                "text": text,
+                "attachments": attachments,
+                "blocks": blocks,
+                "as_user": as_user,
+                "link_names": link_names,
+                "parse": parse,
+                "reply_broadcast": reply_broadcast,
+                "metadata": metadata,
+                "markdown_text": markdown_text,
+            }
+        )
+        if isinstance(file_ids, (list, tuple)):
+            kwargs.update({"file_ids": ",".join(file_ids)})
+        else:
+            kwargs.update({"file_ids": file_ids})
+        _parse_web_class_objects(kwargs)
+        kwargs = _remove_none_values(kwargs)
+        _warn_if_message_text_content_is_missing("chat.update", kwargs)
+        # NOTE: intentionally using json over params for API methods using blocks/attachments
+        return self.api_call("chat.update", json=kwargs)
+
+    def conversations_acceptSharedInvite(
+        self,
+        *,
+        channel_name: str,
+        channel_id: Optional[str] = None,
+        invite_id: Optional[str] = None,
+        free_trial_accepted: Optional[bool] = None,
+        is_private: Optional[bool] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Accepts an invitation to a Slack Connect channel.
+        https://docs.slack.dev/reference/methods/conversations.acceptSharedInvite
+        """
+        if channel_id is None and invite_id is None:
+            raise e.SlackRequestError("Either channel_id or invite_id must be provided.")
+        kwargs.update(
+            {
+                "channel_name": channel_name,
+                "channel_id": channel_id,
+                "invite_id": invite_id,
+                "free_trial_accepted": free_trial_accepted,
+                "is_private": is_private,
+                "team_id": team_id,
+            }
+        )
+        return self.api_call("conversations.acceptSharedInvite", http_verb="POST", params=kwargs)
+
+    def conversations_approveSharedInvite(
+        self,
+        *,
+        invite_id: str,
+        target_team: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Approves an invitation to a Slack Connect channel.
+        https://docs.slack.dev/reference/methods/conversations.approveSharedInvite
+        """
+        kwargs.update({"invite_id": invite_id, "target_team": target_team})
+        return self.api_call("conversations.approveSharedInvite", http_verb="POST", params=kwargs)
+
+    def conversations_archive(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Archives a conversation.
+        https://docs.slack.dev/reference/methods/conversations.archive
+        """
+        kwargs.update({"channel": channel})
+        return self.api_call("conversations.archive", params=kwargs)
+
+    def conversations_close(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Closes a direct message or multi-person direct message.
+        https://docs.slack.dev/reference/methods/conversations.close
+        """
+        kwargs.update({"channel": channel})
+        return self.api_call("conversations.close", params=kwargs)
+
+    def conversations_create(
+        self,
+        *,
+        name: str,
+        is_private: Optional[bool] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Initiates a public or private channel-based conversation
+        https://docs.slack.dev/reference/methods/conversations.create
+        """
+        kwargs.update({"name": name, "is_private": is_private, "team_id": team_id})
+        return self.api_call("conversations.create", params=kwargs)
+
+    def conversations_declineSharedInvite(
+        self,
+        *,
+        invite_id: str,
+        target_team: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Declines a Slack Connect channel invite.
+        https://docs.slack.dev/reference/methods/conversations.declineSharedInvite
+        """
+        kwargs.update({"invite_id": invite_id, "target_team": target_team})
+        return self.api_call("conversations.declineSharedInvite", http_verb="GET", params=kwargs)
+
+    def conversations_externalInvitePermissions_set(
+        self, *, action: str, channel: str, target_team: str, **kwargs
+    ) -> SlackResponse:
+        """Sets a team in a shared External Limited channel to a shared Slack Connect channel or vice versa.
+        https://docs.slack.dev/reference/methods/conversations.externalInvitePermissions.set
+        """
+        kwargs.update(
+            {
+                "action": action,
+                "channel": channel,
+                "target_team": target_team,
+            }
+        )
+        return self.api_call("conversations.externalInvitePermissions.set", params=kwargs)
+
+    def conversations_history(
+        self,
+        *,
+        channel: str,
+        cursor: Optional[str] = None,
+        inclusive: Optional[bool] = None,
+        include_all_metadata: Optional[bool] = None,
+        latest: Optional[str] = None,
+        limit: Optional[int] = None,
+        oldest: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Fetches a conversation's history of messages and events.
+        https://docs.slack.dev/reference/methods/conversations.history
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "cursor": cursor,
+                "inclusive": inclusive,
+                "include_all_metadata": include_all_metadata,
+                "limit": limit,
+                "latest": latest,
+                "oldest": oldest,
+            }
+        )
+        return self.api_call("conversations.history", http_verb="GET", params=kwargs)
+
+    def conversations_info(
+        self,
+        *,
+        channel: str,
+        include_locale: Optional[bool] = None,
+        include_num_members: Optional[bool] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Retrieve information about a conversation.
+        https://docs.slack.dev/reference/methods/conversations.info
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "include_locale": include_locale,
+                "include_num_members": include_num_members,
+            }
+        )
+        return self.api_call("conversations.info", http_verb="GET", params=kwargs)
+
+    def conversations_invite(
+        self,
+        *,
+        channel: str,
+        users: Union[str, Sequence[str]],
+        force: Optional[bool] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Invites users to a channel.
+        https://docs.slack.dev/reference/methods/conversations.invite
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "force": force,
+            }
+        )
+        if isinstance(users, (list, tuple)):
+            kwargs.update({"users": ",".join(users)})
+        else:
+            kwargs.update({"users": users})
+        return self.api_call("conversations.invite", params=kwargs)
+
+    def conversations_inviteShared(
+        self,
+        *,
+        channel: str,
+        emails: Optional[Union[str, Sequence[str]]] = None,
+        user_ids: Optional[Union[str, Sequence[str]]] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Sends an invitation to a Slack Connect channel.
+        https://docs.slack.dev/reference/methods/conversations.inviteShared
+        """
+        if emails is None and user_ids is None:
+            raise e.SlackRequestError("Either emails or user ids must be provided.")
+        kwargs.update({"channel": channel})
+        if isinstance(emails, (list, tuple)):
+            kwargs.update({"emails": ",".join(emails)})
+        else:
+            kwargs.update({"emails": emails})
+        if isinstance(user_ids, (list, tuple)):
+            kwargs.update({"user_ids": ",".join(user_ids)})
+        else:
+            kwargs.update({"user_ids": user_ids})
+        return self.api_call("conversations.inviteShared", http_verb="GET", params=kwargs)
+
+    def conversations_join(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Joins an existing conversation.
+        https://docs.slack.dev/reference/methods/conversations.join
+        """
+        kwargs.update({"channel": channel})
+        return self.api_call("conversations.join", params=kwargs)
+
+    def conversations_kick(
+        self,
+        *,
+        channel: str,
+        user: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Removes a user from a conversation.
+        https://docs.slack.dev/reference/methods/conversations.kick
+        """
+        kwargs.update({"channel": channel, "user": user})
+        return self.api_call("conversations.kick", params=kwargs)
+
+    def conversations_leave(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Leaves a conversation.
+        https://docs.slack.dev/reference/methods/conversations.leave
+        """
+        kwargs.update({"channel": channel})
+        return self.api_call("conversations.leave", params=kwargs)
+
+    def conversations_list(
+        self,
+        *,
+        cursor: Optional[str] = None,
+        exclude_archived: Optional[bool] = None,
+        limit: Optional[int] = None,
+        team_id: Optional[str] = None,
+        types: Optional[Union[str, Sequence[str]]] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Lists all channels in a Slack team.
+        https://docs.slack.dev/reference/methods/conversations.list
+        """
+        kwargs.update(
+            {
+                "cursor": cursor,
+                "exclude_archived": exclude_archived,
+                "limit": limit,
+                "team_id": team_id,
+            }
+        )
+        if isinstance(types, (list, tuple)):
+            kwargs.update({"types": ",".join(types)})
+        else:
+            kwargs.update({"types": types})
+        return self.api_call("conversations.list", http_verb="GET", params=kwargs)
+
+    def conversations_listConnectInvites(
+        self,
+        *,
+        count: Optional[int] = None,
+        cursor: Optional[str] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """List shared channel invites that have been generated
+        or received but have not yet been approved by all parties.
+        https://docs.slack.dev/reference/methods/conversations.listConnectInvites
+        """
+        kwargs.update({"count": count, "cursor": cursor, "team_id": team_id})
+        return self.api_call("conversations.listConnectInvites", params=kwargs)
+
+    def conversations_mark(
+        self,
+        *,
+        channel: str,
+        ts: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Sets the read cursor in a channel.
+        https://docs.slack.dev/reference/methods/conversations.mark
+        """
+        kwargs.update({"channel": channel, "ts": ts})
+        return self.api_call("conversations.mark", params=kwargs)
+
+    def conversations_members(
+        self,
+        *,
+        channel: str,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Retrieve members of a conversation.
+        https://docs.slack.dev/reference/methods/conversations.members
+        """
+        kwargs.update({"channel": channel, "cursor": cursor, "limit": limit})
+        return self.api_call("conversations.members", http_verb="GET", params=kwargs)
+
+    def conversations_open(
+        self,
+        *,
+        channel: Optional[str] = None,
+        return_im: Optional[bool] = None,
+        users: Optional[Union[str, Sequence[str]]] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Opens or resumes a direct message or multi-person direct message.
+        https://docs.slack.dev/reference/methods/conversations.open
+        """
+        if channel is None and users is None:
+            raise e.SlackRequestError("Either channel or users must be provided.")
+        kwargs.update({"channel": channel, "return_im": return_im})
+        if isinstance(users, (list, tuple)):
+            kwargs.update({"users": ",".join(users)})
+        else:
+            kwargs.update({"users": users})
+        return self.api_call("conversations.open", params=kwargs)
+
+    def conversations_rename(
+        self,
+        *,
+        channel: str,
+        name: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Renames a conversation.
+        https://docs.slack.dev/reference/methods/conversations.rename
+        """
+        kwargs.update({"channel": channel, "name": name})
+        return self.api_call("conversations.rename", params=kwargs)
+
+    def conversations_replies(
+        self,
+        *,
+        channel: str,
+        ts: str,
+        cursor: Optional[str] = None,
+        inclusive: Optional[bool] = None,
+        include_all_metadata: Optional[bool] = None,
+        latest: Optional[str] = None,
+        limit: Optional[int] = None,
+        oldest: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Retrieve a thread of messages posted to a conversation
+        https://docs.slack.dev/reference/methods/conversations.replies
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "ts": ts,
+                "cursor": cursor,
+                "inclusive": inclusive,
+                "include_all_metadata": include_all_metadata,
+                "limit": limit,
+                "latest": latest,
+                "oldest": oldest,
+            }
+        )
+        return self.api_call("conversations.replies", http_verb="GET", params=kwargs)
+
+    def conversations_requestSharedInvite_approve(
+        self,
+        *,
+        invite_id: str,
+        channel_id: Optional[str] = None,
+        is_external_limited: Optional[str] = None,
+        message: Optional[Dict[str, Any]] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Approve a request to add an external user to a channel. This also sends them a Slack Connect invite.
+        https://docs.slack.dev/reference/methods/conversations.requestSharedInvite.approve
+        """
+        kwargs.update(
+            {
+                "invite_id": invite_id,
+                "channel_id": channel_id,
+                "is_external_limited": is_external_limited,
+            }
+        )
+        if message is not None:
+            kwargs.update({"message": json.dumps(message)})
+        return self.api_call("conversations.requestSharedInvite.approve", params=kwargs)
+
+    def conversations_requestSharedInvite_deny(
+        self,
+        *,
+        invite_id: str,
+        message: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Deny a request to invite an external user to a channel.
+        https://docs.slack.dev/reference/methods/conversations.requestSharedInvite.deny
+        """
+        kwargs.update({"invite_id": invite_id, "message": message})
+        return self.api_call("conversations.requestSharedInvite.deny", params=kwargs)
+
+    def conversations_requestSharedInvite_list(
+        self,
+        *,
+        cursor: Optional[str] = None,
+        include_approved: Optional[bool] = None,
+        include_denied: Optional[bool] = None,
+        include_expired: Optional[bool] = None,
+        invite_ids: Optional[Union[str, Sequence[str]]] = None,
+        limit: Optional[int] = None,
+        user_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Lists requests to add external users to channels with ability to filter.
+        https://docs.slack.dev/reference/methods/conversations.requestSharedInvite.list
+        """
+        kwargs.update(
+            {
+                "cursor": cursor,
+                "include_approved": include_approved,
+                "include_denied": include_denied,
+                "include_expired": include_expired,
+                "limit": limit,
+                "user_id": user_id,
+            }
+        )
+        if invite_ids is not None:
+            if isinstance(invite_ids, (list, tuple)):
+                kwargs.update({"invite_ids": ",".join(invite_ids)})
+            else:
+                kwargs.update({"invite_ids": invite_ids})
+        return self.api_call("conversations.requestSharedInvite.list", params=kwargs)
+
+    def conversations_setPurpose(
+        self,
+        *,
+        channel: str,
+        purpose: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Sets the purpose for a conversation.
+        https://docs.slack.dev/reference/methods/conversations.setPurpose
+        """
+        kwargs.update({"channel": channel, "purpose": purpose})
+        return self.api_call("conversations.setPurpose", params=kwargs)
+
+    def conversations_setTopic(
+        self,
+        *,
+        channel: str,
+        topic: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Sets the topic for a conversation.
+        https://docs.slack.dev/reference/methods/conversations.setTopic
+        """
+        kwargs.update({"channel": channel, "topic": topic})
+        return self.api_call("conversations.setTopic", params=kwargs)
+
+    def conversations_unarchive(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Reverses conversation archival.
+        https://docs.slack.dev/reference/methods/conversations.unarchive
+        """
+        kwargs.update({"channel": channel})
+        return self.api_call("conversations.unarchive", params=kwargs)
+
+    def conversations_canvases_create(
+        self,
+        *,
+        channel_id: str,
+        document_content: Dict[str, str],
+        **kwargs,
+    ) -> SlackResponse:
+        """Create a Channel Canvas for a channel
+        https://docs.slack.dev/reference/methods/conversations.canvases.create
+        """
+        kwargs.update({"channel_id": channel_id, "document_content": document_content})
+        return self.api_call("conversations.canvases.create", json=kwargs)
+
+    def dialog_open(
+        self,
+        *,
+        dialog: Dict[str, Any],
+        trigger_id: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Open a dialog with a user.
+        https://docs.slack.dev/reference/methods/dialog.open
+        """
+        kwargs.update({"dialog": dialog, "trigger_id": trigger_id})
+        kwargs = _remove_none_values(kwargs)
+        # NOTE: As the dialog can be a dict, this API call works only with json format.
+        return self.api_call("dialog.open", json=kwargs)
+
+    def dnd_endDnd(
+        self,
+        **kwargs,
+    ) -> SlackResponse:
+        """Ends the current user's Do Not Disturb session immediately.
+        https://docs.slack.dev/reference/methods/dnd.endDnd
+        """
+        return self.api_call("dnd.endDnd", params=kwargs)
+
+    def dnd_endSnooze(
+        self,
+        **kwargs,
+    ) -> SlackResponse:
+        """Ends the current user's snooze mode immediately.
+        https://docs.slack.dev/reference/methods/dnd.endSnooze
+        """
+        return self.api_call("dnd.endSnooze", params=kwargs)
+
+    def dnd_info(
+        self,
+        *,
+        team_id: Optional[str] = None,
+        user: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Retrieves a user's current Do Not Disturb status.
+        https://docs.slack.dev/reference/methods/dnd.info
+        """
+        kwargs.update({"team_id": team_id, "user": user})
+        return self.api_call("dnd.info", http_verb="GET", params=kwargs)
+
+    def dnd_setSnooze(
+        self,
+        *,
+        num_minutes: Union[int, str],
+        **kwargs,
+    ) -> SlackResponse:
+        """Turns on Do Not Disturb mode for the current user, or changes its duration.
+        https://docs.slack.dev/reference/methods/dnd.setSnooze
+        """
+        kwargs.update({"num_minutes": num_minutes})
+        return self.api_call("dnd.setSnooze", http_verb="GET", params=kwargs)
+
+    def dnd_teamInfo(
+        self,
+        users: Union[str, Sequence[str]],
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Retrieves the Do Not Disturb status for users on a team.
+        https://docs.slack.dev/reference/methods/dnd.teamInfo
+        """
+        if isinstance(users, (list, tuple)):
+            kwargs.update({"users": ",".join(users)})
+        else:
+            kwargs.update({"users": users})
+        kwargs.update({"team_id": team_id})
+        return self.api_call("dnd.teamInfo", http_verb="GET", params=kwargs)
+
+    def emoji_list(
+        self,
+        include_categories: Optional[bool] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Lists custom emoji for a team.
+        https://docs.slack.dev/reference/methods/emoji.list
+        """
+        kwargs.update({"include_categories": include_categories})
+        return self.api_call("emoji.list", http_verb="GET", params=kwargs)
+
+    def entity_presentDetails(
+        self,
+        trigger_id: str,
+        metadata: Optional[Union[Dict, EntityMetadata]] = None,
+        user_auth_required: Optional[bool] = None,
+        user_auth_url: Optional[str] = None,
+        error: Optional[Dict[str, Any]] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Provides entity details for the flexpane.
+        https://docs.slack.dev/reference/methods/entity.presentDetails/
+        """
+        kwargs.update({"trigger_id": trigger_id})
+        if metadata is not None:
+            kwargs.update({"metadata": metadata})
+        if user_auth_required is not None:
+            kwargs.update({"user_auth_required": user_auth_required})
+        if user_auth_url is not None:
+            kwargs.update({"user_auth_url": user_auth_url})
+        if error is not None:
+            kwargs.update({"error": error})
+        _parse_web_class_objects(kwargs)
+        return self.api_call("entity.presentDetails", json=kwargs)
+
+    def files_comments_delete(
+        self,
+        *,
+        file: str,
+        id: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Deletes an existing comment on a file.
+        https://docs.slack.dev/reference/methods/files.comments.delete
+        """
+        kwargs.update({"file": file, "id": id})
+        return self.api_call("files.comments.delete", params=kwargs)
+
+    def files_delete(
+        self,
+        *,
+        file: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Deletes a file.
+        https://docs.slack.dev/reference/methods/files.delete
+        """
+        kwargs.update({"file": file})
+        return self.api_call("files.delete", params=kwargs)
+
+    def files_info(
+        self,
+        *,
+        file: str,
+        count: Optional[int] = None,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        page: Optional[int] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Gets information about a team file.
+        https://docs.slack.dev/reference/methods/files.info
+        """
+        kwargs.update(
+            {
+                "file": file,
+                "count": count,
+                "cursor": cursor,
+                "limit": limit,
+                "page": page,
+            }
+        )
+        return self.api_call("files.info", http_verb="GET", params=kwargs)
+
+    def files_list(
+        self,
+        *,
+        channel: Optional[str] = None,
+        count: Optional[int] = None,
+        page: Optional[int] = None,
+        show_files_hidden_by_limit: Optional[bool] = None,
+        team_id: Optional[str] = None,
+        ts_from: Optional[str] = None,
+        ts_to: Optional[str] = None,
+        types: Optional[Union[str, Sequence[str]]] = None,
+        user: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Lists & filters team files.
+        https://docs.slack.dev/reference/methods/files.list
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "count": count,
+                "page": page,
+                "show_files_hidden_by_limit": show_files_hidden_by_limit,
+                "team_id": team_id,
+                "ts_from": ts_from,
+                "ts_to": ts_to,
+                "user": user,
+            }
+        )
+        if isinstance(types, (list, tuple)):
+            kwargs.update({"types": ",".join(types)})
+        else:
+            kwargs.update({"types": types})
+        return self.api_call("files.list", http_verb="GET", params=kwargs)
+
+    def files_remote_info(
+        self,
+        *,
+        external_id: Optional[str] = None,
+        file: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Retrieve information about a remote file added to Slack.
+        https://docs.slack.dev/reference/methods/files.remote.info
+        """
+        kwargs.update({"external_id": external_id, "file": file})
+        return self.api_call("files.remote.info", http_verb="GET", params=kwargs)
+
+    def files_remote_list(
+        self,
+        *,
+        channel: Optional[str] = None,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        ts_from: Optional[str] = None,
+        ts_to: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Retrieve information about a remote file added to Slack.
+        https://docs.slack.dev/reference/methods/files.remote.list
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "cursor": cursor,
+                "limit": limit,
+                "ts_from": ts_from,
+                "ts_to": ts_to,
+            }
+        )
+        return self.api_call("files.remote.list", http_verb="GET", params=kwargs)
+
+    def files_remote_add(
+        self,
+        *,
+        external_id: str,
+        external_url: str,
+        title: str,
+        filetype: Optional[str] = None,
+        indexable_file_contents: Optional[Union[str, bytes, IOBase]] = None,
+        preview_image: Optional[Union[str, bytes, IOBase]] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Adds a file from a remote service.
+        https://docs.slack.dev/reference/methods/files.remote.add
+        """
+        kwargs.update(
+            {
+                "external_id": external_id,
+                "external_url": external_url,
+                "title": title,
+                "filetype": filetype,
+            }
+        )
+        files = None
+        # preview_image (file): Preview of the document via multipart/form-data.
+        if preview_image is not None or indexable_file_contents is not None:
+            files = {
+                "preview_image": preview_image,
+                "indexable_file_contents": indexable_file_contents,
+            }
+
+        return self.api_call(
+            # Intentionally using "POST" method over "GET" here
+            "files.remote.add",
+            http_verb="POST",
+            data=kwargs,
+            files=files,
+        )
+
+    def files_remote_update(
+        self,
+        *,
+        external_id: Optional[str] = None,
+        external_url: Optional[str] = None,
+        file: Optional[str] = None,
+        title: Optional[str] = None,
+        filetype: Optional[str] = None,
+        indexable_file_contents: Optional[str] = None,
+        preview_image: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Updates an existing remote file.
+        https://docs.slack.dev/reference/methods/files.remote.update
+        """
+        kwargs.update(
+            {
+                "external_id": external_id,
+                "external_url": external_url,
+                "file": file,
+                "title": title,
+                "filetype": filetype,
+            }
+        )
+        files = None
+        # preview_image (file): Preview of the document via multipart/form-data.
+        if preview_image is not None or indexable_file_contents is not None:
+            files = {
+                "preview_image": preview_image,
+                "indexable_file_contents": indexable_file_contents,
+            }
+
+        return self.api_call(
+            # Intentionally using "POST" method over "GET" here
+            "files.remote.update",
+            http_verb="POST",
+            data=kwargs,
+            files=files,
+        )
+
+    def files_remote_remove(
+        self,
+        *,
+        external_id: Optional[str] = None,
+        file: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Remove a remote file.
+        https://docs.slack.dev/reference/methods/files.remote.remove
+        """
+        kwargs.update({"external_id": external_id, "file": file})
+        return self.api_call("files.remote.remove", http_verb="POST", params=kwargs)
+
+    def files_remote_share(
+        self,
+        *,
+        channels: Union[str, Sequence[str]],
+        external_id: Optional[str] = None,
+        file: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Share a remote file into a channel.
+        https://docs.slack.dev/reference/methods/files.remote.share
+        """
+        if external_id is None and file is None:
+            raise e.SlackRequestError("Either external_id or file must be provided.")
+        if isinstance(channels, (list, tuple)):
+            kwargs.update({"channels": ",".join(channels)})
+        else:
+            kwargs.update({"channels": channels})
+        kwargs.update({"external_id": external_id, "file": file})
+        return self.api_call("files.remote.share", http_verb="GET", params=kwargs)
+
+    def files_revokePublicURL(
+        self,
+        *,
+        file: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Revokes public/external sharing access for a file
+        https://docs.slack.dev/reference/methods/files.revokePublicURL
+        """
+        kwargs.update({"file": file})
+        return self.api_call("files.revokePublicURL", params=kwargs)
+
+    def files_sharedPublicURL(
+        self,
+        *,
+        file: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Enables a file for public/external sharing.
+        https://docs.slack.dev/reference/methods/files.sharedPublicURL
+        """
+        kwargs.update({"file": file})
+        return self.api_call("files.sharedPublicURL", params=kwargs)
+
+    def files_upload(
+        self,
+        *,
+        file: Optional[Union[str, bytes, IOBase]] = None,
+        content: Optional[Union[str, bytes]] = None,
+        filename: Optional[str] = None,
+        filetype: Optional[str] = None,
+        initial_comment: Optional[str] = None,
+        thread_ts: Optional[str] = None,
+        title: Optional[str] = None,
+        channels: Optional[Union[str, Sequence[str]]] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Uploads or creates a file.
+        https://docs.slack.dev/reference/methods/files.upload
+        """
+        _print_files_upload_v2_suggestion()
+
+        if file is None and content is None:
+            raise e.SlackRequestError("The file or content argument must be specified.")
+        if file is not None and content is not None:
+            raise e.SlackRequestError("You cannot specify both the file and the content argument.")
+
+        if isinstance(channels, (list, tuple)):
+            kwargs.update({"channels": ",".join(channels)})
+        else:
+            kwargs.update({"channels": channels})
+        kwargs.update(
+            {
+                "filename": filename,
+                "filetype": filetype,
+                "initial_comment": initial_comment,
+                "thread_ts": thread_ts,
+                "title": title,
+            }
+        )
+        if file:
+            if kwargs.get("filename") is None and isinstance(file, str):
+                # use the local filename if filename is missing
+                if kwargs.get("filename") is None:
+                    kwargs["filename"] = file.split(os.path.sep)[-1]
+            return self.api_call("files.upload", files={"file": file}, data=kwargs)
+        else:
+            kwargs["content"] = content
+            return self.api_call("files.upload", data=kwargs)
+
+    def files_upload_v2(
+        self,
+        *,
+        # for sending a single file
+        filename: Optional[str] = None,  # you can skip this only when sending along with content parameter
+        file: Optional[Union[str, bytes, IOBase, os.PathLike]] = None,
+        content: Optional[Union[str, bytes]] = None,
+        title: Optional[str] = None,
+        alt_txt: Optional[str] = None,
+        snippet_type: Optional[str] = None,
+        # To upload multiple files at a time
+        file_uploads: Optional[List[Dict[str, Any]]] = None,
+        channel: Optional[str] = None,
+        channels: Optional[List[str]] = None,
+        initial_comment: Optional[str] = None,
+        thread_ts: Optional[str] = None,
+        request_file_info: bool = True,  # since v3.23, this flag is no longer necessary
+        **kwargs,
+    ) -> SlackResponse:
+        """This wrapper method provides an easy way to upload files using the following endpoints:
+
+        - step1: https://docs.slack.dev/reference/methods/files.getUploadURLExternal
+
+        - step2: "https://files.slack.com/upload/v1/..." URLs returned from files.getUploadURLExternal API
+
+        - step3: https://docs.slack.dev/reference/methods/files.completeUploadExternal
+            and https://docs.slack.dev/reference/methods/files.info
+
+        """
+        if file is None and content is None and file_uploads is None:
+            raise e.SlackRequestError("Any of file, content, and file_uploads must be specified.")
+        if file is not None and content is not None:
+            raise e.SlackRequestError("You cannot specify both the file and the content argument.")
+
+        # deprecated arguments:
+        filetype = kwargs.get("filetype")
+
+        if filetype is not None:
+            warnings.warn("The filetype parameter is no longer supported. Please remove it from the arguments.")
+
+        # step1: files.getUploadURLExternal per file
+        files: List[Dict[str, Any]] = []
+        if file_uploads is not None:
+            for f in file_uploads:
+                files.append(_to_v2_file_upload_item(f))
+        else:
+            f = _to_v2_file_upload_item(
+                {
+                    "filename": filename,
+                    "file": file,
+                    "content": content,
+                    "title": title,
+                    "alt_txt": alt_txt,
+                    "snippet_type": snippet_type,
+                }
+            )
+            files.append(f)
+
+        for f in files:
+            url_response = self.files_getUploadURLExternal(
+                filename=f.get("filename"),  # type: ignore[arg-type]
+                length=f.get("length"),  # type: ignore[arg-type]
+                alt_txt=f.get("alt_txt"),
+                snippet_type=f.get("snippet_type"),
+                token=kwargs.get("token"),
+            )
+            _validate_for_legacy_client(url_response)
+            f["file_id"] = url_response.get("file_id")  # type: ignore[union-attr, unused-ignore]
+            f["upload_url"] = url_response.get("upload_url")  # type: ignore[union-attr, unused-ignore]
+
+        # step2: "https://files.slack.com/upload/v1/..." per file
+        for f in files:
+            upload_result = self._upload_file(
+                url=f["upload_url"],
+                data=f["data"],
+                logger=self._logger,
+                timeout=self.timeout,
+                proxy=self.proxy,
+                ssl=self.ssl,
+            )
+            if upload_result.status != 200:
+                status = upload_result.status
+                body = upload_result.body
+                message = (
+                    "Failed to upload a file "
+                    f"(status: {status}, body: {body}, filename: {f.get('filename')}, title: {f.get('title')})"
+                )
+                raise e.SlackRequestError(message)
+
+        # step3: files.completeUploadExternal with all the sets of (file_id + title)
+        completion = self.files_completeUploadExternal(
+            files=[{"id": f["file_id"], "title": f["title"]} for f in files],
+            channel_id=channel,
+            channels=channels,
+            initial_comment=initial_comment,
+            thread_ts=thread_ts,
+            **kwargs,
+        )
+        if len(completion.get("files")) == 1:  # type: ignore[arg-type, union-attr, unused-ignore]
+            completion.data["file"] = completion.get("files")[0]  # type: ignore[index, union-attr, unused-ignore]
+        return completion
+
+    def files_getUploadURLExternal(
+        self,
+        *,
+        filename: str,
+        length: int,
+        alt_txt: Optional[str] = None,
+        snippet_type: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Gets a URL for an edge external upload.
+        https://docs.slack.dev/reference/methods/files.getUploadURLExternal
+        """
+        kwargs.update(
+            {
+                "filename": filename,
+                "length": length,
+                "alt_txt": alt_txt,
+                "snippet_type": snippet_type,
+            }
+        )
+        return self.api_call("files.getUploadURLExternal", params=kwargs)
+
+    def files_completeUploadExternal(
+        self,
+        *,
+        files: List[Dict[str, str]],
+        channel_id: Optional[str] = None,
+        channels: Optional[List[str]] = None,
+        initial_comment: Optional[str] = None,
+        thread_ts: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Finishes an upload started with files.getUploadURLExternal.
+        https://docs.slack.dev/reference/methods/files.completeUploadExternal
+        """
+        _files = [{k: v for k, v in f.items() if v is not None} for f in files]
+        kwargs.update(
+            {
+                "files": json.dumps(_files),
+                "channel_id": channel_id,
+                "initial_comment": initial_comment,
+                "thread_ts": thread_ts,
+            }
+        )
+        if channels:
+            kwargs["channels"] = ",".join(channels)
+        return self.api_call("files.completeUploadExternal", params=kwargs)
+
+    def functions_completeSuccess(
+        self,
+        *,
+        function_execution_id: str,
+        outputs: Dict[str, Any],
+        **kwargs,
+    ) -> SlackResponse:
+        """Signal the successful completion of a function
+        https://docs.slack.dev/reference/methods/functions.completeSuccess
+        """
+        kwargs.update({"function_execution_id": function_execution_id, "outputs": json.dumps(outputs)})
+        return self.api_call("functions.completeSuccess", params=kwargs)
+
+    def functions_completeError(
+        self,
+        *,
+        function_execution_id: str,
+        error: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Signal the failure to execute a function
+        https://docs.slack.dev/reference/methods/functions.completeError
+        """
+        kwargs.update({"function_execution_id": function_execution_id, "error": error})
+        return self.api_call("functions.completeError", params=kwargs)
+
+    # --------------------------
+    # Deprecated: groups.*
+    # You can use conversations.* APIs instead.
+    # https://docs.slack.dev/changelog/2020-01-deprecating-antecedents-to-the-conversations-api/
+    # --------------------------
+
+    def groups_archive(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Archives a private channel."""
+        kwargs.update({"channel": channel})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("groups.archive", json=kwargs)
+
+    def groups_create(
+        self,
+        *,
+        name: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Creates a private channel."""
+        kwargs.update({"name": name})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("groups.create", json=kwargs)
+
+    def groups_createChild(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Clones and archives a private channel."""
+        kwargs.update({"channel": channel})
+        return self.api_call("groups.createChild", http_verb="GET", params=kwargs)
+
+    def groups_history(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Fetches history of messages and events from a private channel."""
+        kwargs.update({"channel": channel})
+        return self.api_call("groups.history", http_verb="GET", params=kwargs)
+
+    def groups_info(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Gets information about a private channel."""
+        kwargs.update({"channel": channel})
+        return self.api_call("groups.info", http_verb="GET", params=kwargs)
+
+    def groups_invite(
+        self,
+        *,
+        channel: str,
+        user: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Invites a user to a private channel."""
+        kwargs.update({"channel": channel, "user": user})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("groups.invite", json=kwargs)
+
+    def groups_kick(
+        self,
+        *,
+        channel: str,
+        user: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Removes a user from a private channel."""
+        kwargs.update({"channel": channel, "user": user})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("groups.kick", json=kwargs)
+
+    def groups_leave(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Leaves a private channel."""
+        kwargs.update({"channel": channel})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("groups.leave", json=kwargs)
+
+    def groups_list(
+        self,
+        **kwargs,
+    ) -> SlackResponse:
+        """Lists private channels that the calling user has access to."""
+        return self.api_call("groups.list", http_verb="GET", params=kwargs)
+
+    def groups_mark(
+        self,
+        *,
+        channel: str,
+        ts: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Sets the read cursor in a private channel."""
+        kwargs.update({"channel": channel, "ts": ts})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("groups.mark", json=kwargs)
+
+    def groups_open(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Opens a private channel."""
+        kwargs.update({"channel": channel})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("groups.open", json=kwargs)
+
+    def groups_rename(
+        self,
+        *,
+        channel: str,
+        name: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Renames a private channel."""
+        kwargs.update({"channel": channel, "name": name})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("groups.rename", json=kwargs)
+
+    def groups_replies(
+        self,
+        *,
+        channel: str,
+        thread_ts: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Retrieve a thread of messages posted to a private channel"""
+        kwargs.update({"channel": channel, "thread_ts": thread_ts})
+        return self.api_call("groups.replies", http_verb="GET", params=kwargs)
+
+    def groups_setPurpose(
+        self,
+        *,
+        channel: str,
+        purpose: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Sets the purpose for a private channel."""
+        kwargs.update({"channel": channel, "purpose": purpose})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("groups.setPurpose", json=kwargs)
+
+    def groups_setTopic(
+        self,
+        *,
+        channel: str,
+        topic: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Sets the topic for a private channel."""
+        kwargs.update({"channel": channel, "topic": topic})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("groups.setTopic", json=kwargs)
+
+    def groups_unarchive(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Unarchives a private channel."""
+        kwargs.update({"channel": channel})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("groups.unarchive", json=kwargs)
+
+    # --------------------------
+    # Deprecated: im.*
+    # You can use conversations.* APIs instead.
+    # https://docs.slack.dev/changelog/2020-01-deprecating-antecedents-to-the-conversations-api/
+    # --------------------------
+
+    def im_close(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Close a direct message channel."""
+        kwargs.update({"channel": channel})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("im.close", json=kwargs)
+
+    def im_history(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Fetches history of messages and events from direct message channel."""
+        kwargs.update({"channel": channel})
+        return self.api_call("im.history", http_verb="GET", params=kwargs)
+
+    def im_list(
+        self,
+        **kwargs,
+    ) -> SlackResponse:
+        """Lists direct message channels for the calling user."""
+        return self.api_call("im.list", http_verb="GET", params=kwargs)
+
+    def im_mark(
+        self,
+        *,
+        channel: str,
+        ts: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Sets the read cursor in a direct message channel."""
+        kwargs.update({"channel": channel, "ts": ts})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("im.mark", json=kwargs)
+
+    def im_open(
+        self,
+        *,
+        user: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Opens a direct message channel."""
+        kwargs.update({"user": user})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("im.open", json=kwargs)
+
+    def im_replies(
+        self,
+        *,
+        channel: str,
+        thread_ts: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Retrieve a thread of messages posted to a direct message conversation"""
+        kwargs.update({"channel": channel, "thread_ts": thread_ts})
+        return self.api_call("im.replies", http_verb="GET", params=kwargs)
+
+    # --------------------------
+
+    def migration_exchange(
+        self,
+        *,
+        users: Union[str, Sequence[str]],
+        team_id: Optional[str] = None,
+        to_old: Optional[bool] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """For Enterprise Grid workspaces, map local user IDs to global user IDs
+        https://docs.slack.dev/reference/methods/migration.exchange
+        """
+        if isinstance(users, (list, tuple)):
+            kwargs.update({"users": ",".join(users)})
+        else:
+            kwargs.update({"users": users})
+        kwargs.update({"team_id": team_id, "to_old": to_old})
+        return self.api_call("migration.exchange", http_verb="GET", params=kwargs)
+
+    # --------------------------
+    # Deprecated: mpim.*
+    # You can use conversations.* APIs instead.
+    # https://docs.slack.dev/changelog/2020-01-deprecating-antecedents-to-the-conversations-api/
+    # --------------------------
+
+    def mpim_close(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Closes a multiparty direct message channel."""
+        kwargs.update({"channel": channel})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("mpim.close", json=kwargs)
+
+    def mpim_history(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Fetches history of messages and events from a multiparty direct message."""
+        kwargs.update({"channel": channel})
+        return self.api_call("mpim.history", http_verb="GET", params=kwargs)
+
+    def mpim_list(
+        self,
+        **kwargs,
+    ) -> SlackResponse:
+        """Lists multiparty direct message channels for the calling user."""
+        return self.api_call("mpim.list", http_verb="GET", params=kwargs)
+
+    def mpim_mark(
+        self,
+        *,
+        channel: str,
+        ts: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Sets the read cursor in a multiparty direct message channel."""
+        kwargs.update({"channel": channel, "ts": ts})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("mpim.mark", json=kwargs)
+
+    def mpim_open(
+        self,
+        *,
+        users: Union[str, Sequence[str]],
+        **kwargs,
+    ) -> SlackResponse:
+        """This method opens a multiparty direct message."""
+        if isinstance(users, (list, tuple)):
+            kwargs.update({"users": ",".join(users)})
+        else:
+            kwargs.update({"users": users})
+        return self.api_call("mpim.open", params=kwargs)
+
+    def mpim_replies(
+        self,
+        *,
+        channel: str,
+        thread_ts: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Retrieve a thread of messages posted to a direct message conversation from a
+        multiparty direct message.
+        """
+        kwargs.update({"channel": channel, "thread_ts": thread_ts})
+        return self.api_call("mpim.replies", http_verb="GET", params=kwargs)
+
+    # --------------------------
+
+    def oauth_v2_access(
+        self,
+        *,
+        client_id: str,
+        client_secret: str,
+        # This field is required when processing the OAuth redirect URL requests
+        # while it's absent for token rotation
+        code: Optional[str] = None,
+        redirect_uri: Optional[str] = None,
+        # This field is required for token rotation
+        grant_type: Optional[str] = None,
+        # This field is required for token rotation
+        refresh_token: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Exchanges a temporary OAuth verifier code for an access token.
+        https://docs.slack.dev/reference/methods/oauth.v2.access
+        """
+        if redirect_uri is not None:
+            kwargs.update({"redirect_uri": redirect_uri})
+        if code is not None:
+            kwargs.update({"code": code})
+        if grant_type is not None:
+            kwargs.update({"grant_type": grant_type})
+        if refresh_token is not None:
+            kwargs.update({"refresh_token": refresh_token})
+        return self.api_call(
+            "oauth.v2.access",
+            data=kwargs,
+            auth={"client_id": client_id, "client_secret": client_secret},
+        )
+
+    def oauth_access(
+        self,
+        *,
+        client_id: str,
+        client_secret: str,
+        code: str,
+        redirect_uri: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Exchanges a temporary OAuth verifier code for an access token.
+        https://docs.slack.dev/reference/methods/oauth.access
+        """
+        if redirect_uri is not None:
+            kwargs.update({"redirect_uri": redirect_uri})
+        kwargs.update({"code": code})
+        return self.api_call(
+            "oauth.access",
+            data=kwargs,
+            auth={"client_id": client_id, "client_secret": client_secret},
+        )
+
+    def oauth_v2_exchange(
+        self,
+        *,
+        token: str,
+        client_id: str,
+        client_secret: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Exchanges a legacy access token for a new expiring access token and refresh token
+        https://docs.slack.dev/reference/methods/oauth.v2.exchange
+        """
+        kwargs.update({"client_id": client_id, "client_secret": client_secret, "token": token})
+        return self.api_call("oauth.v2.exchange", params=kwargs)
+
+    def openid_connect_token(
+        self,
+        client_id: str,
+        client_secret: str,
+        code: Optional[str] = None,
+        redirect_uri: Optional[str] = None,
+        grant_type: Optional[str] = None,
+        refresh_token: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Exchanges a temporary OAuth verifier code for an access token for Sign in with Slack.
+        https://docs.slack.dev/reference/methods/openid.connect.token
+        """
+        if redirect_uri is not None:
+            kwargs.update({"redirect_uri": redirect_uri})
+        if code is not None:
+            kwargs.update({"code": code})
+        if grant_type is not None:
+            kwargs.update({"grant_type": grant_type})
+        if refresh_token is not None:
+            kwargs.update({"refresh_token": refresh_token})
+        return self.api_call(
+            "openid.connect.token",
+            data=kwargs,
+            auth={"client_id": client_id, "client_secret": client_secret},
+        )
+
+    def openid_connect_userInfo(
+        self,
+        **kwargs,
+    ) -> SlackResponse:
+        """Get the identity of a user who has authorized Sign in with Slack.
+        https://docs.slack.dev/reference/methods/openid.connect.userInfo
+        """
+        return self.api_call("openid.connect.userInfo", params=kwargs)
+
+    def pins_add(
+        self,
+        *,
+        channel: str,
+        timestamp: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Pins an item to a channel.
+        https://docs.slack.dev/reference/methods/pins.add
+        """
+        kwargs.update({"channel": channel, "timestamp": timestamp})
+        return self.api_call("pins.add", params=kwargs)
+
+    def pins_list(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Lists items pinned to a channel.
+        https://docs.slack.dev/reference/methods/pins.list
+        """
+        kwargs.update({"channel": channel})
+        return self.api_call("pins.list", http_verb="GET", params=kwargs)
+
+    def pins_remove(
+        self,
+        *,
+        channel: str,
+        timestamp: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Un-pins an item from a channel.
+        https://docs.slack.dev/reference/methods/pins.remove
+        """
+        kwargs.update({"channel": channel, "timestamp": timestamp})
+        return self.api_call("pins.remove", params=kwargs)
+
+    def reactions_add(
+        self,
+        *,
+        channel: str,
+        name: str,
+        timestamp: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Adds a reaction to an item.
+        https://docs.slack.dev/reference/methods/reactions.add
+        """
+        kwargs.update({"channel": channel, "name": name, "timestamp": timestamp})
+        return self.api_call("reactions.add", params=kwargs)
+
+    def reactions_get(
+        self,
+        *,
+        channel: Optional[str] = None,
+        file: Optional[str] = None,
+        file_comment: Optional[str] = None,
+        full: Optional[bool] = None,
+        timestamp: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Gets reactions for an item.
+        https://docs.slack.dev/reference/methods/reactions.get
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "file": file,
+                "file_comment": file_comment,
+                "full": full,
+                "timestamp": timestamp,
+            }
+        )
+        return self.api_call("reactions.get", http_verb="GET", params=kwargs)
+
+    def reactions_list(
+        self,
+        *,
+        count: Optional[int] = None,
+        cursor: Optional[str] = None,
+        full: Optional[bool] = None,
+        limit: Optional[int] = None,
+        page: Optional[int] = None,
+        team_id: Optional[str] = None,
+        user: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Lists reactions made by a user.
+        https://docs.slack.dev/reference/methods/reactions.list
+        """
+        kwargs.update(
+            {
+                "count": count,
+                "cursor": cursor,
+                "full": full,
+                "limit": limit,
+                "page": page,
+                "team_id": team_id,
+                "user": user,
+            }
+        )
+        return self.api_call("reactions.list", http_verb="GET", params=kwargs)
+
+    def reactions_remove(
+        self,
+        *,
+        name: str,
+        channel: Optional[str] = None,
+        file: Optional[str] = None,
+        file_comment: Optional[str] = None,
+        timestamp: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Removes a reaction from an item.
+        https://docs.slack.dev/reference/methods/reactions.remove
+        """
+        kwargs.update(
+            {
+                "name": name,
+                "channel": channel,
+                "file": file,
+                "file_comment": file_comment,
+                "timestamp": timestamp,
+            }
+        )
+        return self.api_call("reactions.remove", params=kwargs)
+
+    def reminders_add(
+        self,
+        *,
+        text: str,
+        time: str,
+        team_id: Optional[str] = None,
+        user: Optional[str] = None,
+        recurrence: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Creates a reminder.
+        https://docs.slack.dev/reference/methods/reminders.add
+        """
+        kwargs.update(
+            {
+                "text": text,
+                "time": time,
+                "team_id": team_id,
+                "user": user,
+                "recurrence": recurrence,
+            }
+        )
+        return self.api_call("reminders.add", params=kwargs)
+
+    def reminders_complete(
+        self,
+        *,
+        reminder: str,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Marks a reminder as complete.
+        https://docs.slack.dev/reference/methods/reminders.complete
+        """
+        kwargs.update({"reminder": reminder, "team_id": team_id})
+        return self.api_call("reminders.complete", params=kwargs)
+
+    def reminders_delete(
+        self,
+        *,
+        reminder: str,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Deletes a reminder.
+        https://docs.slack.dev/reference/methods/reminders.delete
+        """
+        kwargs.update({"reminder": reminder, "team_id": team_id})
+        return self.api_call("reminders.delete", params=kwargs)
+
+    def reminders_info(
+        self,
+        *,
+        reminder: str,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Gets information about a reminder.
+        https://docs.slack.dev/reference/methods/reminders.info
+        """
+        kwargs.update({"reminder": reminder, "team_id": team_id})
+        return self.api_call("reminders.info", http_verb="GET", params=kwargs)
+
+    def reminders_list(
+        self,
+        *,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Lists all reminders created by or for a given user.
+        https://docs.slack.dev/reference/methods/reminders.list
+        """
+        kwargs.update({"team_id": team_id})
+        return self.api_call("reminders.list", http_verb="GET", params=kwargs)
+
+    def rtm_connect(
+        self,
+        *,
+        batch_presence_aware: Optional[bool] = None,
+        presence_sub: Optional[bool] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Starts a Real Time Messaging session.
+        https://docs.slack.dev/reference/methods/rtm.connect
+        """
+        kwargs.update({"batch_presence_aware": batch_presence_aware, "presence_sub": presence_sub})
+        return self.api_call("rtm.connect", http_verb="GET", params=kwargs)
+
+    def rtm_start(
+        self,
+        *,
+        batch_presence_aware: Optional[bool] = None,
+        include_locale: Optional[bool] = None,
+        mpim_aware: Optional[bool] = None,
+        no_latest: Optional[bool] = None,
+        no_unreads: Optional[bool] = None,
+        presence_sub: Optional[bool] = None,
+        simple_latest: Optional[bool] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Starts a Real Time Messaging session.
+        https://docs.slack.dev/reference/methods/rtm.start
+        """
+        kwargs.update(
+            {
+                "batch_presence_aware": batch_presence_aware,
+                "include_locale": include_locale,
+                "mpim_aware": mpim_aware,
+                "no_latest": no_latest,
+                "no_unreads": no_unreads,
+                "presence_sub": presence_sub,
+                "simple_latest": simple_latest,
+            }
+        )
+        return self.api_call("rtm.start", http_verb="GET", params=kwargs)
+
+    def search_all(
+        self,
+        *,
+        query: str,
+        count: Optional[int] = None,
+        highlight: Optional[bool] = None,
+        page: Optional[int] = None,
+        sort: Optional[str] = None,
+        sort_dir: Optional[str] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Searches for messages and files matching a query.
+        https://docs.slack.dev/reference/methods/search.all
+        """
+        kwargs.update(
+            {
+                "query": query,
+                "count": count,
+                "highlight": highlight,
+                "page": page,
+                "sort": sort,
+                "sort_dir": sort_dir,
+                "team_id": team_id,
+            }
+        )
+        return self.api_call("search.all", http_verb="GET", params=kwargs)
+
+    def search_files(
+        self,
+        *,
+        query: str,
+        count: Optional[int] = None,
+        highlight: Optional[bool] = None,
+        page: Optional[int] = None,
+        sort: Optional[str] = None,
+        sort_dir: Optional[str] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Searches for files matching a query.
+        https://docs.slack.dev/reference/methods/search.files
+        """
+        kwargs.update(
+            {
+                "query": query,
+                "count": count,
+                "highlight": highlight,
+                "page": page,
+                "sort": sort,
+                "sort_dir": sort_dir,
+                "team_id": team_id,
+            }
+        )
+        return self.api_call("search.files", http_verb="GET", params=kwargs)
+
+    def search_messages(
+        self,
+        *,
+        query: str,
+        count: Optional[int] = None,
+        cursor: Optional[str] = None,
+        highlight: Optional[bool] = None,
+        page: Optional[int] = None,
+        sort: Optional[str] = None,
+        sort_dir: Optional[str] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Searches for messages matching a query.
+        https://docs.slack.dev/reference/methods/search.messages
+        """
+        kwargs.update(
+            {
+                "query": query,
+                "count": count,
+                "cursor": cursor,
+                "highlight": highlight,
+                "page": page,
+                "sort": sort,
+                "sort_dir": sort_dir,
+                "team_id": team_id,
+            }
+        )
+        return self.api_call("search.messages", http_verb="GET", params=kwargs)
+
+    def slackLists_access_delete(
+        self,
+        *,
+        list_id: str,
+        channel_ids: Optional[List[str]] = None,
+        user_ids: Optional[List[str]] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Revoke access to a List for specified entities.
+        https://docs.slack.dev/reference/methods/slackLists.access.delete
+        """
+        kwargs.update({"list_id": list_id, "channel_ids": channel_ids, "user_ids": user_ids})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("slackLists.access.delete", json=kwargs)
+
+    def slackLists_access_set(
+        self,
+        *,
+        list_id: str,
+        access_level: str,
+        channel_ids: Optional[List[str]] = None,
+        user_ids: Optional[List[str]] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Set the access level to a List for specified entities.
+        https://docs.slack.dev/reference/methods/slackLists.access.set
+        """
+        kwargs.update({"list_id": list_id, "access_level": access_level, "channel_ids": channel_ids, "user_ids": user_ids})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("slackLists.access.set", json=kwargs)
+
+    def slackLists_create(
+        self,
+        *,
+        name: str,
+        description_blocks: Optional[Union[str, Sequence[Union[Dict, RichTextBlock]]]] = None,
+        schema: Optional[List[Dict[str, Any]]] = None,
+        copy_from_list_id: Optional[str] = None,
+        include_copied_list_records: Optional[bool] = None,
+        todo_mode: Optional[bool] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Creates a List.
+        https://docs.slack.dev/reference/methods/slackLists.create
+        """
+        kwargs.update(
+            {
+                "name": name,
+                "description_blocks": description_blocks,
+                "schema": schema,
+                "copy_from_list_id": copy_from_list_id,
+                "include_copied_list_records": include_copied_list_records,
+                "todo_mode": todo_mode,
+            }
+        )
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("slackLists.create", json=kwargs)
+
+    def slackLists_download_get(
+        self,
+        *,
+        list_id: str,
+        job_id: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Retrieve List download URL from an export job to download List contents.
+        https://docs.slack.dev/reference/methods/slackLists.download.get
+        """
+        kwargs.update(
+            {
+                "list_id": list_id,
+                "job_id": job_id,
+            }
+        )
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("slackLists.download.get", json=kwargs)
+
+    def slackLists_download_start(
+        self,
+        *,
+        list_id: str,
+        include_archived: Optional[bool] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Initiate a job to export List contents.
+        https://docs.slack.dev/reference/methods/slackLists.download.start
+        """
+        kwargs.update(
+            {
+                "list_id": list_id,
+                "include_archived": include_archived,
+            }
+        )
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("slackLists.download.start", json=kwargs)
+
+    def slackLists_items_create(
+        self,
+        *,
+        list_id: str,
+        duplicated_item_id: Optional[str] = None,
+        parent_item_id: Optional[str] = None,
+        initial_fields: Optional[List[Dict[str, Any]]] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Add a new item to an existing List.
+        https://docs.slack.dev/reference/methods/slackLists.items.create
+        """
+        kwargs.update(
+            {
+                "list_id": list_id,
+                "duplicated_item_id": duplicated_item_id,
+                "parent_item_id": parent_item_id,
+                "initial_fields": initial_fields,
+            }
+        )
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("slackLists.items.create", json=kwargs)
+
+    def slackLists_items_delete(
+        self,
+        *,
+        list_id: str,
+        id: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Deletes an item from an existing List.
+        https://docs.slack.dev/reference/methods/slackLists.items.delete
+        """
+        kwargs.update(
+            {
+                "list_id": list_id,
+                "id": id,
+            }
+        )
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("slackLists.items.delete", json=kwargs)
+
+    def slackLists_items_deleteMultiple(
+        self,
+        *,
+        list_id: str,
+        ids: List[str],
+        **kwargs,
+    ) -> SlackResponse:
+        """Deletes multiple items from an existing List.
+        https://docs.slack.dev/reference/methods/slackLists.items.deleteMultiple
+        """
+        kwargs.update(
+            {
+                "list_id": list_id,
+                "ids": ids,
+            }
+        )
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("slackLists.items.deleteMultiple", json=kwargs)
+
+    def slackLists_items_info(
+        self,
+        *,
+        list_id: str,
+        id: str,
+        include_is_subscribed: Optional[bool] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Get a row from a List.
+        https://docs.slack.dev/reference/methods/slackLists.items.info
+        """
+        kwargs.update(
+            {
+                "list_id": list_id,
+                "id": id,
+                "include_is_subscribed": include_is_subscribed,
+            }
+        )
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("slackLists.items.info", json=kwargs)
+
+    def slackLists_items_list(
+        self,
+        *,
+        list_id: str,
+        limit: Optional[int] = None,
+        cursor: Optional[str] = None,
+        archived: Optional[bool] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Get records from a List.
+        https://docs.slack.dev/reference/methods/slackLists.items.list
+        """
+        kwargs.update(
+            {
+                "list_id": list_id,
+                "limit": limit,
+                "cursor": cursor,
+                "archived": archived,
+            }
+        )
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("slackLists.items.list", json=kwargs)
+
+    def slackLists_items_update(
+        self,
+        *,
+        list_id: str,
+        cells: List[Dict[str, Any]],
+        **kwargs,
+    ) -> SlackResponse:
+        """Updates cells in a List.
+        https://docs.slack.dev/reference/methods/slackLists.items.update
+        """
+        kwargs.update(
+            {
+                "list_id": list_id,
+                "cells": cells,
+            }
+        )
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("slackLists.items.update", json=kwargs)
+
+    def slackLists_update(
+        self,
+        *,
+        id: str,
+        name: Optional[str] = None,
+        description_blocks: Optional[Union[str, Sequence[Union[Dict, RichTextBlock]]]] = None,
+        todo_mode: Optional[bool] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Update a List.
+        https://docs.slack.dev/reference/methods/slackLists.update
+        """
+        kwargs.update(
+            {
+                "id": id,
+                "name": name,
+                "description_blocks": description_blocks,
+                "todo_mode": todo_mode,
+            }
+        )
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("slackLists.update", json=kwargs)
+
+    def stars_add(
+        self,
+        *,
+        channel: Optional[str] = None,
+        file: Optional[str] = None,
+        file_comment: Optional[str] = None,
+        timestamp: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Adds a star to an item.
+        https://docs.slack.dev/reference/methods/stars.add
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "file": file,
+                "file_comment": file_comment,
+                "timestamp": timestamp,
+            }
+        )
+        return self.api_call("stars.add", params=kwargs)
+
+    def stars_list(
+        self,
+        *,
+        count: Optional[int] = None,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        page: Optional[int] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Lists stars for a user.
+        https://docs.slack.dev/reference/methods/stars.list
+        """
+        kwargs.update(
+            {
+                "count": count,
+                "cursor": cursor,
+                "limit": limit,
+                "page": page,
+                "team_id": team_id,
+            }
+        )
+        return self.api_call("stars.list", http_verb="GET", params=kwargs)
+
+    def stars_remove(
+        self,
+        *,
+        channel: Optional[str] = None,
+        file: Optional[str] = None,
+        file_comment: Optional[str] = None,
+        timestamp: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Removes a star from an item.
+        https://docs.slack.dev/reference/methods/stars.remove
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "file": file,
+                "file_comment": file_comment,
+                "timestamp": timestamp,
+            }
+        )
+        return self.api_call("stars.remove", params=kwargs)
+
+    def team_accessLogs(
+        self,
+        *,
+        before: Optional[Union[int, str]] = None,
+        count: Optional[Union[int, str]] = None,
+        page: Optional[Union[int, str]] = None,
+        team_id: Optional[str] = None,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Gets the access logs for the current team.
+        https://docs.slack.dev/reference/methods/team.accessLogs
+        """
+        kwargs.update(
+            {
+                "before": before,
+                "count": count,
+                "page": page,
+                "team_id": team_id,
+                "cursor": cursor,
+                "limit": limit,
+            }
+        )
+        return self.api_call("team.accessLogs", http_verb="GET", params=kwargs)
+
+    def team_billableInfo(
+        self,
+        *,
+        team_id: Optional[str] = None,
+        user: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Gets billable users information for the current team.
+        https://docs.slack.dev/reference/methods/team.billableInfo
+        """
+        kwargs.update({"team_id": team_id, "user": user})
+        return self.api_call("team.billableInfo", http_verb="GET", params=kwargs)
+
+    def team_billing_info(
+        self,
+        **kwargs,
+    ) -> SlackResponse:
+        """Reads a workspace's billing plan information.
+        https://docs.slack.dev/reference/methods/team.billing.info
+        """
+        return self.api_call("team.billing.info", params=kwargs)
+
+    def team_externalTeams_disconnect(
+        self,
+        *,
+        target_team: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Disconnects an external organization.
+        https://docs.slack.dev/reference/methods/team.externalTeams.disconnect
+        """
+        kwargs.update(
+            {
+                "target_team": target_team,
+            }
+        )
+        return self.api_call("team.externalTeams.disconnect", params=kwargs)
+
+    def team_externalTeams_list(
+        self,
+        *,
+        connection_status_filter: Optional[str] = None,
+        slack_connect_pref_filter: Optional[Sequence[str]] = None,
+        sort_direction: Optional[str] = None,
+        sort_field: Optional[str] = None,
+        workspace_filter: Optional[Sequence[str]] = None,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Returns a list of all the external teams connected and details about the connection.
+        https://docs.slack.dev/reference/methods/team.externalTeams.list
+        """
+        kwargs.update(
+            {
+                "connection_status_filter": connection_status_filter,
+                "sort_direction": sort_direction,
+                "sort_field": sort_field,
+                "cursor": cursor,
+                "limit": limit,
+            }
+        )
+        if slack_connect_pref_filter is not None:
+            if isinstance(slack_connect_pref_filter, (list, tuple)):
+                kwargs.update({"slack_connect_pref_filter": ",".join(slack_connect_pref_filter)})
+            else:
+                kwargs.update({"slack_connect_pref_filter": slack_connect_pref_filter})
+        if workspace_filter is not None:
+            if isinstance(workspace_filter, (list, tuple)):
+                kwargs.update({"workspace_filter": ",".join(workspace_filter)})
+            else:
+                kwargs.update({"workspace_filter": workspace_filter})
+        return self.api_call("team.externalTeams.list", http_verb="GET", params=kwargs)
+
+    def team_info(
+        self,
+        *,
+        team: Optional[str] = None,
+        domain: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Gets information about the current team.
+        https://docs.slack.dev/reference/methods/team.info
+        """
+        kwargs.update({"team": team, "domain": domain})
+        return self.api_call("team.info", http_verb="GET", params=kwargs)
+
+    def team_integrationLogs(
+        self,
+        *,
+        app_id: Optional[str] = None,
+        change_type: Optional[str] = None,
+        count: Optional[Union[int, str]] = None,
+        page: Optional[Union[int, str]] = None,
+        service_id: Optional[str] = None,
+        team_id: Optional[str] = None,
+        user: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Gets the integration logs for the current team.
+        https://docs.slack.dev/reference/methods/team.integrationLogs
+        """
+        kwargs.update(
+            {
+                "app_id": app_id,
+                "change_type": change_type,
+                "count": count,
+                "page": page,
+                "service_id": service_id,
+                "team_id": team_id,
+                "user": user,
+            }
+        )
+        return self.api_call("team.integrationLogs", http_verb="GET", params=kwargs)
+
+    def team_profile_get(
+        self,
+        *,
+        visibility: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Retrieve a team's profile.
+        https://docs.slack.dev/reference/methods/team.profile.get
+        """
+        kwargs.update({"visibility": visibility})
+        return self.api_call("team.profile.get", http_verb="GET", params=kwargs)
+
+    def team_preferences_list(
+        self,
+        **kwargs,
+    ) -> SlackResponse:
+        """Retrieve a list of a workspace's team preferences.
+        https://docs.slack.dev/reference/methods/team.preferences.list
+        """
+        return self.api_call("team.preferences.list", params=kwargs)
+
+    def usergroups_create(
+        self,
+        *,
+        name: str,
+        channels: Optional[Union[str, Sequence[str]]] = None,
+        description: Optional[str] = None,
+        handle: Optional[str] = None,
+        include_count: Optional[bool] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Create a User Group
+        https://docs.slack.dev/reference/methods/usergroups.create
+        """
+        kwargs.update(
+            {
+                "name": name,
+                "description": description,
+                "handle": handle,
+                "include_count": include_count,
+                "team_id": team_id,
+            }
+        )
+        if isinstance(channels, (list, tuple)):
+            kwargs.update({"channels": ",".join(channels)})
+        else:
+            kwargs.update({"channels": channels})
+        return self.api_call("usergroups.create", params=kwargs)
+
+    def usergroups_disable(
+        self,
+        *,
+        usergroup: str,
+        include_count: Optional[bool] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Disable an existing User Group
+        https://docs.slack.dev/reference/methods/usergroups.disable
+        """
+        kwargs.update({"usergroup": usergroup, "include_count": include_count, "team_id": team_id})
+        return self.api_call("usergroups.disable", params=kwargs)
+
+    def usergroups_enable(
+        self,
+        *,
+        usergroup: str,
+        include_count: Optional[bool] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Enable a User Group
+        https://docs.slack.dev/reference/methods/usergroups.enable
+        """
+        kwargs.update({"usergroup": usergroup, "include_count": include_count, "team_id": team_id})
+        return self.api_call("usergroups.enable", params=kwargs)
+
+    def usergroups_list(
+        self,
+        *,
+        include_count: Optional[bool] = None,
+        include_disabled: Optional[bool] = None,
+        include_users: Optional[bool] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """List all User Groups for a team
+        https://docs.slack.dev/reference/methods/usergroups.list
+        """
+        kwargs.update(
+            {
+                "include_count": include_count,
+                "include_disabled": include_disabled,
+                "include_users": include_users,
+                "team_id": team_id,
+            }
+        )
+        return self.api_call("usergroups.list", http_verb="GET", params=kwargs)
+
+    def usergroups_update(
+        self,
+        *,
+        usergroup: str,
+        channels: Optional[Union[str, Sequence[str]]] = None,
+        description: Optional[str] = None,
+        handle: Optional[str] = None,
+        include_count: Optional[bool] = None,
+        name: Optional[str] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Update an existing User Group
+        https://docs.slack.dev/reference/methods/usergroups.update
+        """
+        kwargs.update(
+            {
+                "usergroup": usergroup,
+                "description": description,
+                "handle": handle,
+                "include_count": include_count,
+                "name": name,
+                "team_id": team_id,
+            }
+        )
+        if isinstance(channels, (list, tuple)):
+            kwargs.update({"channels": ",".join(channels)})
+        else:
+            kwargs.update({"channels": channels})
+        return self.api_call("usergroups.update", params=kwargs)
+
+    def usergroups_users_list(
+        self,
+        *,
+        usergroup: str,
+        include_disabled: Optional[bool] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """List all users in a User Group
+        https://docs.slack.dev/reference/methods/usergroups.users.list
+        """
+        kwargs.update(
+            {
+                "usergroup": usergroup,
+                "include_disabled": include_disabled,
+                "team_id": team_id,
+            }
+        )
+        return self.api_call("usergroups.users.list", http_verb="GET", params=kwargs)
+
+    def usergroups_users_update(
+        self,
+        *,
+        usergroup: str,
+        users: Union[str, Sequence[str]],
+        include_count: Optional[bool] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Update the list of users for a User Group
+        https://docs.slack.dev/reference/methods/usergroups.users.update
+        """
+        kwargs.update(
+            {
+                "usergroup": usergroup,
+                "include_count": include_count,
+                "team_id": team_id,
+            }
+        )
+        if isinstance(users, (list, tuple)):
+            kwargs.update({"users": ",".join(users)})
+        else:
+            kwargs.update({"users": users})
+        return self.api_call("usergroups.users.update", params=kwargs)
+
+    def users_conversations(
+        self,
+        *,
+        cursor: Optional[str] = None,
+        exclude_archived: Optional[bool] = None,
+        limit: Optional[int] = None,
+        team_id: Optional[str] = None,
+        types: Optional[Union[str, Sequence[str]]] = None,
+        user: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """List conversations the calling user may access.
+        https://docs.slack.dev/reference/methods/users.conversations
+        """
+        kwargs.update(
+            {
+                "cursor": cursor,
+                "exclude_archived": exclude_archived,
+                "limit": limit,
+                "team_id": team_id,
+                "user": user,
+            }
+        )
+        if isinstance(types, (list, tuple)):
+            kwargs.update({"types": ",".join(types)})
+        else:
+            kwargs.update({"types": types})
+        return self.api_call("users.conversations", http_verb="GET", params=kwargs)
+
+    def users_deletePhoto(
+        self,
+        **kwargs,
+    ) -> SlackResponse:
+        """Delete the user profile photo
+        https://docs.slack.dev/reference/methods/users.deletePhoto
+        """
+        return self.api_call("users.deletePhoto", http_verb="GET", params=kwargs)
+
+    def users_getPresence(
+        self,
+        *,
+        user: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Gets user presence information.
+        https://docs.slack.dev/reference/methods/users.getPresence
+        """
+        kwargs.update({"user": user})
+        return self.api_call("users.getPresence", http_verb="GET", params=kwargs)
+
+    def users_identity(
+        self,
+        **kwargs,
+    ) -> SlackResponse:
+        """Get a user's identity.
+        https://docs.slack.dev/reference/methods/users.identity
+        """
+        return self.api_call("users.identity", http_verb="GET", params=kwargs)
+
+    def users_info(
+        self,
+        *,
+        user: str,
+        include_locale: Optional[bool] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Gets information about a user.
+        https://docs.slack.dev/reference/methods/users.info
+        """
+        kwargs.update({"user": user, "include_locale": include_locale})
+        return self.api_call("users.info", http_verb="GET", params=kwargs)
+
+    def users_list(
+        self,
+        *,
+        cursor: Optional[str] = None,
+        include_locale: Optional[bool] = None,
+        limit: Optional[int] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Lists all users in a Slack team.
+        https://docs.slack.dev/reference/methods/users.list
+        """
+        kwargs.update(
+            {
+                "cursor": cursor,
+                "include_locale": include_locale,
+                "limit": limit,
+                "team_id": team_id,
+            }
+        )
+        return self.api_call("users.list", http_verb="GET", params=kwargs)
+
+    def users_lookupByEmail(
+        self,
+        *,
+        email: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Find a user with an email address.
+        https://docs.slack.dev/reference/methods/users.lookupByEmail
+        """
+        kwargs.update({"email": email})
+        return self.api_call("users.lookupByEmail", http_verb="GET", params=kwargs)
+
+    def users_setPhoto(
+        self,
+        *,
+        image: Union[str, IOBase],
+        crop_w: Optional[Union[int, str]] = None,
+        crop_x: Optional[Union[int, str]] = None,
+        crop_y: Optional[Union[int, str]] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Set the user profile photo
+        https://docs.slack.dev/reference/methods/users.setPhoto
+        """
+        kwargs.update({"crop_w": crop_w, "crop_x": crop_x, "crop_y": crop_y})
+        return self.api_call("users.setPhoto", files={"image": image}, data=kwargs)
+
+    def users_setPresence(
+        self,
+        *,
+        presence: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Manually sets user presence.
+        https://docs.slack.dev/reference/methods/users.setPresence
+        """
+        kwargs.update({"presence": presence})
+        return self.api_call("users.setPresence", params=kwargs)
+
+    def users_discoverableContacts_lookup(
+        self,
+        email: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Lookup an email address to see if someone is on Slack
+        https://docs.slack.dev/reference/methods/users.discoverableContacts.lookup
+        """
+        kwargs.update({"email": email})
+        return self.api_call("users.discoverableContacts.lookup", params=kwargs)
+
+    def users_profile_get(
+        self,
+        *,
+        user: Optional[str] = None,
+        include_labels: Optional[bool] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Retrieves a user's profile information.
+        https://docs.slack.dev/reference/methods/users.profile.get
+        """
+        kwargs.update({"user": user, "include_labels": include_labels})
+        return self.api_call("users.profile.get", http_verb="GET", params=kwargs)
+
+    def users_profile_set(
+        self,
+        *,
+        name: Optional[str] = None,
+        value: Optional[str] = None,
+        user: Optional[str] = None,
+        profile: Optional[Dict] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Set the profile information for a user.
+        https://docs.slack.dev/reference/methods/users.profile.set
+        """
+        kwargs.update(
+            {
+                "name": name,
+                "profile": profile,
+                "user": user,
+                "value": value,
+            }
+        )
+        kwargs = _remove_none_values(kwargs)
+        # NOTE: Intentionally using json for the "profile" parameter
+        return self.api_call("users.profile.set", json=kwargs)
+
+    def views_open(
+        self,
+        *,
+        trigger_id: Optional[str] = None,
+        interactivity_pointer: Optional[str] = None,
+        view: Union[dict, View],
+        **kwargs,
+    ) -> SlackResponse:
+        """Open a view for a user.
+        https://docs.slack.dev/reference/methods/views.open
+        See https://docs.slack.dev/surfaces/modals/ for details.
+        """
+        kwargs.update({"trigger_id": trigger_id, "interactivity_pointer": interactivity_pointer})
+        if isinstance(view, View):
+            kwargs.update({"view": view.to_dict()})
+        else:
+            kwargs.update({"view": view})
+        kwargs = _remove_none_values(kwargs)
+        # NOTE: Intentionally using json for the "view" parameter
+        return self.api_call("views.open", json=kwargs)
+
+    def views_push(
+        self,
+        *,
+        trigger_id: Optional[str] = None,
+        interactivity_pointer: Optional[str] = None,
+        view: Union[dict, View],
+        **kwargs,
+    ) -> SlackResponse:
+        """Push a view onto the stack of a root view.
+        Push a new view onto the existing view stack by passing a view
+        payload and a valid trigger_id generated from an interaction
+        within the existing modal.
+        Read the modals documentation (https://docs.slack.dev/surfaces/modals/)
+        to learn more about the lifecycle and intricacies of views.
+        https://docs.slack.dev/reference/methods/views.push
+        """
+        kwargs.update({"trigger_id": trigger_id, "interactivity_pointer": interactivity_pointer})
+        if isinstance(view, View):
+            kwargs.update({"view": view.to_dict()})
+        else:
+            kwargs.update({"view": view})
+        kwargs = _remove_none_values(kwargs)
+        # NOTE: Intentionally using json for the "view" parameter
+        return self.api_call("views.push", json=kwargs)
+
+    def views_update(
+        self,
+        *,
+        view: Union[dict, View],
+        external_id: Optional[str] = None,
+        view_id: Optional[str] = None,
+        hash: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Update an existing view.
+        Update a view by passing a new view definition along with the
+        view_id returned in views.open or the external_id.
+        See the modals documentation (https://docs.slack.dev/surfaces/modals/#updating_views)
+        to learn more about updating views and avoiding race conditions with the hash argument.
+        https://docs.slack.dev/reference/methods/views.update
+        """
+        if isinstance(view, View):
+            kwargs.update({"view": view.to_dict()})
+        else:
+            kwargs.update({"view": view})
+        if external_id:
+            kwargs.update({"external_id": external_id})
+        elif view_id:
+            kwargs.update({"view_id": view_id})
+        else:
+            raise e.SlackRequestError("Either view_id or external_id is required.")
+        kwargs.update({"hash": hash})
+        kwargs = _remove_none_values(kwargs)
+        # NOTE: Intentionally using json for the "view" parameter
+        return self.api_call("views.update", json=kwargs)
+
+    def views_publish(
+        self,
+        *,
+        user_id: str,
+        view: Union[dict, View],
+        hash: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Publish a static view for a User.
+        Create or update the view that comprises an
+        app's Home tab (https://docs.slack.dev/surfaces/app-home/)
+        https://docs.slack.dev/reference/methods/views.publish
+        """
+        kwargs.update({"user_id": user_id, "hash": hash})
+        if isinstance(view, View):
+            kwargs.update({"view": view.to_dict()})
+        else:
+            kwargs.update({"view": view})
+        kwargs = _remove_none_values(kwargs)
+        # NOTE: Intentionally using json for the "view" parameter
+        return self.api_call("views.publish", json=kwargs)
+
+    def workflows_featured_add(
+        self,
+        *,
+        channel_id: str,
+        trigger_ids: Union[str, Sequence[str]],
+        **kwargs,
+    ) -> SlackResponse:
+        """Add featured workflows to a channel.
+        https://docs.slack.dev/reference/methods/workflows.featured.add
+        """
+        kwargs.update({"channel_id": channel_id})
+        if isinstance(trigger_ids, (list, tuple)):
+            kwargs.update({"trigger_ids": ",".join(trigger_ids)})
+        else:
+            kwargs.update({"trigger_ids": trigger_ids})
+        return self.api_call("workflows.featured.add", params=kwargs)
+
+    def workflows_featured_list(
+        self,
+        *,
+        channel_ids: Union[str, Sequence[str]],
+        **kwargs,
+    ) -> SlackResponse:
+        """List the featured workflows for specified channels.
+        https://docs.slack.dev/reference/methods/workflows.featured.list
+        """
+        if isinstance(channel_ids, (list, tuple)):
+            kwargs.update({"channel_ids": ",".join(channel_ids)})
+        else:
+            kwargs.update({"channel_ids": channel_ids})
+        return self.api_call("workflows.featured.list", params=kwargs)
+
+    def workflows_featured_remove(
+        self,
+        *,
+        channel_id: str,
+        trigger_ids: Union[str, Sequence[str]],
+        **kwargs,
+    ) -> SlackResponse:
+        """Remove featured workflows from a channel.
+        https://docs.slack.dev/reference/methods/workflows.featured.remove
+        """
+        kwargs.update({"channel_id": channel_id})
+        if isinstance(trigger_ids, (list, tuple)):
+            kwargs.update({"trigger_ids": ",".join(trigger_ids)})
+        else:
+            kwargs.update({"trigger_ids": trigger_ids})
+        return self.api_call("workflows.featured.remove", params=kwargs)
+
+    def workflows_featured_set(
+        self,
+        *,
+        channel_id: str,
+        trigger_ids: Union[str, Sequence[str]],
+        **kwargs,
+    ) -> SlackResponse:
+        """Set featured workflows for a channel.
+        https://docs.slack.dev/reference/methods/workflows.featured.set
+        """
+        kwargs.update({"channel_id": channel_id})
+        if isinstance(trigger_ids, (list, tuple)):
+            kwargs.update({"trigger_ids": ",".join(trigger_ids)})
+        else:
+            kwargs.update({"trigger_ids": trigger_ids})
+        return self.api_call("workflows.featured.set", params=kwargs)
+
+    def workflows_stepCompleted(
+        self,
+        *,
+        workflow_step_execute_id: str,
+        outputs: Optional[dict] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Indicate a successful outcome of a workflow step's execution.
+        https://docs.slack.dev/reference/methods/workflows.stepCompleted
+        """
+        kwargs.update({"workflow_step_execute_id": workflow_step_execute_id})
+        if outputs is not None:
+            kwargs.update({"outputs": outputs})
+        kwargs = _remove_none_values(kwargs)
+        # NOTE: Intentionally using json for the "outputs" parameter
+        return self.api_call("workflows.stepCompleted", json=kwargs)
+
+    def workflows_stepFailed(
+        self,
+        *,
+        workflow_step_execute_id: str,
+        error: Dict[str, str],
+        **kwargs,
+    ) -> SlackResponse:
+        """Indicate an unsuccessful outcome of a workflow step's execution.
+        https://docs.slack.dev/reference/methods/workflows.stepFailed
+        """
+        kwargs.update(
+            {
+                "workflow_step_execute_id": workflow_step_execute_id,
+                "error": error,
+            }
+        )
+        kwargs = _remove_none_values(kwargs)
+        # NOTE: Intentionally using json for the "error" parameter
+        return self.api_call("workflows.stepFailed", json=kwargs)
+
+    def workflows_updateStep(
+        self,
+        *,
+        workflow_step_edit_id: str,
+        inputs: Optional[Dict[str, Any]] = None,
+        outputs: Optional[List[Dict[str, str]]] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Update the configuration for a workflow extension step.
+        https://docs.slack.dev/reference/methods/workflows.updateStep
+        """
+        kwargs.update({"workflow_step_edit_id": workflow_step_edit_id})
+        if inputs is not None:
+            kwargs.update({"inputs": inputs})
+        if outputs is not None:
+            kwargs.update({"outputs": outputs})
+        kwargs = _remove_none_values(kwargs)
+        # NOTE: Intentionally using json for the "inputs" / "outputs" parameters
+        return self.api_call("workflows.updateStep", json=kwargs)
+
+

A WebClient allows apps to communicate with the Slack Platform's Web API.

+

https://docs.slack.dev/reference/methods

+

The Slack Web API is an interface for querying information from +and enacting change in a Slack workspace.

+

This client handles constructing and sending HTTP requests to Slack +as well as parsing any responses received into a SlackResponse.

+

Attributes

+
+
token : str
+
A string specifying an xoxp-* or xoxb-* token.
+
base_url : str
+
A string representing the Slack API base URL. +Default is 'https://slack.com/api/'
+
timeout : int
+
The maximum number of seconds the client will wait +to connect and receive a response from Slack. +Default is 30 seconds.
+
ssl : SSLContext
+
An ssl.SSLContext instance, helpful for specifying +your own custom certificate chain.
+
proxy : str
+
String representing a fully-qualified URL to a proxy through +which to route all requests to the Slack API. Even if this parameter +is not specified, if any of the following environment variables are +present, they will be loaded into this parameter: HTTPS_PROXY, +https_proxy, HTTP_PROXY or http_proxy.
+
headers : dict
+
Additional request headers to attach to all requests.
+
+

Methods

+

api_call: Constructs a request and executes the API call to Slack.

+

Example of recommended usage:

+
    import os
+    from slack_sdk import WebClient
+
+    client = WebClient(token=os.environ['SLACK_API_TOKEN'])
+    response = client.chat_postMessage(
+        channel='#random',
+        text="Hello world!")
+    assert response["ok"]
+    assert response["message"]["text"] == "Hello world!"
+
+

Example manually creating an API request:

+
    import os
+    from slack_sdk import WebClient
+
+    client = WebClient(token=os.environ['SLACK_API_TOKEN'])
+    response = client.api_call(
+        api_method='chat.postMessage',
+        json={'channel': '#random','text': "Hello world!"}
+    )
+    assert response["ok"]
+    assert response["message"]["text"] == "Hello world!"
+
+

Note

+

Any attributes or methods prefixed with _underscores are +intended to be "private" internal use only. They may be changed or +removed at anytime.

+

Ancestors

+ +

Methods

+
+
+def admin_analytics_getFile(self,
*,
type: str,
date: str | None = None,
metadata_only: bool | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_analytics_getFile(
+    self,
+    *,
+    type: str,
+    date: Optional[str] = None,
+    metadata_only: Optional[bool] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Retrieve analytics data for a given date, presented as a compressed JSON file
+    https://docs.slack.dev/reference/methods/admin.analytics.getFile
+    """
+    kwargs.update({"type": type})
+    if date is not None:
+        kwargs.update({"date": date})
+    if metadata_only is not None:
+        kwargs.update({"metadata_only": metadata_only})
+    return self.api_call("admin.analytics.getFile", params=kwargs)
+
+

Retrieve analytics data for a given date, presented as a compressed JSON file +https://docs.slack.dev/reference/methods/admin.analytics.getFile

+
+
+def admin_apps_activities_list(self,
*,
app_id: str | None = None,
component_id: str | None = None,
component_type: str | None = None,
log_event_type: str | None = None,
max_date_created: int | None = None,
min_date_created: int | None = None,
min_log_level: str | None = None,
sort_direction: str | None = None,
source: str | None = None,
team_id: str | None = None,
trace_id: str | None = None,
cursor: str | None = None,
limit: int | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_apps_activities_list(
+    self,
+    *,
+    app_id: Optional[str] = None,
+    component_id: Optional[str] = None,
+    component_type: Optional[str] = None,
+    log_event_type: Optional[str] = None,
+    max_date_created: Optional[int] = None,
+    min_date_created: Optional[int] = None,
+    min_log_level: Optional[str] = None,
+    sort_direction: Optional[str] = None,
+    source: Optional[str] = None,
+    team_id: Optional[str] = None,
+    trace_id: Optional[str] = None,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Get logs for a specified team/org
+    https://docs.slack.dev/reference/methods/admin.apps.activities.list
+    """
+    kwargs.update(
+        {
+            "app_id": app_id,
+            "component_id": component_id,
+            "component_type": component_type,
+            "log_event_type": log_event_type,
+            "max_date_created": max_date_created,
+            "min_date_created": min_date_created,
+            "min_log_level": min_log_level,
+            "sort_direction": sort_direction,
+            "source": source,
+            "team_id": team_id,
+            "trace_id": trace_id,
+            "cursor": cursor,
+            "limit": limit,
+        }
+    )
+    return self.api_call("admin.apps.activities.list", params=kwargs)
+
+ +
+
+def admin_apps_approve(self,
*,
app_id: str | None = None,
request_id: str | None = None,
enterprise_id: str | None = None,
team_id: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_apps_approve(
+    self,
+    *,
+    app_id: Optional[str] = None,
+    request_id: Optional[str] = None,
+    enterprise_id: Optional[str] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Approve an app for installation on a workspace.
+    Either app_id or request_id is required.
+    These IDs can be obtained either directly via the app_requested event,
+    or by the admin.apps.requests.list method.
+    https://docs.slack.dev/reference/methods/admin.apps.approve
+    """
+    if app_id:
+        kwargs.update({"app_id": app_id})
+    elif request_id:
+        kwargs.update({"request_id": request_id})
+    else:
+        raise e.SlackRequestError("The app_id or request_id argument must be specified.")
+
+    kwargs.update(
+        {
+            "enterprise_id": enterprise_id,
+            "team_id": team_id,
+        }
+    )
+    return self.api_call("admin.apps.approve", params=kwargs)
+
+

Approve an app for installation on a workspace. +Either app_id or request_id is required. +These IDs can be obtained either directly via the app_requested event, +or by the admin.apps.requests.list method. +https://docs.slack.dev/reference/methods/admin.apps.approve

+
+
+def admin_apps_approved_list(self,
*,
cursor: str | None = None,
limit: int | None = None,
enterprise_id: str | None = None,
team_id: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_apps_approved_list(
+    self,
+    *,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    enterprise_id: Optional[str] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """List approved apps for an org or workspace.
+    https://docs.slack.dev/reference/methods/admin.apps.approved.list
+    """
+    kwargs.update(
+        {
+            "cursor": cursor,
+            "limit": limit,
+            "enterprise_id": enterprise_id,
+            "team_id": team_id,
+        }
+    )
+    return self.api_call("admin.apps.approved.list", http_verb="GET", params=kwargs)
+
+

List approved apps for an org or workspace. +https://docs.slack.dev/reference/methods/admin.apps.approved.list

+
+
+def admin_apps_clearResolution(self,
*,
app_id: str,
enterprise_id: str | None = None,
team_id: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_apps_clearResolution(
+    self,
+    *,
+    app_id: str,
+    enterprise_id: Optional[str] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Clear an app resolution
+    https://docs.slack.dev/reference/methods/admin.apps.clearResolution
+    """
+    kwargs.update(
+        {
+            "app_id": app_id,
+            "enterprise_id": enterprise_id,
+            "team_id": team_id,
+        }
+    )
+    return self.api_call("admin.apps.clearResolution", http_verb="POST", params=kwargs)
+
+ +
+
+def admin_apps_config_lookup(self, *, app_ids: str | Sequence[str], **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_apps_config_lookup(
+    self,
+    *,
+    app_ids: Union[str, Sequence[str]],
+    **kwargs,
+) -> SlackResponse:
+    """Look up the app config for connectors by their IDs
+    https://docs.slack.dev/reference/methods/admin.apps.config.lookup
+    """
+    if isinstance(app_ids, (list, tuple)):
+        kwargs.update({"app_ids": ",".join(app_ids)})
+    else:
+        kwargs.update({"app_ids": app_ids})
+    return self.api_call("admin.apps.config.lookup", params=kwargs)
+
+

Look up the app config for connectors by their IDs +https://docs.slack.dev/reference/methods/admin.apps.config.lookup

+
+
+def admin_apps_config_set(self,
*,
app_id: str,
domain_restrictions: Dict[str, Any] | None = None,
workflow_auth_strategy: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_apps_config_set(
+    self,
+    *,
+    app_id: str,
+    domain_restrictions: Optional[Dict[str, Any]] = None,
+    workflow_auth_strategy: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Set the app config for a connector
+    https://docs.slack.dev/reference/methods/admin.apps.config.set
+    """
+    kwargs.update(
+        {
+            "app_id": app_id,
+            "workflow_auth_strategy": workflow_auth_strategy,
+        }
+    )
+    if domain_restrictions is not None:
+        kwargs.update({"domain_restrictions": json.dumps(domain_restrictions)})
+    return self.api_call("admin.apps.config.set", params=kwargs)
+
+ +
+
+def admin_apps_requests_cancel(self,
*,
request_id: str,
enterprise_id: str | None = None,
team_id: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_apps_requests_cancel(
+    self,
+    *,
+    request_id: str,
+    enterprise_id: Optional[str] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """List app requests for a team/workspace.
+    https://docs.slack.dev/reference/methods/admin.apps.requests.cancel
+    """
+    kwargs.update(
+        {
+            "request_id": request_id,
+            "enterprise_id": enterprise_id,
+            "team_id": team_id,
+        }
+    )
+    return self.api_call("admin.apps.requests.cancel", http_verb="POST", params=kwargs)
+
+ +
+
+def admin_apps_requests_list(self,
*,
cursor: str | None = None,
limit: int | None = None,
team_id: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_apps_requests_list(
+    self,
+    *,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """List app requests for a team/workspace.
+    https://docs.slack.dev/reference/methods/admin.apps.requests.list
+    """
+    kwargs.update(
+        {
+            "cursor": cursor,
+            "limit": limit,
+            "team_id": team_id,
+        }
+    )
+    return self.api_call("admin.apps.requests.list", http_verb="GET", params=kwargs)
+
+ +
+
+def admin_apps_restrict(self,
*,
app_id: str | None = None,
request_id: str | None = None,
enterprise_id: str | None = None,
team_id: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_apps_restrict(
+    self,
+    *,
+    app_id: Optional[str] = None,
+    request_id: Optional[str] = None,
+    enterprise_id: Optional[str] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Restrict an app for installation on a workspace.
+    Exactly one of the team_id or enterprise_id arguments is required, not both.
+    Either app_id or request_id is required. These IDs can be obtained either directly
+    via the app_requested event, or by the admin.apps.requests.list method.
+    https://docs.slack.dev/reference/methods/admin.apps.restrict
+    """
+    if app_id:
+        kwargs.update({"app_id": app_id})
+    elif request_id:
+        kwargs.update({"request_id": request_id})
+    else:
+        raise e.SlackRequestError("The app_id or request_id argument must be specified.")
+
+    kwargs.update(
+        {
+            "enterprise_id": enterprise_id,
+            "team_id": team_id,
+        }
+    )
+    return self.api_call("admin.apps.restrict", params=kwargs)
+
+

Restrict an app for installation on a workspace. +Exactly one of the team_id or enterprise_id arguments is required, not both. +Either app_id or request_id is required. These IDs can be obtained either directly +via the app_requested event, or by the admin.apps.requests.list method. +https://docs.slack.dev/reference/methods/admin.apps.restrict

+
+
+def admin_apps_restricted_list(self,
*,
cursor: str | None = None,
limit: int | None = None,
enterprise_id: str | None = None,
team_id: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_apps_restricted_list(
+    self,
+    *,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    enterprise_id: Optional[str] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """List restricted apps for an org or workspace.
+    https://docs.slack.dev/reference/methods/admin.apps.restricted.list
+    """
+    kwargs.update(
+        {
+            "cursor": cursor,
+            "limit": limit,
+            "enterprise_id": enterprise_id,
+            "team_id": team_id,
+        }
+    )
+    return self.api_call("admin.apps.restricted.list", http_verb="GET", params=kwargs)
+
+

List restricted apps for an org or workspace. +https://docs.slack.dev/reference/methods/admin.apps.restricted.list

+
+
+def admin_apps_uninstall(self,
*,
app_id: str,
enterprise_id: str | None = None,
team_ids: str | Sequence[str] | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_apps_uninstall(
+    self,
+    *,
+    app_id: str,
+    enterprise_id: Optional[str] = None,
+    team_ids: Optional[Union[str, Sequence[str]]] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Uninstall an app from one or many workspaces, or an entire enterprise organization.
+    With an org-level token, enterprise_id or team_ids is required.
+    https://docs.slack.dev/reference/methods/admin.apps.uninstall
+    """
+    kwargs.update({"app_id": app_id})
+    if enterprise_id is not None:
+        kwargs.update({"enterprise_id": enterprise_id})
+    if team_ids is not None:
+        if isinstance(team_ids, (list, tuple)):
+            kwargs.update({"team_ids": ",".join(team_ids)})
+        else:
+            kwargs.update({"team_ids": team_ids})
+    return self.api_call("admin.apps.uninstall", http_verb="POST", params=kwargs)
+
+

Uninstall an app from one or many workspaces, or an entire enterprise organization. +With an org-level token, enterprise_id or team_ids is required. +https://docs.slack.dev/reference/methods/admin.apps.uninstall

+
+
+def admin_auth_policy_assignEntities(self,
*,
entity_ids: str | Sequence[str],
policy_name: str,
entity_type: str,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_auth_policy_assignEntities(
+    self,
+    *,
+    entity_ids: Union[str, Sequence[str]],
+    policy_name: str,
+    entity_type: str,
+    **kwargs,
+) -> SlackResponse:
+    """Assign entities to a particular authentication policy.
+    https://docs.slack.dev/reference/methods/admin.auth.policy.assignEntities
+    """
+    if isinstance(entity_ids, (list, tuple)):
+        kwargs.update({"entity_ids": ",".join(entity_ids)})
+    else:
+        kwargs.update({"entity_ids": entity_ids})
+    kwargs.update({"policy_name": policy_name})
+    kwargs.update({"entity_type": entity_type})
+    return self.api_call("admin.auth.policy.assignEntities", http_verb="POST", params=kwargs)
+
+

Assign entities to a particular authentication policy. +https://docs.slack.dev/reference/methods/admin.auth.policy.assignEntities

+
+
+def admin_auth_policy_getEntities(self,
*,
policy_name: str,
cursor: str | None = None,
entity_type: str | None = None,
limit: int | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_auth_policy_getEntities(
+    self,
+    *,
+    policy_name: str,
+    cursor: Optional[str] = None,
+    entity_type: Optional[str] = None,
+    limit: Optional[int] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Fetch all the entities assigned to a particular authentication policy by name.
+    https://docs.slack.dev/reference/methods/admin.auth.policy.getEntities
+    """
+    kwargs.update({"policy_name": policy_name})
+    if cursor is not None:
+        kwargs.update({"cursor": cursor})
+    if entity_type is not None:
+        kwargs.update({"entity_type": entity_type})
+    if limit is not None:
+        kwargs.update({"limit": limit})
+    return self.api_call("admin.auth.policy.getEntities", http_verb="POST", params=kwargs)
+
+

Fetch all the entities assigned to a particular authentication policy by name. +https://docs.slack.dev/reference/methods/admin.auth.policy.getEntities

+
+
+def admin_auth_policy_removeEntities(self,
*,
entity_ids: str | Sequence[str],
policy_name: str,
entity_type: str,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_auth_policy_removeEntities(
+    self,
+    *,
+    entity_ids: Union[str, Sequence[str]],
+    policy_name: str,
+    entity_type: str,
+    **kwargs,
+) -> SlackResponse:
+    """Remove specified entities from a specified authentication policy.
+    https://docs.slack.dev/reference/methods/admin.auth.policy.removeEntities
+    """
+    if isinstance(entity_ids, (list, tuple)):
+        kwargs.update({"entity_ids": ",".join(entity_ids)})
+    else:
+        kwargs.update({"entity_ids": entity_ids})
+    kwargs.update({"policy_name": policy_name})
+    kwargs.update({"entity_type": entity_type})
+    return self.api_call("admin.auth.policy.removeEntities", http_verb="POST", params=kwargs)
+
+

Remove specified entities from a specified authentication policy. +https://docs.slack.dev/reference/methods/admin.auth.policy.removeEntities

+
+
+def admin_barriers_create(self,
*,
barriered_from_usergroup_ids: str | Sequence[str],
primary_usergroup_id: str,
restricted_subjects: str | Sequence[str],
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_barriers_create(
+    self,
+    *,
+    barriered_from_usergroup_ids: Union[str, Sequence[str]],
+    primary_usergroup_id: str,
+    restricted_subjects: Union[str, Sequence[str]],
+    **kwargs,
+) -> SlackResponse:
+    """Create an Information Barrier
+    https://docs.slack.dev/reference/methods/admin.barriers.create
+    """
+    kwargs.update({"primary_usergroup_id": primary_usergroup_id})
+    if isinstance(barriered_from_usergroup_ids, (list, tuple)):
+        kwargs.update({"barriered_from_usergroup_ids": ",".join(barriered_from_usergroup_ids)})
+    else:
+        kwargs.update({"barriered_from_usergroup_ids": barriered_from_usergroup_ids})
+    if isinstance(restricted_subjects, (list, tuple)):
+        kwargs.update({"restricted_subjects": ",".join(restricted_subjects)})
+    else:
+        kwargs.update({"restricted_subjects": restricted_subjects})
+    return self.api_call("admin.barriers.create", http_verb="POST", params=kwargs)
+
+ +
+
+def admin_barriers_delete(self, *, barrier_id: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_barriers_delete(
+    self,
+    *,
+    barrier_id: str,
+    **kwargs,
+) -> SlackResponse:
+    """Delete an existing Information Barrier
+    https://docs.slack.dev/reference/methods/admin.barriers.delete
+    """
+    kwargs.update({"barrier_id": barrier_id})
+    return self.api_call("admin.barriers.delete", http_verb="POST", params=kwargs)
+
+ +
+
+def admin_barriers_list(self, *, cursor: str | None = None, limit: int | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_barriers_list(
+    self,
+    *,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Get all Information Barriers for your organization
+    https://docs.slack.dev/reference/methods/admin.barriers.list"""
+    kwargs.update(
+        {
+            "cursor": cursor,
+            "limit": limit,
+        }
+    )
+    return self.api_call("admin.barriers.list", http_verb="GET", params=kwargs)
+
+

Get all Information Barriers for your organization +https://docs.slack.dev/reference/methods/admin.barriers.list

+
+
+def admin_barriers_update(self,
*,
barrier_id: str,
barriered_from_usergroup_ids: str | Sequence[str],
primary_usergroup_id: str,
restricted_subjects: str | Sequence[str],
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_barriers_update(
+    self,
+    *,
+    barrier_id: str,
+    barriered_from_usergroup_ids: Union[str, Sequence[str]],
+    primary_usergroup_id: str,
+    restricted_subjects: Union[str, Sequence[str]],
+    **kwargs,
+) -> SlackResponse:
+    """Update an existing Information Barrier
+    https://docs.slack.dev/reference/methods/admin.barriers.update
+    """
+    kwargs.update({"barrier_id": barrier_id, "primary_usergroup_id": primary_usergroup_id})
+    if isinstance(barriered_from_usergroup_ids, (list, tuple)):
+        kwargs.update({"barriered_from_usergroup_ids": ",".join(barriered_from_usergroup_ids)})
+    else:
+        kwargs.update({"barriered_from_usergroup_ids": barriered_from_usergroup_ids})
+    if isinstance(restricted_subjects, (list, tuple)):
+        kwargs.update({"restricted_subjects": ",".join(restricted_subjects)})
+    else:
+        kwargs.update({"restricted_subjects": restricted_subjects})
+    return self.api_call("admin.barriers.update", http_verb="POST", params=kwargs)
+
+ +
+
+def admin_conversations_archive(self, *, channel_id: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_conversations_archive(
+    self,
+    *,
+    channel_id: str,
+    **kwargs,
+) -> SlackResponse:
+    """Archive a public or private channel.
+    https://docs.slack.dev/reference/methods/admin.conversations.archive
+    """
+    kwargs.update({"channel_id": channel_id})
+    return self.api_call("admin.conversations.archive", params=kwargs)
+
+ +
+
+def admin_conversations_bulkArchive(self, *, channel_ids: str | Sequence[str], **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_conversations_bulkArchive(
+    self,
+    *,
+    channel_ids: Union[Sequence[str], str],
+    **kwargs,
+) -> SlackResponse:
+    """Archive public or private channels in bulk.
+    https://docs.slack.dev/reference/methods/admin.conversations.bulkArchive
+    """
+    kwargs.update({"channel_ids": ",".join(channel_ids) if isinstance(channel_ids, (list, tuple)) else channel_ids})
+    return self.api_call("admin.conversations.bulkArchive", params=kwargs)
+
+ +
+
+def admin_conversations_bulkDelete(self, *, channel_ids: str | Sequence[str], **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_conversations_bulkDelete(
+    self,
+    *,
+    channel_ids: Union[Sequence[str], str],
+    **kwargs,
+) -> SlackResponse:
+    """Delete public or private channels in bulk.
+    https://slack.com/api/admin.conversations.bulkDelete
+    """
+    kwargs.update({"channel_ids": ",".join(channel_ids) if isinstance(channel_ids, (list, tuple)) else channel_ids})
+    return self.api_call("admin.conversations.bulkDelete", params=kwargs)
+
+

Delete public or private channels in bulk. +https://slack.com/api/admin.conversations.bulkDelete

+
+
+def admin_conversations_bulkMove(self, *, channel_ids: str | Sequence[str], target_team_id: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_conversations_bulkMove(
+    self,
+    *,
+    channel_ids: Union[Sequence[str], str],
+    target_team_id: str,
+    **kwargs,
+) -> SlackResponse:
+    """Move public or private channels in bulk.
+    https://docs.slack.dev/reference/methods/admin.conversations.bulkMove
+    """
+    kwargs.update(
+        {
+            "target_team_id": target_team_id,
+            "channel_ids": ",".join(channel_ids) if isinstance(channel_ids, (list, tuple)) else channel_ids,
+        }
+    )
+    return self.api_call("admin.conversations.bulkMove", params=kwargs)
+
+ +
+
+def admin_conversations_convertToPrivate(self, *, channel_id: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_conversations_convertToPrivate(
+    self,
+    *,
+    channel_id: str,
+    **kwargs,
+) -> SlackResponse:
+    """Convert a public channel to a private channel.
+    https://docs.slack.dev/reference/methods/admin.conversations.convertToPrivate
+    """
+    kwargs.update({"channel_id": channel_id})
+    return self.api_call("admin.conversations.convertToPrivate", params=kwargs)
+
+ +
+
+def admin_conversations_convertToPublic(self, *, channel_id: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_conversations_convertToPublic(
+    self,
+    *,
+    channel_id: str,
+    **kwargs,
+) -> SlackResponse:
+    """Convert a privte channel to a public channel.
+    https://docs.slack.dev/reference/methods/admin.conversations.convertToPublic
+    """
+    kwargs.update({"channel_id": channel_id})
+    return self.api_call("admin.conversations.convertToPublic", params=kwargs)
+
+ +
+
+def admin_conversations_create(self,
*,
is_private: bool,
name: str,
description: str | None = None,
org_wide: bool | None = None,
team_id: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_conversations_create(
+    self,
+    *,
+    is_private: bool,
+    name: str,
+    description: Optional[str] = None,
+    org_wide: Optional[bool] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Create a public or private channel-based conversation.
+    https://docs.slack.dev/reference/methods/admin.conversations.create
+    """
+    kwargs.update(
+        {
+            "is_private": is_private,
+            "name": name,
+            "description": description,
+            "org_wide": org_wide,
+            "team_id": team_id,
+        }
+    )
+    return self.api_call("admin.conversations.create", params=kwargs)
+
+

Create a public or private channel-based conversation. +https://docs.slack.dev/reference/methods/admin.conversations.create

+
+
+def admin_conversations_createForObjects(self,
*,
object_id: str,
salesforce_org_id: str,
invite_object_team: bool | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_conversations_createForObjects(
+    self,
+    *,
+    object_id: str,
+    salesforce_org_id: str,
+    invite_object_team: Optional[bool] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Create a Salesforce channel for the corresponding object provided.
+    https://docs.slack.dev/reference/methods/admin.conversations.createForObjects
+    """
+    kwargs.update(
+        {"object_id": object_id, "salesforce_org_id": salesforce_org_id, "invite_object_team": invite_object_team}
+    )
+    return self.api_call("admin.conversations.createForObjects", params=kwargs)
+
+

Create a Salesforce channel for the corresponding object provided. +https://docs.slack.dev/reference/methods/admin.conversations.createForObjects

+
+
+def admin_conversations_delete(self, *, channel_id: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_conversations_delete(
+    self,
+    *,
+    channel_id: str,
+    **kwargs,
+) -> SlackResponse:
+    """Delete a public or private channel.
+    https://docs.slack.dev/reference/methods/admin.conversations.delete
+    """
+    kwargs.update({"channel_id": channel_id})
+    return self.api_call("admin.conversations.delete", params=kwargs)
+
+ +
+
+def admin_conversations_disconnectShared(self,
*,
channel_id: str,
leaving_team_ids: str | Sequence[str] | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_conversations_disconnectShared(
+    self,
+    *,
+    channel_id: str,
+    leaving_team_ids: Optional[Union[str, Sequence[str]]] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Disconnect a connected channel from one or more workspaces.
+    https://docs.slack.dev/reference/methods/admin.conversations.disconnectShared
+    """
+    kwargs.update({"channel_id": channel_id})
+    if isinstance(leaving_team_ids, (list, tuple)):
+        kwargs.update({"leaving_team_ids": ",".join(leaving_team_ids)})
+    else:
+        kwargs.update({"leaving_team_ids": leaving_team_ids})
+    return self.api_call("admin.conversations.disconnectShared", params=kwargs)
+
+

Disconnect a connected channel from one or more workspaces. +https://docs.slack.dev/reference/methods/admin.conversations.disconnectShared

+
+
+def admin_conversations_ekm_listOriginalConnectedChannelInfo(self,
*,
channel_ids: str | Sequence[str] | None = None,
cursor: str | None = None,
limit: int | None = None,
team_ids: str | Sequence[str] | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_conversations_ekm_listOriginalConnectedChannelInfo(
+    self,
+    *,
+    channel_ids: Optional[Union[str, Sequence[str]]] = None,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    team_ids: Optional[Union[str, Sequence[str]]] = None,
+    **kwargs,
+) -> SlackResponse:
+    """List all disconnected channels—i.e.,
+    channels that were once connected to other workspaces and then disconnected—and
+    the corresponding original channel IDs for key revocation with EKM.
+    https://docs.slack.dev/reference/methods/admin.conversations.ekm.listOriginalConnectedChannelInfo
+    """
+    kwargs.update(
+        {
+            "cursor": cursor,
+            "limit": limit,
+        }
+    )
+    if isinstance(channel_ids, (list, tuple)):
+        kwargs.update({"channel_ids": ",".join(channel_ids)})
+    else:
+        kwargs.update({"channel_ids": channel_ids})
+    if isinstance(team_ids, (list, tuple)):
+        kwargs.update({"team_ids": ",".join(team_ids)})
+    else:
+        kwargs.update({"team_ids": team_ids})
+    return self.api_call("admin.conversations.ekm.listOriginalConnectedChannelInfo", params=kwargs)
+
+

List all disconnected channels—i.e., +channels that were once connected to other workspaces and then disconnected—and +the corresponding original channel IDs for key revocation with EKM. +https://docs.slack.dev/reference/methods/admin.conversations.ekm.listOriginalConnectedChannelInfo

+
+
+def admin_conversations_getConversationPrefs(self, *, channel_id: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_conversations_getConversationPrefs(
+    self,
+    *,
+    channel_id: str,
+    **kwargs,
+) -> SlackResponse:
+    """Get conversation preferences for a public or private channel.
+    https://docs.slack.dev/reference/methods/admin.conversations.getConversationPrefs
+    """
+    kwargs.update({"channel_id": channel_id})
+    return self.api_call("admin.conversations.getConversationPrefs", params=kwargs)
+
+

Get conversation preferences for a public or private channel. +https://docs.slack.dev/reference/methods/admin.conversations.getConversationPrefs

+
+
+def admin_conversations_getCustomRetention(self, *, channel_id: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_conversations_getCustomRetention(
+    self,
+    *,
+    channel_id: str,
+    **kwargs,
+) -> SlackResponse:
+    """Get a channel's retention policy
+    https://docs.slack.dev/reference/methods/admin.conversations.getCustomRetention
+    """
+    kwargs.update({"channel_id": channel_id})
+    return self.api_call("admin.conversations.getCustomRetention", params=kwargs)
+
+ +
+
+def admin_conversations_getTeams(self,
*,
channel_id: str,
cursor: str | None = None,
limit: int | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_conversations_getTeams(
+    self,
+    *,
+    channel_id: str,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Set the workspaces in an Enterprise grid org that connect to a channel.
+    https://docs.slack.dev/reference/methods/admin.conversations.getTeams
+    """
+    kwargs.update(
+        {
+            "channel_id": channel_id,
+            "cursor": cursor,
+            "limit": limit,
+        }
+    )
+    return self.api_call("admin.conversations.getTeams", params=kwargs)
+
+

Set the workspaces in an Enterprise grid org that connect to a channel. +https://docs.slack.dev/reference/methods/admin.conversations.getTeams

+
+
+def admin_conversations_invite(self, *, channel_id: str, user_ids: str | Sequence[str], **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_conversations_invite(
+    self,
+    *,
+    channel_id: str,
+    user_ids: Union[str, Sequence[str]],
+    **kwargs,
+) -> SlackResponse:
+    """Invite a user to a public or private channel.
+    https://docs.slack.dev/reference/methods/admin.conversations.invite
+    """
+    kwargs.update({"channel_id": channel_id})
+    if isinstance(user_ids, (list, tuple)):
+        kwargs.update({"user_ids": ",".join(user_ids)})
+    else:
+        kwargs.update({"user_ids": user_ids})
+    # NOTE: the endpoint is unable to handle Content-Type: application/json as of Sep 3, 2020.
+    return self.api_call("admin.conversations.invite", params=kwargs)
+
+

Invite a user to a public or private channel. +https://docs.slack.dev/reference/methods/admin.conversations.invite

+
+
+def admin_conversations_linkObjects(self, *, channel: str, record_id: str, salesforce_org_id: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_conversations_linkObjects(
+    self,
+    *,
+    channel: str,
+    record_id: str,
+    salesforce_org_id: str,
+    **kwargs,
+) -> SlackResponse:
+    """Link a Salesforce record to a channel.
+    https://docs.slack.dev/reference/methods/admin.conversations.linkObjects
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "record_id": record_id,
+            "salesforce_org_id": salesforce_org_id,
+        }
+    )
+    return self.api_call("admin.conversations.linkObjects", params=kwargs)
+
+ +
+
+def admin_conversations_lookup(self,
*,
last_message_activity_before: int,
team_ids: str | Sequence[str],
cursor: str | None = None,
limit: int | None = None,
max_member_count: int | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_conversations_lookup(
+    self,
+    *,
+    last_message_activity_before: int,
+    team_ids: Union[str, Sequence[str]],
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    max_member_count: Optional[int] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Returns channels on the given team using the filters.
+    https://docs.slack.dev/reference/methods/admin.conversations.lookup
+    """
+    kwargs.update(
+        {
+            "last_message_activity_before": last_message_activity_before,
+            "cursor": cursor,
+            "limit": limit,
+            "max_member_count": max_member_count,
+        }
+    )
+    if isinstance(team_ids, (list, tuple)):
+        kwargs.update({"team_ids": ",".join(team_ids)})
+    else:
+        kwargs.update({"team_ids": team_ids})
+    return self.api_call("admin.conversations.lookup", params=kwargs)
+
+

Returns channels on the given team using the filters. +https://docs.slack.dev/reference/methods/admin.conversations.lookup

+
+
+def admin_conversations_removeCustomRetention(self, *, channel_id: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_conversations_removeCustomRetention(
+    self,
+    *,
+    channel_id: str,
+    **kwargs,
+) -> SlackResponse:
+    """Remove a channel's retention policy
+    https://docs.slack.dev/reference/methods/admin.conversations.removeCustomRetention
+    """
+    kwargs.update({"channel_id": channel_id})
+    return self.api_call("admin.conversations.removeCustomRetention", params=kwargs)
+
+ +
+
+def admin_conversations_rename(self, *, channel_id: str, name: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_conversations_rename(
+    self,
+    *,
+    channel_id: str,
+    name: str,
+    **kwargs,
+) -> SlackResponse:
+    """Rename a public or private channel.
+    https://docs.slack.dev/reference/methods/admin.conversations.rename
+    """
+    kwargs.update({"channel_id": channel_id, "name": name})
+    return self.api_call("admin.conversations.rename", params=kwargs)
+
+ +
+
+def admin_conversations_restrictAccess_addGroup(self, *, channel_id: str, group_id: str, team_id: str | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_conversations_restrictAccess_addGroup(
+    self,
+    *,
+    channel_id: str,
+    group_id: str,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Add an allowlist of IDP groups for accessing a channel.
+    https://docs.slack.dev/reference/methods/admin.conversations.restrictAccess.addGroup
+    """
+    kwargs.update(
+        {
+            "channel_id": channel_id,
+            "group_id": group_id,
+            "team_id": team_id,
+        }
+    )
+    return self.api_call(
+        "admin.conversations.restrictAccess.addGroup",
+        http_verb="GET",
+        params=kwargs,
+    )
+
+

Add an allowlist of IDP groups for accessing a channel. +https://docs.slack.dev/reference/methods/admin.conversations.restrictAccess.addGroup

+
+
+def admin_conversations_restrictAccess_listGroups(self, *, channel_id: str, team_id: str | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_conversations_restrictAccess_listGroups(
+    self,
+    *,
+    channel_id: str,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """List all IDP Groups linked to a channel.
+    https://docs.slack.dev/reference/methods/admin.conversations.restrictAccess.listGroups
+    """
+    kwargs.update(
+        {
+            "channel_id": channel_id,
+            "team_id": team_id,
+        }
+    )
+    return self.api_call(
+        "admin.conversations.restrictAccess.listGroups",
+        http_verb="GET",
+        params=kwargs,
+    )
+
+ +
+
+def admin_conversations_restrictAccess_removeGroup(self, *, channel_id: str, group_id: str, team_id: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_conversations_restrictAccess_removeGroup(
+    self,
+    *,
+    channel_id: str,
+    group_id: str,
+    team_id: str,
+    **kwargs,
+) -> SlackResponse:
+    """Remove a linked IDP group linked from a private channel.
+    https://docs.slack.dev/reference/methods/admin.conversations.restrictAccess.removeGroup
+    """
+    kwargs.update(
+        {
+            "channel_id": channel_id,
+            "group_id": group_id,
+            "team_id": team_id,
+        }
+    )
+    return self.api_call(
+        "admin.conversations.restrictAccess.removeGroup",
+        http_verb="GET",
+        params=kwargs,
+    )
+
+

Remove a linked IDP group linked from a private channel. +https://docs.slack.dev/reference/methods/admin.conversations.restrictAccess.removeGroup

+
+ +
+
+ +Expand source code + +
def admin_conversations_search(
+    self,
+    *,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    query: Optional[str] = None,
+    search_channel_types: Optional[Union[str, Sequence[str]]] = None,
+    sort: Optional[str] = None,
+    sort_dir: Optional[str] = None,
+    team_ids: Optional[Union[str, Sequence[str]]] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Search for public or private channels in an Enterprise organization.
+    https://docs.slack.dev/reference/methods/admin.conversations.search
+    """
+    kwargs.update(
+        {
+            "cursor": cursor,
+            "limit": limit,
+            "query": query,
+            "sort": sort,
+            "sort_dir": sort_dir,
+        }
+    )
+
+    if isinstance(search_channel_types, (list, tuple)):
+        kwargs.update({"search_channel_types": ",".join(search_channel_types)})
+    else:
+        kwargs.update({"search_channel_types": search_channel_types})
+
+    if isinstance(team_ids, (list, tuple)):
+        kwargs.update({"team_ids": ",".join(team_ids)})
+    else:
+        kwargs.update({"team_ids": team_ids})
+
+    return self.api_call("admin.conversations.search", params=kwargs)
+
+

Search for public or private channels in an Enterprise organization. +https://docs.slack.dev/reference/methods/admin.conversations.search

+
+
+def admin_conversations_setConversationPrefs(self, *, channel_id: str, prefs: str | Dict[str, str], **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_conversations_setConversationPrefs(
+    self,
+    *,
+    channel_id: str,
+    prefs: Union[str, Dict[str, str]],
+    **kwargs,
+) -> SlackResponse:
+    """Set the posting permissions for a public or private channel.
+    https://docs.slack.dev/reference/methods/admin.conversations.setConversationPrefs
+    """
+    kwargs.update({"channel_id": channel_id})
+    if isinstance(prefs, dict):
+        kwargs.update({"prefs": json.dumps(prefs)})
+    else:
+        kwargs.update({"prefs": prefs})
+    return self.api_call("admin.conversations.setConversationPrefs", params=kwargs)
+
+

Set the posting permissions for a public or private channel. +https://docs.slack.dev/reference/methods/admin.conversations.setConversationPrefs

+
+
+def admin_conversations_setCustomRetention(self, *, channel_id: str, duration_days: int, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_conversations_setCustomRetention(
+    self,
+    *,
+    channel_id: str,
+    duration_days: int,
+    **kwargs,
+) -> SlackResponse:
+    """Set a channel's retention policy
+    https://docs.slack.dev/reference/methods/admin.conversations.setCustomRetention
+    """
+    kwargs.update({"channel_id": channel_id, "duration_days": duration_days})
+    return self.api_call("admin.conversations.setCustomRetention", params=kwargs)
+
+ +
+
+def admin_conversations_setTeams(self,
*,
channel_id: str,
org_channel: bool | None = None,
target_team_ids: str | Sequence[str] | None = None,
team_id: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_conversations_setTeams(
+    self,
+    *,
+    channel_id: str,
+    org_channel: Optional[bool] = None,
+    target_team_ids: Optional[Union[str, Sequence[str]]] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Set the workspaces in an Enterprise grid org that connect to a public or private channel.
+    https://docs.slack.dev/reference/methods/admin.conversations.setTeams
+    """
+    kwargs.update(
+        {
+            "channel_id": channel_id,
+            "org_channel": org_channel,
+            "team_id": team_id,
+        }
+    )
+    if isinstance(target_team_ids, (list, tuple)):
+        kwargs.update({"target_team_ids": ",".join(target_team_ids)})
+    else:
+        kwargs.update({"target_team_ids": target_team_ids})
+    return self.api_call("admin.conversations.setTeams", params=kwargs)
+
+

Set the workspaces in an Enterprise grid org that connect to a public or private channel. +https://docs.slack.dev/reference/methods/admin.conversations.setTeams

+
+
+def admin_conversations_unarchive(self, *, channel_id: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_conversations_unarchive(
+    self,
+    *,
+    channel_id: str,
+    **kwargs,
+) -> SlackResponse:
+    """Unarchive a public or private channel.
+    https://docs.slack.dev/reference/methods/admin.conversations.archive
+    """
+    kwargs.update({"channel_id": channel_id})
+    return self.api_call("admin.conversations.unarchive", params=kwargs)
+
+ +
+
+def admin_conversations_unlinkObjects(self, *, channel: str, new_name: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_conversations_unlinkObjects(
+    self,
+    *,
+    channel: str,
+    new_name: str,
+    **kwargs,
+) -> SlackResponse:
+    """Unlink a Salesforce record from a channel.
+    https://docs.slack.dev/reference/methods/admin.conversations.unlinkObjects
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "new_name": new_name,
+        }
+    )
+    return self.api_call("admin.conversations.unlinkObjects", params=kwargs)
+
+ +
+
+def admin_emoji_add(self, *, name: str, url: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_emoji_add(
+    self,
+    *,
+    name: str,
+    url: str,
+    **kwargs,
+) -> SlackResponse:
+    """Add an emoji.
+    https://docs.slack.dev/reference/methods/admin.emoji.add
+    """
+    kwargs.update({"name": name, "url": url})
+    return self.api_call("admin.emoji.add", http_verb="GET", params=kwargs)
+
+ +
+
+def admin_emoji_addAlias(self, *, alias_for: str, name: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_emoji_addAlias(
+    self,
+    *,
+    alias_for: str,
+    name: str,
+    **kwargs,
+) -> SlackResponse:
+    """Add an emoji alias.
+    https://docs.slack.dev/reference/methods/admin.emoji.addAlias
+    """
+    kwargs.update({"alias_for": alias_for, "name": name})
+    return self.api_call("admin.emoji.addAlias", http_verb="GET", params=kwargs)
+
+ +
+
+def admin_emoji_list(self, *, cursor: str | None = None, limit: int | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_emoji_list(
+    self,
+    *,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    **kwargs,
+) -> SlackResponse:
+    """List emoji for an Enterprise Grid organization.
+    https://docs.slack.dev/reference/methods/admin.emoji.list
+    """
+    kwargs.update({"cursor": cursor, "limit": limit})
+    return self.api_call("admin.emoji.list", http_verb="GET", params=kwargs)
+
+

List emoji for an Enterprise Grid organization. +https://docs.slack.dev/reference/methods/admin.emoji.list

+
+
+def admin_emoji_remove(self, *, name: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_emoji_remove(
+    self,
+    *,
+    name: str,
+    **kwargs,
+) -> SlackResponse:
+    """Remove an emoji across an Enterprise Grid organization.
+    https://docs.slack.dev/reference/methods/admin.emoji.remove
+    """
+    kwargs.update({"name": name})
+    return self.api_call("admin.emoji.remove", http_verb="GET", params=kwargs)
+
+

Remove an emoji across an Enterprise Grid organization. +https://docs.slack.dev/reference/methods/admin.emoji.remove

+
+
+def admin_emoji_rename(self, *, name: str, new_name: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_emoji_rename(
+    self,
+    *,
+    name: str,
+    new_name: str,
+    **kwargs,
+) -> SlackResponse:
+    """Rename an emoji.
+    https://docs.slack.dev/reference/methods/admin.emoji.rename
+    """
+    kwargs.update({"name": name, "new_name": new_name})
+    return self.api_call("admin.emoji.rename", http_verb="GET", params=kwargs)
+
+ +
+
+def admin_functions_list(self,
*,
app_ids: str | Sequence[str],
team_id: str | None = None,
cursor: str | None = None,
limit: int | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_functions_list(
+    self,
+    *,
+    app_ids: Union[str, Sequence[str]],
+    team_id: Optional[str] = None,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Look up functions by a set of apps
+    https://docs.slack.dev/reference/methods/admin.functions.list
+    """
+    if isinstance(app_ids, (list, tuple)):
+        kwargs.update({"app_ids": ",".join(app_ids)})
+    else:
+        kwargs.update({"app_ids": app_ids})
+    kwargs.update(
+        {
+            "team_id": team_id,
+            "cursor": cursor,
+            "limit": limit,
+        }
+    )
+    return self.api_call("admin.functions.list", params=kwargs)
+
+ +
+
+def admin_functions_permissions_lookup(self, *, function_ids: str | Sequence[str], **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_functions_permissions_lookup(
+    self,
+    *,
+    function_ids: Union[str, Sequence[str]],
+    **kwargs,
+) -> SlackResponse:
+    """Lookup the visibility of multiple Slack functions
+    and include the users if it is limited to particular named entities.
+    https://docs.slack.dev/reference/methods/admin.functions.permissions.lookup
+    """
+    if isinstance(function_ids, (list, tuple)):
+        kwargs.update({"function_ids": ",".join(function_ids)})
+    else:
+        kwargs.update({"function_ids": function_ids})
+    return self.api_call("admin.functions.permissions.lookup", params=kwargs)
+
+

Lookup the visibility of multiple Slack functions +and include the users if it is limited to particular named entities. +https://docs.slack.dev/reference/methods/admin.functions.permissions.lookup

+
+
+def admin_functions_permissions_set(self,
*,
function_id: str,
visibility: str,
user_ids: str | Sequence[str] | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_functions_permissions_set(
+    self,
+    *,
+    function_id: str,
+    visibility: str,
+    user_ids: Optional[Union[str, Sequence[str]]] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Set the visibility of a Slack function
+    and define the users or workspaces if it is set to named_entities
+    https://docs.slack.dev/reference/methods/admin.functions.permissions.set
+    """
+    kwargs.update(
+        {
+            "function_id": function_id,
+            "visibility": visibility,
+        }
+    )
+    if user_ids is not None:
+        if isinstance(user_ids, (list, tuple)):
+            kwargs.update({"user_ids": ",".join(user_ids)})
+        else:
+            kwargs.update({"user_ids": user_ids})
+    return self.api_call("admin.functions.permissions.set", params=kwargs)
+
+

Set the visibility of a Slack function +and define the users or workspaces if it is set to named_entities +https://docs.slack.dev/reference/methods/admin.functions.permissions.set

+
+
+def admin_inviteRequests_approve(self, *, invite_request_id: str, team_id: str | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_inviteRequests_approve(
+    self,
+    *,
+    invite_request_id: str,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Approve a workspace invite request.
+    https://docs.slack.dev/reference/methods/admin.inviteRequests.approve
+    """
+    kwargs.update({"invite_request_id": invite_request_id, "team_id": team_id})
+    return self.api_call("admin.inviteRequests.approve", params=kwargs)
+
+ +
+
+def admin_inviteRequests_approved_list(self,
*,
cursor: str | None = None,
limit: int | None = None,
team_id: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_inviteRequests_approved_list(
+    self,
+    *,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """List all approved workspace invite requests.
+    https://docs.slack.dev/reference/methods/admin.inviteRequests.approved.list
+    """
+    kwargs.update(
+        {
+            "cursor": cursor,
+            "limit": limit,
+            "team_id": team_id,
+        }
+    )
+    return self.api_call("admin.inviteRequests.approved.list", params=kwargs)
+
+ +
+
+def admin_inviteRequests_denied_list(self,
*,
cursor: str | None = None,
limit: int | None = None,
team_id: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_inviteRequests_denied_list(
+    self,
+    *,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """List all denied workspace invite requests.
+    https://docs.slack.dev/reference/methods/admin.inviteRequests.denied.list
+    """
+    kwargs.update(
+        {
+            "cursor": cursor,
+            "limit": limit,
+            "team_id": team_id,
+        }
+    )
+    return self.api_call("admin.inviteRequests.denied.list", params=kwargs)
+
+ +
+
+def admin_inviteRequests_deny(self, *, invite_request_id: str, team_id: str | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_inviteRequests_deny(
+    self,
+    *,
+    invite_request_id: str,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Deny a workspace invite request.
+    https://docs.slack.dev/reference/methods/admin.inviteRequests.deny
+    """
+    kwargs.update({"invite_request_id": invite_request_id, "team_id": team_id})
+    return self.api_call("admin.inviteRequests.deny", params=kwargs)
+
+ +
+
+def admin_inviteRequests_list(self, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_inviteRequests_list(
+    self,
+    **kwargs,
+) -> SlackResponse:
+    """List all pending workspace invite requests."""
+    return self.api_call("admin.inviteRequests.list", params=kwargs)
+
+

List all pending workspace invite requests.

+
+
+def admin_roles_addAssignments(self,
*,
role_id: str,
entity_ids: str | Sequence[str],
user_ids: str | Sequence[str],
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_roles_addAssignments(
+    self,
+    *,
+    role_id: str,
+    entity_ids: Union[str, Sequence[str]],
+    user_ids: Union[str, Sequence[str]],
+    **kwargs,
+) -> SlackResponse:
+    """Adds members to the specified role with the specified scopes
+    https://docs.slack.dev/reference/methods/admin.roles.addAssignments
+    """
+    kwargs.update({"role_id": role_id})
+    if isinstance(entity_ids, (list, tuple)):
+        kwargs.update({"entity_ids": ",".join(entity_ids)})
+    else:
+        kwargs.update({"entity_ids": entity_ids})
+    if isinstance(user_ids, (list, tuple)):
+        kwargs.update({"user_ids": ",".join(user_ids)})
+    else:
+        kwargs.update({"user_ids": user_ids})
+    return self.api_call("admin.roles.addAssignments", params=kwargs)
+
+

Adds members to the specified role with the specified scopes +https://docs.slack.dev/reference/methods/admin.roles.addAssignments

+
+
+def admin_roles_listAssignments(self,
*,
role_ids: str | Sequence[str] | None = None,
entity_ids: str | Sequence[str] | None = None,
cursor: str | None = None,
limit: str | int | None = None,
sort_dir: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_roles_listAssignments(
+    self,
+    *,
+    role_ids: Optional[Union[str, Sequence[str]]] = None,
+    entity_ids: Optional[Union[str, Sequence[str]]] = None,
+    cursor: Optional[str] = None,
+    limit: Optional[Union[str, int]] = None,
+    sort_dir: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Lists assignments for all roles across entities.
+        Options to scope results by any combination of roles or entities
+    https://docs.slack.dev/reference/methods/admin.roles.listAssignments
+    """
+    kwargs.update({"cursor": cursor, "limit": limit, "sort_dir": sort_dir})
+    if isinstance(entity_ids, (list, tuple)):
+        kwargs.update({"entity_ids": ",".join(entity_ids)})
+    else:
+        kwargs.update({"entity_ids": entity_ids})
+    if isinstance(role_ids, (list, tuple)):
+        kwargs.update({"role_ids": ",".join(role_ids)})
+    else:
+        kwargs.update({"role_ids": role_ids})
+    return self.api_call("admin.roles.listAssignments", params=kwargs)
+
+

Lists assignments for all roles across entities. +Options to scope results by any combination of roles or entities +https://docs.slack.dev/reference/methods/admin.roles.listAssignments

+
+
+def admin_roles_removeAssignments(self,
*,
role_id: str,
entity_ids: str | Sequence[str],
user_ids: str | Sequence[str],
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_roles_removeAssignments(
+    self,
+    *,
+    role_id: str,
+    entity_ids: Union[str, Sequence[str]],
+    user_ids: Union[str, Sequence[str]],
+    **kwargs,
+) -> SlackResponse:
+    """Removes a set of users from a role for the given scopes and entities
+    https://docs.slack.dev/reference/methods/admin.roles.removeAssignments
+    """
+    kwargs.update({"role_id": role_id})
+    if isinstance(entity_ids, (list, tuple)):
+        kwargs.update({"entity_ids": ",".join(entity_ids)})
+    else:
+        kwargs.update({"entity_ids": entity_ids})
+    if isinstance(user_ids, (list, tuple)):
+        kwargs.update({"user_ids": ",".join(user_ids)})
+    else:
+        kwargs.update({"user_ids": user_ids})
+    return self.api_call("admin.roles.removeAssignments", params=kwargs)
+
+

Removes a set of users from a role for the given scopes and entities +https://docs.slack.dev/reference/methods/admin.roles.removeAssignments

+
+
+def admin_teams_admins_list(self, *, team_id: str, cursor: str | None = None, limit: int | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_teams_admins_list(
+    self,
+    *,
+    team_id: str,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    **kwargs,
+) -> SlackResponse:
+    """List all of the admins on a given workspace.
+    https://docs.slack.dev/reference/methods/admin.inviteRequests.list
+    """
+    kwargs.update(
+        {
+            "cursor": cursor,
+            "limit": limit,
+            "team_id": team_id,
+        }
+    )
+    return self.api_call("admin.teams.admins.list", http_verb="GET", params=kwargs)
+
+

List all of the admins on a given workspace. +https://docs.slack.dev/reference/methods/admin.inviteRequests.list

+
+
+def admin_teams_create(self,
*,
team_domain: str,
team_name: str,
team_description: str | None = None,
team_discoverability: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_teams_create(
+    self,
+    *,
+    team_domain: str,
+    team_name: str,
+    team_description: Optional[str] = None,
+    team_discoverability: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Create an Enterprise team.
+    https://docs.slack.dev/reference/methods/admin.teams.create
+    """
+    kwargs.update(
+        {
+            "team_domain": team_domain,
+            "team_name": team_name,
+            "team_description": team_description,
+            "team_discoverability": team_discoverability,
+        }
+    )
+    return self.api_call("admin.teams.create", params=kwargs)
+
+ +
+
+def admin_teams_list(self, *, cursor: str | None = None, limit: int | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_teams_list(
+    self,
+    *,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    **kwargs,
+) -> SlackResponse:
+    """List all teams on an Enterprise organization.
+    https://docs.slack.dev/reference/methods/admin.teams.list
+    """
+    kwargs.update({"cursor": cursor, "limit": limit})
+    return self.api_call("admin.teams.list", params=kwargs)
+
+

List all teams on an Enterprise organization. +https://docs.slack.dev/reference/methods/admin.teams.list

+
+
+def admin_teams_owners_list(self, *, team_id: str, cursor: str | None = None, limit: int | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_teams_owners_list(
+    self,
+    *,
+    team_id: str,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    **kwargs,
+) -> SlackResponse:
+    """List all of the admins on a given workspace.
+    https://docs.slack.dev/reference/methods/admin.teams.owners.list
+    """
+    kwargs.update({"team_id": team_id, "cursor": cursor, "limit": limit})
+    return self.api_call("admin.teams.owners.list", http_verb="GET", params=kwargs)
+
+

List all of the admins on a given workspace. +https://docs.slack.dev/reference/methods/admin.teams.owners.list

+
+
+def admin_teams_settings_info(self, *, team_id: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_teams_settings_info(
+    self,
+    *,
+    team_id: str,
+    **kwargs,
+) -> SlackResponse:
+    """Fetch information about settings in a workspace
+    https://docs.slack.dev/reference/methods/admin.teams.settings.info
+    """
+    kwargs.update({"team_id": team_id})
+    return self.api_call("admin.teams.settings.info", params=kwargs)
+
+

Fetch information about settings in a workspace +https://docs.slack.dev/reference/methods/admin.teams.settings.info

+
+
+def admin_teams_settings_setDefaultChannels(self, *, team_id: str, channel_ids: str | Sequence[str], **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_teams_settings_setDefaultChannels(
+    self,
+    *,
+    team_id: str,
+    channel_ids: Union[str, Sequence[str]],
+    **kwargs,
+) -> SlackResponse:
+    """Set the default channels of a workspace.
+    https://docs.slack.dev/reference/methods/admin.teams.settings.setDefaultChannels
+    """
+    kwargs.update({"team_id": team_id})
+    if isinstance(channel_ids, (list, tuple)):
+        kwargs.update({"channel_ids": ",".join(channel_ids)})
+    else:
+        kwargs.update({"channel_ids": channel_ids})
+    return self.api_call("admin.teams.settings.setDefaultChannels", http_verb="GET", params=kwargs)
+
+ +
+
+def admin_teams_settings_setDescription(self, *, team_id: str, description: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_teams_settings_setDescription(
+    self,
+    *,
+    team_id: str,
+    description: str,
+    **kwargs,
+) -> SlackResponse:
+    """Set the description of a given workspace.
+    https://docs.slack.dev/reference/methods/admin.teams.settings.setDescription
+    """
+    kwargs.update({"team_id": team_id, "description": description})
+    return self.api_call("admin.teams.settings.setDescription", params=kwargs)
+
+ +
+
+def admin_teams_settings_setDiscoverability(self, *, team_id: str, discoverability: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_teams_settings_setDiscoverability(
+    self,
+    *,
+    team_id: str,
+    discoverability: str,
+    **kwargs,
+) -> SlackResponse:
+    """Sets the icon of a workspace.
+    https://docs.slack.dev/reference/methods/admin.teams.settings.setDiscoverability
+    """
+    kwargs.update({"team_id": team_id, "discoverability": discoverability})
+    return self.api_call("admin.teams.settings.setDiscoverability", params=kwargs)
+
+ +
+
+def admin_teams_settings_setIcon(self, *, team_id: str, image_url: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_teams_settings_setIcon(
+    self,
+    *,
+    team_id: str,
+    image_url: str,
+    **kwargs,
+) -> SlackResponse:
+    """Sets the icon of a workspace.
+    https://docs.slack.dev/reference/methods/admin.teams.settings.setIcon
+    """
+    kwargs.update({"team_id": team_id, "image_url": image_url})
+    return self.api_call("admin.teams.settings.setIcon", http_verb="GET", params=kwargs)
+
+ +
+
+def admin_teams_settings_setName(self, *, team_id: str, name: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_teams_settings_setName(
+    self,
+    *,
+    team_id: str,
+    name: str,
+    **kwargs,
+) -> SlackResponse:
+    """Sets the icon of a workspace.
+    https://docs.slack.dev/reference/methods/admin.teams.settings.setName
+    """
+    kwargs.update({"team_id": team_id, "name": name})
+    return self.api_call("admin.teams.settings.setName", params=kwargs)
+
+ +
+
+def admin_usergroups_addChannels(self,
*,
channel_ids: str | Sequence[str],
usergroup_id: str,
team_id: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_usergroups_addChannels(
+    self,
+    *,
+    channel_ids: Union[str, Sequence[str]],
+    usergroup_id: str,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Add one or more default channels to an IDP group.
+    https://docs.slack.dev/reference/methods/admin.usergroups.addChannels
+    """
+    kwargs.update({"team_id": team_id, "usergroup_id": usergroup_id})
+    if isinstance(channel_ids, (list, tuple)):
+        kwargs.update({"channel_ids": ",".join(channel_ids)})
+    else:
+        kwargs.update({"channel_ids": channel_ids})
+    return self.api_call("admin.usergroups.addChannels", params=kwargs)
+
+

Add one or more default channels to an IDP group. +https://docs.slack.dev/reference/methods/admin.usergroups.addChannels

+
+
+def admin_usergroups_addTeams(self,
*,
usergroup_id: str,
team_ids: str | Sequence[str],
auto_provision: bool | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_usergroups_addTeams(
+    self,
+    *,
+    usergroup_id: str,
+    team_ids: Union[str, Sequence[str]],
+    auto_provision: Optional[bool] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Associate one or more default workspaces with an organization-wide IDP group.
+    https://docs.slack.dev/reference/methods/admin.usergroups.addTeams
+    """
+    kwargs.update({"usergroup_id": usergroup_id, "auto_provision": auto_provision})
+    if isinstance(team_ids, (list, tuple)):
+        kwargs.update({"team_ids": ",".join(team_ids)})
+    else:
+        kwargs.update({"team_ids": team_ids})
+    return self.api_call("admin.usergroups.addTeams", params=kwargs)
+
+

Associate one or more default workspaces with an organization-wide IDP group. +https://docs.slack.dev/reference/methods/admin.usergroups.addTeams

+
+
+def admin_usergroups_listChannels(self,
*,
usergroup_id: str,
include_num_members: bool | None = None,
team_id: bool | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_usergroups_listChannels(
+    self,
+    *,
+    usergroup_id: str,
+    include_num_members: Optional[bool] = None,
+    team_id: Optional[bool] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Add one or more default channels to an IDP group.
+    https://docs.slack.dev/reference/methods/admin.usergroups.listChannels
+    """
+    kwargs.update(
+        {
+            "usergroup_id": usergroup_id,
+            "include_num_members": include_num_members,
+            "team_id": team_id,
+        }
+    )
+    return self.api_call("admin.usergroups.listChannels", params=kwargs)
+
+

Add one or more default channels to an IDP group. +https://docs.slack.dev/reference/methods/admin.usergroups.listChannels

+
+
+def admin_usergroups_removeChannels(self, *, usergroup_id: str, channel_ids: str | Sequence[str], **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_usergroups_removeChannels(
+    self,
+    *,
+    usergroup_id: str,
+    channel_ids: Union[str, Sequence[str]],
+    **kwargs,
+) -> SlackResponse:
+    """Add one or more default channels to an IDP group.
+    https://docs.slack.dev/reference/methods/admin.usergroups.removeChannels
+    """
+    kwargs.update({"usergroup_id": usergroup_id})
+    if isinstance(channel_ids, (list, tuple)):
+        kwargs.update({"channel_ids": ",".join(channel_ids)})
+    else:
+        kwargs.update({"channel_ids": channel_ids})
+    return self.api_call("admin.usergroups.removeChannels", params=kwargs)
+
+

Add one or more default channels to an IDP group. +https://docs.slack.dev/reference/methods/admin.usergroups.removeChannels

+
+
+def admin_users_assign(self,
*,
team_id: str,
user_id: str,
channel_ids: str | Sequence[str] | None = None,
is_restricted: bool | None = None,
is_ultra_restricted: bool | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_users_assign(
+    self,
+    *,
+    team_id: str,
+    user_id: str,
+    channel_ids: Optional[Union[str, Sequence[str]]] = None,
+    is_restricted: Optional[bool] = None,
+    is_ultra_restricted: Optional[bool] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Add an Enterprise user to a workspace.
+    https://docs.slack.dev/reference/methods/admin.users.assign
+    """
+    kwargs.update(
+        {
+            "team_id": team_id,
+            "user_id": user_id,
+            "is_restricted": is_restricted,
+            "is_ultra_restricted": is_ultra_restricted,
+        }
+    )
+    if isinstance(channel_ids, (list, tuple)):
+        kwargs.update({"channel_ids": ",".join(channel_ids)})
+    else:
+        kwargs.update({"channel_ids": channel_ids})
+    return self.api_call("admin.users.assign", params=kwargs)
+
+

Add an Enterprise user to a workspace. +https://docs.slack.dev/reference/methods/admin.users.assign

+
+
+def admin_users_invite(self,
*,
team_id: str,
email: str,
channel_ids: str | Sequence[str],
custom_message: str | None = None,
email_password_policy_enabled: bool | None = None,
guest_expiration_ts: str | float | None = None,
is_restricted: bool | None = None,
is_ultra_restricted: bool | None = None,
real_name: str | None = None,
resend: bool | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_users_invite(
+    self,
+    *,
+    team_id: str,
+    email: str,
+    channel_ids: Union[str, Sequence[str]],
+    custom_message: Optional[str] = None,
+    email_password_policy_enabled: Optional[bool] = None,
+    guest_expiration_ts: Optional[Union[str, float]] = None,
+    is_restricted: Optional[bool] = None,
+    is_ultra_restricted: Optional[bool] = None,
+    real_name: Optional[str] = None,
+    resend: Optional[bool] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Invite a user to a workspace.
+    https://docs.slack.dev/reference/methods/admin.users.invite
+    """
+    kwargs.update(
+        {
+            "team_id": team_id,
+            "email": email,
+            "custom_message": custom_message,
+            "email_password_policy_enabled": email_password_policy_enabled,
+            "guest_expiration_ts": str(guest_expiration_ts) if guest_expiration_ts is not None else None,
+            "is_restricted": is_restricted,
+            "is_ultra_restricted": is_ultra_restricted,
+            "real_name": real_name,
+            "resend": resend,
+        }
+    )
+    if isinstance(channel_ids, (list, tuple)):
+        kwargs.update({"channel_ids": ",".join(channel_ids)})
+    else:
+        kwargs.update({"channel_ids": channel_ids})
+    return self.api_call("admin.users.invite", params=kwargs)
+
+ +
+
+def admin_users_list(self,
*,
team_id: str | None = None,
include_deactivated_user_workspaces: bool | None = None,
is_active: bool | None = None,
cursor: str | None = None,
limit: int | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_users_list(
+    self,
+    *,
+    team_id: Optional[str] = None,
+    include_deactivated_user_workspaces: Optional[bool] = None,
+    is_active: Optional[bool] = None,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    **kwargs,
+) -> SlackResponse:
+    """List users on a workspace
+    https://docs.slack.dev/reference/methods/admin.users.list
+    """
+    kwargs.update(
+        {
+            "team_id": team_id,
+            "include_deactivated_user_workspaces": include_deactivated_user_workspaces,
+            "is_active": is_active,
+            "cursor": cursor,
+            "limit": limit,
+        }
+    )
+    return self.api_call("admin.users.list", params=kwargs)
+
+ +
+
+def admin_users_remove(self, *, team_id: str, user_id: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_users_remove(
+    self,
+    *,
+    team_id: str,
+    user_id: str,
+    **kwargs,
+) -> SlackResponse:
+    """Remove a user from a workspace.
+    https://docs.slack.dev/reference/methods/admin.users.remove
+    """
+    kwargs.update({"team_id": team_id, "user_id": user_id})
+    return self.api_call("admin.users.remove", params=kwargs)
+
+ +
+
+def admin_users_session_clearSettings(self, *, user_ids: str | Sequence[str], **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_users_session_clearSettings(
+    self,
+    *,
+    user_ids: Union[str, Sequence[str]],
+    **kwargs,
+) -> SlackResponse:
+    """Clear user-specific session settings—the session duration
+    and what happens when the client closes—for a list of users.
+    https://docs.slack.dev/reference/methods/admin.users.session.clearSettings
+    """
+    if isinstance(user_ids, (list, tuple)):
+        kwargs.update({"user_ids": ",".join(user_ids)})
+    else:
+        kwargs.update({"user_ids": user_ids})
+    return self.api_call("admin.users.session.clearSettings", params=kwargs)
+
+

Clear user-specific session settings—the session duration +and what happens when the client closes—for a list of users. +https://docs.slack.dev/reference/methods/admin.users.session.clearSettings

+
+
+def admin_users_session_getSettings(self, *, user_ids: str | Sequence[str], **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_users_session_getSettings(
+    self,
+    *,
+    user_ids: Union[str, Sequence[str]],
+    **kwargs,
+) -> SlackResponse:
+    """Get user-specific session settings—the session duration
+    and what happens when the client closes—given a list of users.
+    https://docs.slack.dev/reference/methods/admin.users.session.getSettings
+    """
+    if isinstance(user_ids, (list, tuple)):
+        kwargs.update({"user_ids": ",".join(user_ids)})
+    else:
+        kwargs.update({"user_ids": user_ids})
+    return self.api_call("admin.users.session.getSettings", params=kwargs)
+
+

Get user-specific session settings—the session duration +and what happens when the client closes—given a list of users. +https://docs.slack.dev/reference/methods/admin.users.session.getSettings

+
+
+def admin_users_session_invalidate(self, *, session_id: str, team_id: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_users_session_invalidate(
+    self,
+    *,
+    session_id: str,
+    team_id: str,
+    **kwargs,
+) -> SlackResponse:
+    """Invalidate a single session for a user by session_id.
+    https://docs.slack.dev/reference/methods/admin.users.session.invalidate
+    """
+    kwargs.update({"session_id": session_id, "team_id": team_id})
+    return self.api_call("admin.users.session.invalidate", params=kwargs)
+
+

Invalidate a single session for a user by session_id. +https://docs.slack.dev/reference/methods/admin.users.session.invalidate

+
+
+def admin_users_session_list(self,
*,
cursor: str | None = None,
limit: int | None = None,
team_id: str | None = None,
user_id: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_users_session_list(
+    self,
+    *,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    team_id: Optional[str] = None,
+    user_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Lists all active user sessions for an organization
+    https://docs.slack.dev/reference/methods/admin.users.session.list
+    """
+    kwargs.update(
+        {
+            "cursor": cursor,
+            "limit": limit,
+            "team_id": team_id,
+            "user_id": user_id,
+        }
+    )
+    return self.api_call("admin.users.session.list", params=kwargs)
+
+

Lists all active user sessions for an organization +https://docs.slack.dev/reference/methods/admin.users.session.list

+
+
+def admin_users_session_reset(self,
*,
user_id: str,
mobile_only: bool | None = None,
web_only: bool | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_users_session_reset(
+    self,
+    *,
+    user_id: str,
+    mobile_only: Optional[bool] = None,
+    web_only: Optional[bool] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Wipes all valid sessions on all devices for a given user.
+    https://docs.slack.dev/reference/methods/admin.users.session.reset
+    """
+    kwargs.update(
+        {
+            "user_id": user_id,
+            "mobile_only": mobile_only,
+            "web_only": web_only,
+        }
+    )
+    return self.api_call("admin.users.session.reset", params=kwargs)
+
+

Wipes all valid sessions on all devices for a given user. +https://docs.slack.dev/reference/methods/admin.users.session.reset

+
+
+def admin_users_session_resetBulk(self,
*,
user_ids: str | Sequence[str],
mobile_only: bool | None = None,
web_only: bool | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_users_session_resetBulk(
+    self,
+    *,
+    user_ids: Union[str, Sequence[str]],
+    mobile_only: Optional[bool] = None,
+    web_only: Optional[bool] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Enqueues an asynchronous job to wipe all valid sessions on all devices for a given list of users
+    https://docs.slack.dev/reference/methods/admin.users.session.resetBulk
+    """
+    if isinstance(user_ids, (list, tuple)):
+        kwargs.update({"user_ids": ",".join(user_ids)})
+    else:
+        kwargs.update({"user_ids": user_ids})
+    kwargs.update(
+        {
+            "mobile_only": mobile_only,
+            "web_only": web_only,
+        }
+    )
+    return self.api_call("admin.users.session.resetBulk", params=kwargs)
+
+

Enqueues an asynchronous job to wipe all valid sessions on all devices for a given list of users +https://docs.slack.dev/reference/methods/admin.users.session.resetBulk

+
+
+def admin_users_session_setSettings(self,
*,
user_ids: str | Sequence[str],
desktop_app_browser_quit: bool | None = None,
duration: int | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_users_session_setSettings(
+    self,
+    *,
+    user_ids: Union[str, Sequence[str]],
+    desktop_app_browser_quit: Optional[bool] = None,
+    duration: Optional[int] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Configure the user-level session settings—the session duration
+    and what happens when the client closes—for one or more users.
+    https://docs.slack.dev/reference/methods/admin.users.session.setSettings
+    """
+    if isinstance(user_ids, (list, tuple)):
+        kwargs.update({"user_ids": ",".join(user_ids)})
+    else:
+        kwargs.update({"user_ids": user_ids})
+    kwargs.update(
+        {
+            "desktop_app_browser_quit": desktop_app_browser_quit,
+            "duration": duration,
+        }
+    )
+    return self.api_call("admin.users.session.setSettings", params=kwargs)
+
+

Configure the user-level session settings—the session duration +and what happens when the client closes—for one or more users. +https://docs.slack.dev/reference/methods/admin.users.session.setSettings

+
+
+def admin_users_setAdmin(self, *, team_id: str, user_id: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_users_setAdmin(
+    self,
+    *,
+    team_id: str,
+    user_id: str,
+    **kwargs,
+) -> SlackResponse:
+    """Set an existing guest, regular user, or owner to be an admin user.
+    https://docs.slack.dev/reference/methods/admin.users.setAdmin
+    """
+    kwargs.update({"team_id": team_id, "user_id": user_id})
+    return self.api_call("admin.users.setAdmin", params=kwargs)
+
+

Set an existing guest, regular user, or owner to be an admin user. +https://docs.slack.dev/reference/methods/admin.users.setAdmin

+
+
+def admin_users_setExpiration(self, *, expiration_ts: int, user_id: str, team_id: str | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_users_setExpiration(
+    self,
+    *,
+    expiration_ts: int,
+    user_id: str,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Set an expiration for a guest user.
+    https://docs.slack.dev/reference/methods/admin.users.setExpiration
+    """
+    kwargs.update({"expiration_ts": expiration_ts, "team_id": team_id, "user_id": user_id})
+    return self.api_call("admin.users.setExpiration", params=kwargs)
+
+ +
+
+def admin_users_setOwner(self, *, team_id: str, user_id: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_users_setOwner(
+    self,
+    *,
+    team_id: str,
+    user_id: str,
+    **kwargs,
+) -> SlackResponse:
+    """Set an existing guest, regular user, or admin user to be a workspace owner.
+    https://docs.slack.dev/reference/methods/admin.users.setOwner
+    """
+    kwargs.update({"team_id": team_id, "user_id": user_id})
+    return self.api_call("admin.users.setOwner", params=kwargs)
+
+

Set an existing guest, regular user, or admin user to be a workspace owner. +https://docs.slack.dev/reference/methods/admin.users.setOwner

+
+
+def admin_users_setRegular(self, *, team_id: str, user_id: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_users_setRegular(
+    self,
+    *,
+    team_id: str,
+    user_id: str,
+    **kwargs,
+) -> SlackResponse:
+    """Set an existing guest user, admin user, or owner to be a regular user.
+    https://docs.slack.dev/reference/methods/admin.users.setRegular
+    """
+    kwargs.update({"team_id": team_id, "user_id": user_id})
+    return self.api_call("admin.users.setRegular", params=kwargs)
+
+

Set an existing guest user, admin user, or owner to be a regular user. +https://docs.slack.dev/reference/methods/admin.users.setRegular

+
+
+def admin_users_unsupportedVersions_export(self,
*,
date_end_of_support: str | int | None = None,
date_sessions_started: str | int | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_users_unsupportedVersions_export(
+    self,
+    *,
+    date_end_of_support: Optional[Union[str, int]] = None,
+    date_sessions_started: Optional[Union[str, int]] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Ask Slackbot to send you an export listing all workspace members using unsupported software,
+    presented as a zipped CSV file.
+    https://docs.slack.dev/reference/methods/admin.users.unsupportedVersions.export
+    """
+    kwargs.update(
+        {
+            "date_end_of_support": date_end_of_support,
+            "date_sessions_started": date_sessions_started,
+        }
+    )
+    return self.api_call("admin.users.unsupportedVersions.export", params=kwargs)
+
+

Ask Slackbot to send you an export listing all workspace members using unsupported software, +presented as a zipped CSV file. +https://docs.slack.dev/reference/methods/admin.users.unsupportedVersions.export

+
+
+def admin_workflows_collaborators_add(self,
*,
collaborator_ids: str | Sequence[str],
workflow_ids: str | Sequence[str],
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_workflows_collaborators_add(
+    self,
+    *,
+    collaborator_ids: Union[str, Sequence[str]],
+    workflow_ids: Union[str, Sequence[str]],
+    **kwargs,
+) -> SlackResponse:
+    """Add collaborators to workflows within the team or enterprise
+    https://docs.slack.dev/reference/methods/admin.workflows.collaborators.add
+    """
+    if isinstance(collaborator_ids, (list, tuple)):
+        kwargs.update({"collaborator_ids": ",".join(collaborator_ids)})
+    else:
+        kwargs.update({"collaborator_ids": collaborator_ids})
+    if isinstance(workflow_ids, (list, tuple)):
+        kwargs.update({"workflow_ids": ",".join(workflow_ids)})
+    else:
+        kwargs.update({"workflow_ids": workflow_ids})
+    return self.api_call("admin.workflows.collaborators.add", params=kwargs)
+
+

Add collaborators to workflows within the team or enterprise +https://docs.slack.dev/reference/methods/admin.workflows.collaborators.add

+
+
+def admin_workflows_collaborators_remove(self,
*,
collaborator_ids: str | Sequence[str],
workflow_ids: str | Sequence[str],
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_workflows_collaborators_remove(
+    self,
+    *,
+    collaborator_ids: Union[str, Sequence[str]],
+    workflow_ids: Union[str, Sequence[str]],
+    **kwargs,
+) -> SlackResponse:
+    """Remove collaborators from workflows within the team or enterprise
+    https://docs.slack.dev/reference/methods/admin.workflows.collaborators.remove
+    """
+    if isinstance(collaborator_ids, (list, tuple)):
+        kwargs.update({"collaborator_ids": ",".join(collaborator_ids)})
+    else:
+        kwargs.update({"collaborator_ids": collaborator_ids})
+    if isinstance(workflow_ids, (list, tuple)):
+        kwargs.update({"workflow_ids": ",".join(workflow_ids)})
+    else:
+        kwargs.update({"workflow_ids": workflow_ids})
+    return self.api_call("admin.workflows.collaborators.remove", params=kwargs)
+
+

Remove collaborators from workflows within the team or enterprise +https://docs.slack.dev/reference/methods/admin.workflows.collaborators.remove

+
+
+def admin_workflows_permissions_lookup(self,
*,
workflow_ids: str | Sequence[str],
max_workflow_triggers: int | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_workflows_permissions_lookup(
+    self,
+    *,
+    workflow_ids: Union[str, Sequence[str]],
+    max_workflow_triggers: Optional[int] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Look up the permissions for a set of workflows
+    https://docs.slack.dev/reference/methods/admin.workflows.permissions.lookup
+    """
+    if isinstance(workflow_ids, (list, tuple)):
+        kwargs.update({"workflow_ids": ",".join(workflow_ids)})
+    else:
+        kwargs.update({"workflow_ids": workflow_ids})
+    kwargs.update(
+        {
+            "max_workflow_triggers": max_workflow_triggers,
+        }
+    )
+    return self.api_call("admin.workflows.permissions.lookup", params=kwargs)
+
+ +
+ +
+
+ +Expand source code + +
def admin_workflows_search(
+    self,
+    *,
+    app_id: Optional[str] = None,
+    collaborator_ids: Optional[Union[str, Sequence[str]]] = None,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    no_collaborators: Optional[bool] = None,
+    num_trigger_ids: Optional[int] = None,
+    query: Optional[str] = None,
+    sort: Optional[str] = None,
+    sort_dir: Optional[str] = None,
+    source: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Search workflows within the team or enterprise
+    https://docs.slack.dev/reference/methods/admin.workflows.search
+    """
+    if collaborator_ids is not None:
+        if isinstance(collaborator_ids, (list, tuple)):
+            kwargs.update({"collaborator_ids": ",".join(collaborator_ids)})
+        else:
+            kwargs.update({"collaborator_ids": collaborator_ids})
+    kwargs.update(
+        {
+            "app_id": app_id,
+            "cursor": cursor,
+            "limit": limit,
+            "no_collaborators": no_collaborators,
+            "num_trigger_ids": num_trigger_ids,
+            "query": query,
+            "sort": sort,
+            "sort_dir": sort_dir,
+            "source": source,
+        }
+    )
+    return self.api_call("admin.workflows.search", params=kwargs)
+
+

Search workflows within the team or enterprise +https://docs.slack.dev/reference/methods/admin.workflows.search

+
+
+def admin_workflows_unpublish(self, *, workflow_ids: str | Sequence[str], **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_workflows_unpublish(
+    self,
+    *,
+    workflow_ids: Union[str, Sequence[str]],
+    **kwargs,
+) -> SlackResponse:
+    """Unpublish workflows within the team or enterprise
+    https://docs.slack.dev/reference/methods/admin.workflows.unpublish
+    """
+    if isinstance(workflow_ids, (list, tuple)):
+        kwargs.update({"workflow_ids": ",".join(workflow_ids)})
+    else:
+        kwargs.update({"workflow_ids": workflow_ids})
+    return self.api_call("admin.workflows.unpublish", params=kwargs)
+
+

Unpublish workflows within the team or enterprise +https://docs.slack.dev/reference/methods/admin.workflows.unpublish

+
+
+def api_test(self, *, error: str | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def api_test(
+    self,
+    *,
+    error: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Checks API calling code.
+    https://docs.slack.dev/reference/methods/api.test
+    """
+    kwargs.update({"error": error})
+    return self.api_call("api.test", params=kwargs)
+
+ +
+
+def apps_connections_open(self, *, app_token: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def apps_connections_open(
+    self,
+    *,
+    app_token: str,
+    **kwargs,
+) -> SlackResponse:
+    """Generate a temporary Socket Mode WebSocket URL that your app can connect to
+    in order to receive events and interactive payloads
+    https://docs.slack.dev/reference/methods/apps.connections.open
+    """
+    kwargs.update({"token": app_token})
+    return self.api_call("apps.connections.open", http_verb="POST", params=kwargs)
+
+

Generate a temporary Socket Mode WebSocket URL that your app can connect to +in order to receive events and interactive payloads +https://docs.slack.dev/reference/methods/apps.connections.open

+
+
+def apps_event_authorizations_list(self,
*,
event_context: str,
cursor: str | None = None,
limit: int | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def apps_event_authorizations_list(
+    self,
+    *,
+    event_context: str,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Get a list of authorizations for the given event context.
+    Each authorization represents an app installation that the event is visible to.
+    https://docs.slack.dev/reference/methods/apps.event.authorizations.list
+    """
+    kwargs.update({"event_context": event_context, "cursor": cursor, "limit": limit})
+    return self.api_call("apps.event.authorizations.list", params=kwargs)
+
+

Get a list of authorizations for the given event context. +Each authorization represents an app installation that the event is visible to. +https://docs.slack.dev/reference/methods/apps.event.authorizations.list

+
+
+def apps_manifest_create(self, *, manifest: str | Dict[str, Any], **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def apps_manifest_create(
+    self,
+    *,
+    manifest: Union[str, Dict[str, Any]],
+    **kwargs,
+) -> SlackResponse:
+    """Create an app from an app manifest
+    https://docs.slack.dev/reference/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 +
+
+
+ +Expand source code + +
def apps_manifest_delete(
+    self,
+    *,
+    app_id: str,
+    **kwargs,
+) -> SlackResponse:
+    """Permanently deletes an app created through app manifests
+    https://docs.slack.dev/reference/methods/apps.manifest.delete
+    """
+    kwargs.update({"app_id": app_id})
+    return self.api_call("apps.manifest.delete", params=kwargs)
+
+

Permanently deletes an app created through app manifests +https://docs.slack.dev/reference/methods/apps.manifest.delete

+
+
+def apps_manifest_export(self, *, app_id: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def apps_manifest_export(
+    self,
+    *,
+    app_id: str,
+    **kwargs,
+) -> SlackResponse:
+    """Export an app manifest from an existing app
+    https://docs.slack.dev/reference/methods/apps.manifest.export
+    """
+    kwargs.update({"app_id": app_id})
+    return self.api_call("apps.manifest.export", params=kwargs)
+
+

Export an app manifest from an existing app +https://docs.slack.dev/reference/methods/apps.manifest.export

+
+
+def apps_manifest_update(self, *, app_id: str, manifest: str | Dict[str, Any], **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def apps_manifest_update(
+    self,
+    *,
+    app_id: str,
+    manifest: Union[str, Dict[str, Any]],
+    **kwargs,
+) -> SlackResponse:
+    """Update an app from an app manifest
+    https://docs.slack.dev/reference/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: str | Dict[str, Any], app_id: str | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def apps_manifest_validate(
+    self,
+    *,
+    manifest: Union[str, Dict[str, Any]],
+    app_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Validate an app manifest
+    https://docs.slack.dev/reference/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 apps_uninstall(self, *, client_id: str, client_secret: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def apps_uninstall(
+    self,
+    *,
+    client_id: str,
+    client_secret: str,
+    **kwargs,
+) -> SlackResponse:
+    """Uninstalls your app from a workspace.
+    https://docs.slack.dev/reference/methods/apps.uninstall
+    """
+    kwargs.update({"client_id": client_id, "client_secret": client_secret})
+    return self.api_call("apps.uninstall", params=kwargs)
+
+

Uninstalls your app from a workspace. +https://docs.slack.dev/reference/methods/apps.uninstall

+
+
+def assistant_threads_setStatus(self,
*,
channel_id: str,
thread_ts: str,
status: str,
loading_messages: List[str] | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def assistant_threads_setStatus(
+    self,
+    *,
+    channel_id: str,
+    thread_ts: str,
+    status: str,
+    loading_messages: Optional[List[str]] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Set the status for an AI assistant thread.
+    https://docs.slack.dev/reference/methods/assistant.threads.setStatus
+    """
+    kwargs.update(
+        {"channel_id": channel_id, "thread_ts": thread_ts, "status": status, "loading_messages": loading_messages}
+    )
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("assistant.threads.setStatus", json=kwargs)
+
+ +
+
+def assistant_threads_setSuggestedPrompts(self,
*,
channel_id: str,
thread_ts: str,
title: str | None = None,
prompts: List[Dict[str, str]],
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def assistant_threads_setSuggestedPrompts(
+    self,
+    *,
+    channel_id: str,
+    thread_ts: str,
+    title: Optional[str] = None,
+    prompts: List[Dict[str, str]],
+    **kwargs,
+) -> SlackResponse:
+    """Set suggested prompts for the given assistant thread.
+    https://docs.slack.dev/reference/methods/assistant.threads.setSuggestedPrompts
+    """
+    kwargs.update({"channel_id": channel_id, "thread_ts": thread_ts, "prompts": prompts})
+    if title is not None:
+        kwargs.update({"title": title})
+    return self.api_call("assistant.threads.setSuggestedPrompts", json=kwargs)
+
+

Set suggested prompts for the given assistant thread. +https://docs.slack.dev/reference/methods/assistant.threads.setSuggestedPrompts

+
+
+def assistant_threads_setTitle(self, *, channel_id: str, thread_ts: str, title: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def assistant_threads_setTitle(
+    self,
+    *,
+    channel_id: str,
+    thread_ts: str,
+    title: str,
+    **kwargs,
+) -> SlackResponse:
+    """Set the title for the given assistant thread.
+    https://docs.slack.dev/reference/methods/assistant.threads.setTitle
+    """
+    kwargs.update({"channel_id": channel_id, "thread_ts": thread_ts, "title": title})
+    return self.api_call("assistant.threads.setTitle", params=kwargs)
+
+

Set the title for the given assistant thread. +https://docs.slack.dev/reference/methods/assistant.threads.setTitle

+
+
+def auth_revoke(self, *, test: bool | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def auth_revoke(
+    self,
+    *,
+    test: Optional[bool] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Revokes a token.
+    https://docs.slack.dev/reference/methods/auth.revoke
+    """
+    kwargs.update({"test": test})
+    return self.api_call("auth.revoke", http_verb="GET", params=kwargs)
+
+ +
+
+def auth_teams_list(self,
cursor: str | None = None,
limit: int | None = None,
include_icon: bool | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def auth_teams_list(
+    self,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    include_icon: Optional[bool] = None,
+    **kwargs,
+) -> SlackResponse:
+    """List the workspaces a token can access.
+    https://docs.slack.dev/reference/methods/auth.teams.list
+    """
+    kwargs.update({"cursor": cursor, "limit": limit, "include_icon": include_icon})
+    return self.api_call("auth.teams.list", params=kwargs)
+
+

List the workspaces a token can access. +https://docs.slack.dev/reference/methods/auth.teams.list

+
+
+def auth_test(self, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def auth_test(
+    self,
+    **kwargs,
+) -> SlackResponse:
+    """Checks authentication & identity.
+    https://docs.slack.dev/reference/methods/auth.test
+    """
+    return self.api_call("auth.test", params=kwargs)
+
+

Checks authentication & identity. +https://docs.slack.dev/reference/methods/auth.test

+
+
+def bookmarks_add(self,
*,
channel_id: str,
title: str,
type: str,
emoji: str | None = None,
entity_id: str | None = None,
link: str | None = None,
parent_id: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def bookmarks_add(
+    self,
+    *,
+    channel_id: str,
+    title: str,
+    type: str,
+    emoji: Optional[str] = None,
+    entity_id: Optional[str] = None,
+    link: Optional[str] = None,  # include when type is 'link'
+    parent_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Add bookmark to a channel.
+    https://docs.slack.dev/reference/methods/bookmarks.add
+    """
+    kwargs.update(
+        {
+            "channel_id": channel_id,
+            "title": title,
+            "type": type,
+            "emoji": emoji,
+            "entity_id": entity_id,
+            "link": link,
+            "parent_id": parent_id,
+        }
+    )
+    return self.api_call("bookmarks.add", http_verb="POST", params=kwargs)
+
+ +
+
+def bookmarks_edit(self,
*,
bookmark_id: str,
channel_id: str,
emoji: str | None = None,
link: str | None = None,
title: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def bookmarks_edit(
+    self,
+    *,
+    bookmark_id: str,
+    channel_id: str,
+    emoji: Optional[str] = None,
+    link: Optional[str] = None,
+    title: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Edit bookmark.
+    https://docs.slack.dev/reference/methods/bookmarks.edit
+    """
+    kwargs.update(
+        {
+            "bookmark_id": bookmark_id,
+            "channel_id": channel_id,
+            "emoji": emoji,
+            "link": link,
+            "title": title,
+        }
+    )
+    return self.api_call("bookmarks.edit", http_verb="POST", params=kwargs)
+
+ +
+
+def bookmarks_list(self, *, channel_id: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def bookmarks_list(
+    self,
+    *,
+    channel_id: str,
+    **kwargs,
+) -> SlackResponse:
+    """List bookmark for the channel.
+    https://docs.slack.dev/reference/methods/bookmarks.list
+    """
+    kwargs.update({"channel_id": channel_id})
+    return self.api_call("bookmarks.list", http_verb="POST", params=kwargs)
+
+ +
+
+def bookmarks_remove(self, *, bookmark_id: str, channel_id: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def bookmarks_remove(
+    self,
+    *,
+    bookmark_id: str,
+    channel_id: str,
+    **kwargs,
+) -> SlackResponse:
+    """Remove bookmark from the channel.
+    https://docs.slack.dev/reference/methods/bookmarks.remove
+    """
+    kwargs.update({"bookmark_id": bookmark_id, "channel_id": channel_id})
+    return self.api_call("bookmarks.remove", http_verb="POST", params=kwargs)
+
+ +
+
+def bots_info(self, *, bot: str | None = None, team_id: str | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def bots_info(
+    self,
+    *,
+    bot: Optional[str] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Gets information about a bot user.
+    https://docs.slack.dev/reference/methods/bots.info
+    """
+    kwargs.update({"bot": bot, "team_id": team_id})
+    return self.api_call("bots.info", http_verb="GET", params=kwargs)
+
+

Gets information about a bot user. +https://docs.slack.dev/reference/methods/bots.info

+
+
+def calls_add(self,
*,
external_unique_id: str,
join_url: str,
created_by: str | None = None,
date_start: int | None = None,
desktop_app_join_url: str | None = None,
external_display_id: str | None = None,
title: str | None = None,
users: str | Sequence[Dict[str, str]] | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def calls_add(
+    self,
+    *,
+    external_unique_id: str,
+    join_url: str,
+    created_by: Optional[str] = None,
+    date_start: Optional[int] = None,
+    desktop_app_join_url: Optional[str] = None,
+    external_display_id: Optional[str] = None,
+    title: Optional[str] = None,
+    users: Optional[Union[str, Sequence[Dict[str, str]]]] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Registers a new Call.
+    https://docs.slack.dev/reference/methods/calls.add
+    """
+    kwargs.update(
+        {
+            "external_unique_id": external_unique_id,
+            "join_url": join_url,
+            "created_by": created_by,
+            "date_start": date_start,
+            "desktop_app_join_url": desktop_app_join_url,
+            "external_display_id": external_display_id,
+            "title": title,
+        }
+    )
+    _update_call_participants(
+        kwargs,
+        users if users is not None else kwargs.get("users"),  # type: ignore[arg-type]
+    )
+    return self.api_call("calls.add", http_verb="POST", params=kwargs)
+
+ +
+
+def calls_end(self, *, id: str, duration: int | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def calls_end(
+    self,
+    *,
+    id: str,
+    duration: Optional[int] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Ends a Call.
+    https://docs.slack.dev/reference/methods/calls.end
+    """
+    kwargs.update({"id": id, "duration": duration})
+    return self.api_call("calls.end", http_verb="POST", params=kwargs)
+
+ +
+
+def calls_info(self, *, id: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def calls_info(
+    self,
+    *,
+    id: str,
+    **kwargs,
+) -> SlackResponse:
+    """Returns information about a Call.
+    https://docs.slack.dev/reference/methods/calls.info
+    """
+    kwargs.update({"id": id})
+    return self.api_call("calls.info", http_verb="POST", params=kwargs)
+
+

Returns information about a Call. +https://docs.slack.dev/reference/methods/calls.info

+
+
+def calls_participants_add(self, *, id: str, users: str | Sequence[Dict[str, str]], **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def calls_participants_add(
+    self,
+    *,
+    id: str,
+    users: Union[str, Sequence[Dict[str, str]]],
+    **kwargs,
+) -> SlackResponse:
+    """Registers new participants added to a Call.
+    https://docs.slack.dev/reference/methods/calls.participants.add
+    """
+    kwargs.update({"id": id})
+    _update_call_participants(kwargs, users)
+    return self.api_call("calls.participants.add", http_verb="POST", params=kwargs)
+
+

Registers new participants added to a Call. +https://docs.slack.dev/reference/methods/calls.participants.add

+
+
+def calls_participants_remove(self, *, id: str, users: str | Sequence[Dict[str, str]], **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def calls_participants_remove(
+    self,
+    *,
+    id: str,
+    users: Union[str, Sequence[Dict[str, str]]],
+    **kwargs,
+) -> SlackResponse:
+    """Registers participants removed from a Call.
+    https://docs.slack.dev/reference/methods/calls.participants.remove
+    """
+    kwargs.update({"id": id})
+    _update_call_participants(kwargs, users)
+    return self.api_call("calls.participants.remove", http_verb="POST", params=kwargs)
+
+

Registers participants removed from a Call. +https://docs.slack.dev/reference/methods/calls.participants.remove

+
+
+def calls_update(self,
*,
id: str,
desktop_app_join_url: str | None = None,
join_url: str | None = None,
title: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def calls_update(
+    self,
+    *,
+    id: str,
+    desktop_app_join_url: Optional[str] = None,
+    join_url: Optional[str] = None,
+    title: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Updates information about a Call.
+    https://docs.slack.dev/reference/methods/calls.update
+    """
+    kwargs.update(
+        {
+            "id": id,
+            "desktop_app_join_url": desktop_app_join_url,
+            "join_url": join_url,
+            "title": title,
+        }
+    )
+    return self.api_call("calls.update", http_verb="POST", params=kwargs)
+
+ +
+
+def canvases_access_delete(self,
*,
canvas_id: str,
channel_ids: str | Sequence[str] | None = None,
user_ids: str | Sequence[str] | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def canvases_access_delete(
+    self,
+    *,
+    canvas_id: str,
+    channel_ids: Optional[Union[Sequence[str], str]] = None,
+    user_ids: Optional[Union[Sequence[str], str]] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Create a Channel Canvas for a channel
+    https://docs.slack.dev/reference/methods/canvases.access.delete
+    """
+    kwargs.update({"canvas_id": canvas_id})
+    if channel_ids is not None:
+        if isinstance(channel_ids, (list, tuple)):
+            kwargs.update({"channel_ids": ",".join(channel_ids)})
+        else:
+            kwargs.update({"channel_ids": channel_ids})
+    if user_ids is not None:
+        if isinstance(user_ids, (list, tuple)):
+            kwargs.update({"user_ids": ",".join(user_ids)})
+        else:
+            kwargs.update({"user_ids": user_ids})
+    return self.api_call("canvases.access.delete", params=kwargs)
+
+ +
+
+def canvases_access_set(self,
*,
canvas_id: str,
access_level: str,
channel_ids: str | Sequence[str] | None = None,
user_ids: str | Sequence[str] | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def canvases_access_set(
+    self,
+    *,
+    canvas_id: str,
+    access_level: str,
+    channel_ids: Optional[Union[Sequence[str], str]] = None,
+    user_ids: Optional[Union[Sequence[str], str]] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Sets the access level to a canvas for specified entities
+    https://docs.slack.dev/reference/methods/canvases.access.set
+    """
+    kwargs.update({"canvas_id": canvas_id, "access_level": access_level})
+    if channel_ids is not None:
+        if isinstance(channel_ids, (list, tuple)):
+            kwargs.update({"channel_ids": ",".join(channel_ids)})
+        else:
+            kwargs.update({"channel_ids": channel_ids})
+    if user_ids is not None:
+        if isinstance(user_ids, (list, tuple)):
+            kwargs.update({"user_ids": ",".join(user_ids)})
+        else:
+            kwargs.update({"user_ids": user_ids})
+
+    return self.api_call("canvases.access.set", params=kwargs)
+
+

Sets the access level to a canvas for specified entities +https://docs.slack.dev/reference/methods/canvases.access.set

+
+
+def canvases_create(self, *, title: str | None = None, document_content: Dict[str, str], **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def canvases_create(
+    self,
+    *,
+    title: Optional[str] = None,
+    document_content: Dict[str, str],
+    **kwargs,
+) -> SlackResponse:
+    """Create Canvas for a user
+    https://docs.slack.dev/reference/methods/canvases.create
+    """
+    kwargs.update({"title": title, "document_content": document_content})
+    return self.api_call("canvases.create", json=kwargs)
+
+ +
+
+def canvases_delete(self, *, canvas_id: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def canvases_delete(
+    self,
+    *,
+    canvas_id: str,
+    **kwargs,
+) -> SlackResponse:
+    """Deletes a canvas
+    https://docs.slack.dev/reference/methods/canvases.delete
+    """
+    kwargs.update({"canvas_id": canvas_id})
+    return self.api_call("canvases.delete", params=kwargs)
+
+ +
+
+def canvases_edit(self, *, canvas_id: str, changes: Sequence[Dict[str, Any]], **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def canvases_edit(
+    self,
+    *,
+    canvas_id: str,
+    changes: Sequence[Dict[str, Any]],
+    **kwargs,
+) -> SlackResponse:
+    """Update an existing canvas
+    https://docs.slack.dev/reference/methods/canvases.edit
+    """
+    kwargs.update({"canvas_id": canvas_id, "changes": changes})
+    return self.api_call("canvases.edit", json=kwargs)
+
+ +
+
+def canvases_sections_lookup(self, *, canvas_id: str, criteria: Dict[str, Any], **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def canvases_sections_lookup(
+    self,
+    *,
+    canvas_id: str,
+    criteria: Dict[str, Any],
+    **kwargs,
+) -> SlackResponse:
+    """Find sections matching the provided criteria
+    https://docs.slack.dev/reference/methods/canvases.sections.lookup
+    """
+    kwargs.update({"canvas_id": canvas_id, "criteria": json.dumps(criteria)})
+    return self.api_call("canvases.sections.lookup", params=kwargs)
+
+

Find sections matching the provided criteria +https://docs.slack.dev/reference/methods/canvases.sections.lookup

+
+
+def channels_archive(self, *, channel: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def channels_archive(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> SlackResponse:
+    """Archives a channel."""
+    kwargs.update({"channel": channel})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("channels.archive", json=kwargs)
+
+

Archives a channel.

+
+
+def channels_create(self, *, name: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def channels_create(
+    self,
+    *,
+    name: str,
+    **kwargs,
+) -> SlackResponse:
+    """Creates a channel."""
+    kwargs.update({"name": name})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("channels.create", json=kwargs)
+
+

Creates a channel.

+
+
+def channels_history(self, *, channel: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def channels_history(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> SlackResponse:
+    """Fetches history of messages and events from a channel."""
+    kwargs.update({"channel": channel})
+    return self.api_call("channels.history", http_verb="GET", params=kwargs)
+
+

Fetches history of messages and events from a channel.

+
+
+def channels_info(self, *, channel: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def channels_info(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> SlackResponse:
+    """Gets information about a channel."""
+    kwargs.update({"channel": channel})
+    return self.api_call("channels.info", http_verb="GET", params=kwargs)
+
+

Gets information about a channel.

+
+
+def channels_invite(self, *, channel: str, user: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def channels_invite(
+    self,
+    *,
+    channel: str,
+    user: str,
+    **kwargs,
+) -> SlackResponse:
+    """Invites a user to a channel."""
+    kwargs.update({"channel": channel, "user": user})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("channels.invite", json=kwargs)
+
+

Invites a user to a channel.

+
+
+def channels_join(self, *, name: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def channels_join(
+    self,
+    *,
+    name: str,
+    **kwargs,
+) -> SlackResponse:
+    """Joins a channel, creating it if needed."""
+    kwargs.update({"name": name})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("channels.join", json=kwargs)
+
+

Joins a channel, creating it if needed.

+
+
+def channels_kick(self, *, channel: str, user: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def channels_kick(
+    self,
+    *,
+    channel: str,
+    user: str,
+    **kwargs,
+) -> SlackResponse:
+    """Removes a user from a channel."""
+    kwargs.update({"channel": channel, "user": user})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("channels.kick", json=kwargs)
+
+

Removes a user from a channel.

+
+
+def channels_leave(self, *, channel: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def channels_leave(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> SlackResponse:
+    """Leaves a channel."""
+    kwargs.update({"channel": channel})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("channels.leave", json=kwargs)
+
+

Leaves a channel.

+
+
+def channels_list(self, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def channels_list(
+    self,
+    **kwargs,
+) -> SlackResponse:
+    """Lists all channels in a Slack team."""
+    return self.api_call("channels.list", http_verb="GET", params=kwargs)
+
+

Lists all channels in a Slack team.

+
+
+def channels_mark(self, *, channel: str, ts: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def channels_mark(
+    self,
+    *,
+    channel: str,
+    ts: str,
+    **kwargs,
+) -> SlackResponse:
+    """Sets the read cursor in a channel."""
+    kwargs.update({"channel": channel, "ts": ts})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("channels.mark", json=kwargs)
+
+

Sets the read cursor in a channel.

+
+
+def channels_rename(self, *, channel: str, name: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def channels_rename(
+    self,
+    *,
+    channel: str,
+    name: str,
+    **kwargs,
+) -> SlackResponse:
+    """Renames a channel."""
+    kwargs.update({"channel": channel, "name": name})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("channels.rename", json=kwargs)
+
+

Renames a channel.

+
+
+def channels_replies(self, *, channel: str, thread_ts: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def channels_replies(
+    self,
+    *,
+    channel: str,
+    thread_ts: str,
+    **kwargs,
+) -> SlackResponse:
+    """Retrieve a thread of messages posted to a channel"""
+    kwargs.update({"channel": channel, "thread_ts": thread_ts})
+    return self.api_call("channels.replies", http_verb="GET", params=kwargs)
+
+

Retrieve a thread of messages posted to a channel

+
+
+def channels_setPurpose(self, *, channel: str, purpose: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def channels_setPurpose(
+    self,
+    *,
+    channel: str,
+    purpose: str,
+    **kwargs,
+) -> SlackResponse:
+    """Sets the purpose for a channel."""
+    kwargs.update({"channel": channel, "purpose": purpose})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("channels.setPurpose", json=kwargs)
+
+

Sets the purpose for a channel.

+
+
+def channels_setTopic(self, *, channel: str, topic: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def channels_setTopic(
+    self,
+    *,
+    channel: str,
+    topic: str,
+    **kwargs,
+) -> SlackResponse:
+    """Sets the topic for a channel."""
+    kwargs.update({"channel": channel, "topic": topic})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("channels.setTopic", json=kwargs)
+
+

Sets the topic for a channel.

+
+
+def channels_unarchive(self, *, channel: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def channels_unarchive(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> SlackResponse:
+    """Unarchives a channel."""
+    kwargs.update({"channel": channel})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("channels.unarchive", json=kwargs)
+
+

Unarchives a channel.

+
+
+def chat_appendStream(self, *, channel: str, ts: str, markdown_text: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def chat_appendStream(
+    self,
+    *,
+    channel: str,
+    ts: str,
+    markdown_text: str,
+    **kwargs,
+) -> SlackResponse:
+    """Appends text to an existing streaming conversation.
+    https://docs.slack.dev/reference/methods/chat.appendStream
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "ts": ts,
+            "markdown_text": markdown_text,
+        }
+    )
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("chat.appendStream", json=kwargs)
+
+

Appends text to an existing streaming conversation. +https://docs.slack.dev/reference/methods/chat.appendStream

+
+
+def chat_delete(self, *, channel: str, ts: str, as_user: bool | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def chat_delete(
+    self,
+    *,
+    channel: str,
+    ts: str,
+    as_user: Optional[bool] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Deletes a message.
+    https://docs.slack.dev/reference/methods/chat.delete
+    """
+    kwargs.update({"channel": channel, "ts": ts, "as_user": as_user})
+    return self.api_call("chat.delete", params=kwargs)
+
+ +
+
+def chat_deleteScheduledMessage(self,
*,
channel: str,
scheduled_message_id: str,
as_user: bool | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def chat_deleteScheduledMessage(
+    self,
+    *,
+    channel: str,
+    scheduled_message_id: str,
+    as_user: Optional[bool] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Deletes a scheduled message.
+    https://docs.slack.dev/reference/methods/chat.deleteScheduledMessage
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "scheduled_message_id": scheduled_message_id,
+            "as_user": as_user,
+        }
+    )
+    return self.api_call("chat.deleteScheduledMessage", params=kwargs)
+
+ +
+ +
+
+ +Expand source code + +
def chat_getPermalink(
+    self,
+    *,
+    channel: str,
+    message_ts: str,
+    **kwargs,
+) -> SlackResponse:
+    """Retrieve a permalink URL for a specific extant message
+    https://docs.slack.dev/reference/methods/chat.getPermalink
+    """
+    kwargs.update({"channel": channel, "message_ts": message_ts})
+    return self.api_call("chat.getPermalink", http_verb="GET", params=kwargs)
+
+

Retrieve a permalink URL for a specific extant message +https://docs.slack.dev/reference/methods/chat.getPermalink

+
+
+def chat_meMessage(self, *, channel: str, text: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def chat_meMessage(
+    self,
+    *,
+    channel: str,
+    text: str,
+    **kwargs,
+) -> SlackResponse:
+    """Share a me message into a channel.
+    https://docs.slack.dev/reference/methods/chat.meMessage
+    """
+    kwargs.update({"channel": channel, "text": text})
+    return self.api_call("chat.meMessage", params=kwargs)
+
+ +
+
+def chat_postEphemeral(self,
*,
channel: str,
user: str,
text: str | None = None,
as_user: bool | None = None,
attachments: str | Sequence[Dict | Attachment] | None = None,
blocks: str | Sequence[Dict | Block] | None = None,
thread_ts: str | None = None,
icon_emoji: str | None = None,
icon_url: str | None = None,
link_names: bool | None = None,
username: str | None = None,
parse: str | None = None,
markdown_text: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def chat_postEphemeral(
+    self,
+    *,
+    channel: str,
+    user: str,
+    text: Optional[str] = None,
+    as_user: Optional[bool] = None,
+    attachments: Optional[Union[str, Sequence[Union[Dict, Attachment]]]] = None,
+    blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None,
+    thread_ts: Optional[str] = None,
+    icon_emoji: Optional[str] = None,
+    icon_url: Optional[str] = None,
+    link_names: Optional[bool] = None,
+    username: Optional[str] = None,
+    parse: Optional[str] = None,
+    markdown_text: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Sends an ephemeral message to a user in a channel.
+    https://docs.slack.dev/reference/methods/chat.postEphemeral
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "user": user,
+            "text": text,
+            "as_user": as_user,
+            "attachments": attachments,
+            "blocks": blocks,
+            "thread_ts": thread_ts,
+            "icon_emoji": icon_emoji,
+            "icon_url": icon_url,
+            "link_names": link_names,
+            "username": username,
+            "parse": parse,
+            "markdown_text": markdown_text,
+        }
+    )
+    _parse_web_class_objects(kwargs)
+    kwargs = _remove_none_values(kwargs)
+    _warn_if_message_text_content_is_missing("chat.postEphemeral", kwargs)
+    # NOTE: intentionally using json over params for the API methods using blocks/attachments
+    return self.api_call("chat.postEphemeral", json=kwargs)
+
+

Sends an ephemeral message to a user in a channel. +https://docs.slack.dev/reference/methods/chat.postEphemeral

+
+
+def chat_postMessage(self,
*,
channel: str,
text: str | None = None,
as_user: bool | None = None,
attachments: str | Sequence[Dict | Attachment] | None = None,
blocks: str | Sequence[Dict | Block] | None = None,
thread_ts: str | None = None,
reply_broadcast: bool | None = None,
unfurl_links: bool | None = None,
unfurl_media: bool | None = None,
container_id: str | None = None,
icon_emoji: str | None = None,
icon_url: str | None = None,
mrkdwn: bool | None = None,
link_names: bool | None = None,
username: str | None = None,
parse: str | None = None,
metadata: Dict | Metadata | EventAndEntityMetadata | None = None,
markdown_text: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def chat_postMessage(
+    self,
+    *,
+    channel: str,
+    text: Optional[str] = None,
+    as_user: Optional[bool] = None,
+    attachments: Optional[Union[str, Sequence[Union[Dict, Attachment]]]] = None,
+    blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None,
+    thread_ts: Optional[str] = None,
+    reply_broadcast: Optional[bool] = None,
+    unfurl_links: Optional[bool] = None,
+    unfurl_media: Optional[bool] = None,
+    container_id: Optional[str] = None,
+    icon_emoji: Optional[str] = None,
+    icon_url: Optional[str] = None,
+    mrkdwn: Optional[bool] = None,
+    link_names: Optional[bool] = None,
+    username: Optional[str] = None,
+    parse: Optional[str] = None,  # none, full
+    metadata: Optional[Union[Dict, Metadata, EventAndEntityMetadata]] = None,
+    markdown_text: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Sends a message to a channel.
+    https://docs.slack.dev/reference/methods/chat.postMessage
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "text": text,
+            "as_user": as_user,
+            "attachments": attachments,
+            "blocks": blocks,
+            "thread_ts": thread_ts,
+            "reply_broadcast": reply_broadcast,
+            "unfurl_links": unfurl_links,
+            "unfurl_media": unfurl_media,
+            "container_id": container_id,
+            "icon_emoji": icon_emoji,
+            "icon_url": icon_url,
+            "mrkdwn": mrkdwn,
+            "link_names": link_names,
+            "username": username,
+            "parse": parse,
+            "metadata": metadata,
+            "markdown_text": markdown_text,
+        }
+    )
+    _parse_web_class_objects(kwargs)
+    kwargs = _remove_none_values(kwargs)
+    _warn_if_message_text_content_is_missing("chat.postMessage", kwargs)
+    # NOTE: intentionally using json over params for the API methods using blocks/attachments
+    return self.api_call("chat.postMessage", json=kwargs)
+
+ +
+
+def chat_scheduleMessage(self,
*,
channel: str,
post_at: str | int,
text: str | None = None,
as_user: bool | None = None,
attachments: str | Sequence[Dict | Attachment] | None = None,
blocks: str | Sequence[Dict | Block] | None = None,
thread_ts: str | None = None,
parse: str | None = None,
reply_broadcast: bool | None = None,
unfurl_links: bool | None = None,
unfurl_media: bool | None = None,
link_names: bool | None = None,
metadata: Dict | Metadata | None = None,
markdown_text: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def chat_scheduleMessage(
+    self,
+    *,
+    channel: str,
+    post_at: Union[str, int],
+    text: Optional[str] = None,
+    as_user: Optional[bool] = None,
+    attachments: Optional[Union[str, Sequence[Union[Dict, Attachment]]]] = None,
+    blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None,
+    thread_ts: Optional[str] = None,
+    parse: Optional[str] = None,
+    reply_broadcast: Optional[bool] = None,
+    unfurl_links: Optional[bool] = None,
+    unfurl_media: Optional[bool] = None,
+    link_names: Optional[bool] = None,
+    metadata: Optional[Union[Dict, Metadata]] = None,
+    markdown_text: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Schedules a message.
+    https://docs.slack.dev/reference/methods/chat.scheduleMessage
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "post_at": post_at,
+            "text": text,
+            "as_user": as_user,
+            "attachments": attachments,
+            "blocks": blocks,
+            "thread_ts": thread_ts,
+            "reply_broadcast": reply_broadcast,
+            "parse": parse,
+            "unfurl_links": unfurl_links,
+            "unfurl_media": unfurl_media,
+            "link_names": link_names,
+            "metadata": metadata,
+            "markdown_text": markdown_text,
+        }
+    )
+    _parse_web_class_objects(kwargs)
+    kwargs = _remove_none_values(kwargs)
+    _warn_if_message_text_content_is_missing("chat.scheduleMessage", kwargs)
+    # NOTE: intentionally using json over params for the API methods using blocks/attachments
+    return self.api_call("chat.scheduleMessage", json=kwargs)
+
+ +
+
+def chat_scheduledMessages_list(self,
*,
channel: str | None = None,
cursor: str | None = None,
latest: str | None = None,
limit: int | None = None,
oldest: str | None = None,
team_id: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def chat_scheduledMessages_list(
+    self,
+    *,
+    channel: Optional[str] = None,
+    cursor: Optional[str] = None,
+    latest: Optional[str] = None,
+    limit: Optional[int] = None,
+    oldest: Optional[str] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Lists all scheduled messages.
+    https://docs.slack.dev/reference/methods/chat.scheduledMessages.list
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "cursor": cursor,
+            "latest": latest,
+            "limit": limit,
+            "oldest": oldest,
+            "team_id": team_id,
+        }
+    )
+    return self.api_call("chat.scheduledMessages.list", params=kwargs)
+
+ +
+
+def chat_startStream(self,
*,
channel: str,
thread_ts: str,
markdown_text: str | None = None,
recipient_team_id: str | None = None,
recipient_user_id: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def chat_startStream(
+    self,
+    *,
+    channel: str,
+    thread_ts: str,
+    markdown_text: Optional[str] = None,
+    recipient_team_id: Optional[str] = None,
+    recipient_user_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Starts a new streaming conversation.
+    https://docs.slack.dev/reference/methods/chat.startStream
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "thread_ts": thread_ts,
+            "markdown_text": markdown_text,
+            "recipient_team_id": recipient_team_id,
+            "recipient_user_id": recipient_user_id,
+        }
+    )
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("chat.startStream", json=kwargs)
+
+

Starts a new streaming conversation. +https://docs.slack.dev/reference/methods/chat.startStream

+
+
+def chat_stopStream(self,
*,
channel: str,
ts: str,
markdown_text: str | None = None,
blocks: str | Sequence[Dict | Block] | None = None,
metadata: Dict | Metadata | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def chat_stopStream(
+    self,
+    *,
+    channel: str,
+    ts: str,
+    markdown_text: Optional[str] = None,
+    blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None,
+    metadata: Optional[Union[Dict, Metadata]] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Stops a streaming conversation.
+    https://docs.slack.dev/reference/methods/chat.stopStream
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "ts": ts,
+            "markdown_text": markdown_text,
+            "blocks": blocks,
+            "metadata": metadata,
+        }
+    )
+    _parse_web_class_objects(kwargs)
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("chat.stopStream", json=kwargs)
+
+ +
+
+def chat_stream(self,
*,
buffer_size: int = 256,
channel: str,
thread_ts: str,
recipient_team_id: str | None = None,
recipient_user_id: str | None = None,
**kwargs) ‑> ChatStream
+
+
+
+ +Expand source code + +
def chat_stream(
+    self,
+    *,
+    buffer_size: int = 256,
+    channel: str,
+    thread_ts: str,
+    recipient_team_id: Optional[str] = None,
+    recipient_user_id: Optional[str] = None,
+    **kwargs,
+) -> ChatStream:
+    """Stream markdown text into a conversation.
+
+    This method starts a new chat stream in a conversation that can be appended to. After appending an entire message,
+    the stream can be stopped with concluding arguments such as "blocks" for gathering feedback.
+
+    The following methods are used:
+
+    - chat.startStream: Starts a new streaming conversation.
+      [Reference](https://docs.slack.dev/reference/methods/chat.startStream).
+    - chat.appendStream: Appends text to an existing streaming conversation.
+      [Reference](https://docs.slack.dev/reference/methods/chat.appendStream).
+    - chat.stopStream: Stops a streaming conversation.
+      [Reference](https://docs.slack.dev/reference/methods/chat.stopStream).
+
+    Args:
+        buffer_size: The length of markdown_text to buffer in-memory before calling a stream method. Increasing this
+          value decreases the number of method calls made for the same amount of text, which is useful to avoid rate
+          limits. Default: 256.
+        channel: An encoded ID that represents a channel, private group, or DM.
+        thread_ts: Provide another message's ts value to reply to. Streamed messages should always be replies to a user
+          request.
+        recipient_team_id: The encoded ID of the team the user receiving the streaming text belongs to. Required when
+          streaming to channels.
+        recipient_user_id: The encoded ID of the user to receive the streaming text. Required when streaming to channels.
+        **kwargs: Additional arguments passed to the underlying API calls.
+
+    Returns:
+        ChatStream instance for managing the stream
+
+    Example:
+        ```python
+        streamer = client.chat_stream(
+            channel="C0123456789",
+            thread_ts="1700000001.123456",
+            recipient_team_id="T0123456789",
+            recipient_user_id="U0123456789",
+        )
+        streamer.append(markdown_text="**hello wo")
+        streamer.append(markdown_text="rld!**")
+        streamer.stop()
+        ```
+    """
+    return ChatStream(
+        self,
+        logger=self._logger,
+        channel=channel,
+        thread_ts=thread_ts,
+        recipient_team_id=recipient_team_id,
+        recipient_user_id=recipient_user_id,
+        buffer_size=buffer_size,
+        **kwargs,
+    )
+
+

Stream markdown text into a conversation.

+

This method starts a new chat stream in a conversation that can be appended to. After appending an entire message, +the stream can be stopped with concluding arguments such as "blocks" for gathering feedback.

+

The following methods are used:

+
    +
  • chat.startStream: Starts a new streaming conversation. +Reference.
  • +
  • chat.appendStream: Appends text to an existing streaming conversation. +Reference.
  • +
  • chat.stopStream: Stops a streaming conversation. +Reference.
  • +
+

Args

+
+
buffer_size
+
The length of markdown_text to buffer in-memory before calling a stream method. Increasing this +value decreases the number of method calls made for the same amount of text, which is useful to avoid rate +limits. Default: 256.
+
channel
+
An encoded ID that represents a channel, private group, or DM.
+
thread_ts
+
Provide another message's ts value to reply to. Streamed messages should always be replies to a user +request.
+
recipient_team_id
+
The encoded ID of the team the user receiving the streaming text belongs to. Required when +streaming to channels.
+
recipient_user_id
+
The encoded ID of the user to receive the streaming text. Required when streaming to channels.
+
**kwargs
+
Additional arguments passed to the underlying API calls.
+
+

Returns

+

ChatStream instance for managing the stream

+

Example

+
streamer = client.chat_stream(
+    channel="C0123456789",
+    thread_ts="1700000001.123456",
+    recipient_team_id="T0123456789",
+    recipient_user_id="U0123456789",
+)
+streamer.append(markdown_text="**hello wo")
+streamer.append(markdown_text="rld!**")
+streamer.stop()
+
+
+
+def chat_unfurl(self,
*,
channel: str | None = None,
ts: str | None = None,
source: str | None = None,
unfurl_id: str | None = None,
unfurls: Dict[str, Dict] | None = None,
metadata: Dict | EventAndEntityMetadata | None = None,
user_auth_blocks: str | Sequence[Dict | Block] | None = None,
user_auth_message: str | None = None,
user_auth_required: bool | None = None,
user_auth_url: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def chat_unfurl(
+    self,
+    *,
+    channel: Optional[str] = None,
+    ts: Optional[str] = None,
+    source: Optional[str] = None,
+    unfurl_id: Optional[str] = None,
+    unfurls: Optional[Dict[str, Dict]] = None,  # or user_auth_*
+    metadata: Optional[Union[Dict, EventAndEntityMetadata]] = None,
+    user_auth_blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None,
+    user_auth_message: Optional[str] = None,
+    user_auth_required: Optional[bool] = None,
+    user_auth_url: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Provide custom unfurl behavior for user-posted URLs.
+    https://docs.slack.dev/reference/methods/chat.unfurl
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "ts": ts,
+            "source": source,
+            "unfurl_id": unfurl_id,
+            "unfurls": unfurls,
+            "metadata": metadata,
+            "user_auth_blocks": user_auth_blocks,
+            "user_auth_message": user_auth_message,
+            "user_auth_required": user_auth_required,
+            "user_auth_url": user_auth_url,
+        }
+    )
+    _parse_web_class_objects(kwargs)  # for user_auth_blocks
+    kwargs = _remove_none_values(kwargs)
+    # NOTE: intentionally using json over params for API methods using blocks/attachments
+    return self.api_call("chat.unfurl", json=kwargs)
+
+

Provide custom unfurl behavior for user-posted URLs. +https://docs.slack.dev/reference/methods/chat.unfurl

+
+
+def chat_update(self,
*,
channel: str,
ts: str,
text: str | None = None,
attachments: str | Sequence[Dict | Attachment] | None = None,
blocks: str | Sequence[Dict | Block] | None = None,
as_user: bool | None = None,
file_ids: str | Sequence[str] | None = None,
link_names: bool | None = None,
parse: str | None = None,
reply_broadcast: bool | None = None,
metadata: Dict | Metadata | None = None,
markdown_text: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def chat_update(
+    self,
+    *,
+    channel: str,
+    ts: str,
+    text: Optional[str] = None,
+    attachments: Optional[Union[str, Sequence[Union[Dict, Attachment]]]] = None,
+    blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None,
+    as_user: Optional[bool] = None,
+    file_ids: Optional[Union[str, Sequence[str]]] = None,
+    link_names: Optional[bool] = None,
+    parse: Optional[str] = None,  # none, full
+    reply_broadcast: Optional[bool] = None,
+    metadata: Optional[Union[Dict, Metadata]] = None,
+    markdown_text: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Updates a message in a channel.
+    https://docs.slack.dev/reference/methods/chat.update
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "ts": ts,
+            "text": text,
+            "attachments": attachments,
+            "blocks": blocks,
+            "as_user": as_user,
+            "link_names": link_names,
+            "parse": parse,
+            "reply_broadcast": reply_broadcast,
+            "metadata": metadata,
+            "markdown_text": markdown_text,
+        }
+    )
+    if isinstance(file_ids, (list, tuple)):
+        kwargs.update({"file_ids": ",".join(file_ids)})
+    else:
+        kwargs.update({"file_ids": file_ids})
+    _parse_web_class_objects(kwargs)
+    kwargs = _remove_none_values(kwargs)
+    _warn_if_message_text_content_is_missing("chat.update", kwargs)
+    # NOTE: intentionally using json over params for API methods using blocks/attachments
+    return self.api_call("chat.update", json=kwargs)
+
+ +
+
+def conversations_acceptSharedInvite(self,
*,
channel_name: str,
channel_id: str | None = None,
invite_id: str | None = None,
free_trial_accepted: bool | None = None,
is_private: bool | None = None,
team_id: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def conversations_acceptSharedInvite(
+    self,
+    *,
+    channel_name: str,
+    channel_id: Optional[str] = None,
+    invite_id: Optional[str] = None,
+    free_trial_accepted: Optional[bool] = None,
+    is_private: Optional[bool] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Accepts an invitation to a Slack Connect channel.
+    https://docs.slack.dev/reference/methods/conversations.acceptSharedInvite
+    """
+    if channel_id is None and invite_id is None:
+        raise e.SlackRequestError("Either channel_id or invite_id must be provided.")
+    kwargs.update(
+        {
+            "channel_name": channel_name,
+            "channel_id": channel_id,
+            "invite_id": invite_id,
+            "free_trial_accepted": free_trial_accepted,
+            "is_private": is_private,
+            "team_id": team_id,
+        }
+    )
+    return self.api_call("conversations.acceptSharedInvite", http_verb="POST", params=kwargs)
+
+

Accepts an invitation to a Slack Connect channel. +https://docs.slack.dev/reference/methods/conversations.acceptSharedInvite

+
+
+def conversations_approveSharedInvite(self, *, invite_id: str, target_team: str | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def conversations_approveSharedInvite(
+    self,
+    *,
+    invite_id: str,
+    target_team: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Approves an invitation to a Slack Connect channel.
+    https://docs.slack.dev/reference/methods/conversations.approveSharedInvite
+    """
+    kwargs.update({"invite_id": invite_id, "target_team": target_team})
+    return self.api_call("conversations.approveSharedInvite", http_verb="POST", params=kwargs)
+
+

Approves an invitation to a Slack Connect channel. +https://docs.slack.dev/reference/methods/conversations.approveSharedInvite

+
+
+def conversations_archive(self, *, channel: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def conversations_archive(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> SlackResponse:
+    """Archives a conversation.
+    https://docs.slack.dev/reference/methods/conversations.archive
+    """
+    kwargs.update({"channel": channel})
+    return self.api_call("conversations.archive", params=kwargs)
+
+ +
+
+def conversations_canvases_create(self, *, channel_id: str, document_content: Dict[str, str], **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def conversations_canvases_create(
+    self,
+    *,
+    channel_id: str,
+    document_content: Dict[str, str],
+    **kwargs,
+) -> SlackResponse:
+    """Create a Channel Canvas for a channel
+    https://docs.slack.dev/reference/methods/conversations.canvases.create
+    """
+    kwargs.update({"channel_id": channel_id, "document_content": document_content})
+    return self.api_call("conversations.canvases.create", json=kwargs)
+
+ +
+
+def conversations_close(self, *, channel: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def conversations_close(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> SlackResponse:
+    """Closes a direct message or multi-person direct message.
+    https://docs.slack.dev/reference/methods/conversations.close
+    """
+    kwargs.update({"channel": channel})
+    return self.api_call("conversations.close", params=kwargs)
+
+

Closes a direct message or multi-person direct message. +https://docs.slack.dev/reference/methods/conversations.close

+
+
+def conversations_create(self,
*,
name: str,
is_private: bool | None = None,
team_id: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def conversations_create(
+    self,
+    *,
+    name: str,
+    is_private: Optional[bool] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Initiates a public or private channel-based conversation
+    https://docs.slack.dev/reference/methods/conversations.create
+    """
+    kwargs.update({"name": name, "is_private": is_private, "team_id": team_id})
+    return self.api_call("conversations.create", params=kwargs)
+
+

Initiates a public or private channel-based conversation +https://docs.slack.dev/reference/methods/conversations.create

+
+
+def conversations_declineSharedInvite(self, *, invite_id: str, target_team: str | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def conversations_declineSharedInvite(
+    self,
+    *,
+    invite_id: str,
+    target_team: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Declines a Slack Connect channel invite.
+    https://docs.slack.dev/reference/methods/conversations.declineSharedInvite
+    """
+    kwargs.update({"invite_id": invite_id, "target_team": target_team})
+    return self.api_call("conversations.declineSharedInvite", http_verb="GET", params=kwargs)
+
+ +
+
+def conversations_externalInvitePermissions_set(self, *, action: str, channel: str, target_team: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def conversations_externalInvitePermissions_set(
+    self, *, action: str, channel: str, target_team: str, **kwargs
+) -> SlackResponse:
+    """Sets a team in a shared External Limited channel to a shared Slack Connect channel or vice versa.
+    https://docs.slack.dev/reference/methods/conversations.externalInvitePermissions.set
+    """
+    kwargs.update(
+        {
+            "action": action,
+            "channel": channel,
+            "target_team": target_team,
+        }
+    )
+    return self.api_call("conversations.externalInvitePermissions.set", params=kwargs)
+
+

Sets a team in a shared External Limited channel to a shared Slack Connect channel or vice versa. +https://docs.slack.dev/reference/methods/conversations.externalInvitePermissions.set

+
+
+def conversations_history(self,
*,
channel: str,
cursor: str | None = None,
inclusive: bool | None = None,
include_all_metadata: bool | None = None,
latest: str | None = None,
limit: int | None = None,
oldest: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def conversations_history(
+    self,
+    *,
+    channel: str,
+    cursor: Optional[str] = None,
+    inclusive: Optional[bool] = None,
+    include_all_metadata: Optional[bool] = None,
+    latest: Optional[str] = None,
+    limit: Optional[int] = None,
+    oldest: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Fetches a conversation's history of messages and events.
+    https://docs.slack.dev/reference/methods/conversations.history
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "cursor": cursor,
+            "inclusive": inclusive,
+            "include_all_metadata": include_all_metadata,
+            "limit": limit,
+            "latest": latest,
+            "oldest": oldest,
+        }
+    )
+    return self.api_call("conversations.history", http_verb="GET", params=kwargs)
+
+

Fetches a conversation's history of messages and events. +https://docs.slack.dev/reference/methods/conversations.history

+
+
+def conversations_info(self,
*,
channel: str,
include_locale: bool | None = None,
include_num_members: bool | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def conversations_info(
+    self,
+    *,
+    channel: str,
+    include_locale: Optional[bool] = None,
+    include_num_members: Optional[bool] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Retrieve information about a conversation.
+    https://docs.slack.dev/reference/methods/conversations.info
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "include_locale": include_locale,
+            "include_num_members": include_num_members,
+        }
+    )
+    return self.api_call("conversations.info", http_verb="GET", params=kwargs)
+
+

Retrieve information about a conversation. +https://docs.slack.dev/reference/methods/conversations.info

+
+
+def conversations_invite(self,
*,
channel: str,
users: str | Sequence[str],
force: bool | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def conversations_invite(
+    self,
+    *,
+    channel: str,
+    users: Union[str, Sequence[str]],
+    force: Optional[bool] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Invites users to a channel.
+    https://docs.slack.dev/reference/methods/conversations.invite
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "force": force,
+        }
+    )
+    if isinstance(users, (list, tuple)):
+        kwargs.update({"users": ",".join(users)})
+    else:
+        kwargs.update({"users": users})
+    return self.api_call("conversations.invite", params=kwargs)
+
+ +
+
+def conversations_inviteShared(self,
*,
channel: str,
emails: str | Sequence[str] | None = None,
user_ids: str | Sequence[str] | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def conversations_inviteShared(
+    self,
+    *,
+    channel: str,
+    emails: Optional[Union[str, Sequence[str]]] = None,
+    user_ids: Optional[Union[str, Sequence[str]]] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Sends an invitation to a Slack Connect channel.
+    https://docs.slack.dev/reference/methods/conversations.inviteShared
+    """
+    if emails is None and user_ids is None:
+        raise e.SlackRequestError("Either emails or user ids must be provided.")
+    kwargs.update({"channel": channel})
+    if isinstance(emails, (list, tuple)):
+        kwargs.update({"emails": ",".join(emails)})
+    else:
+        kwargs.update({"emails": emails})
+    if isinstance(user_ids, (list, tuple)):
+        kwargs.update({"user_ids": ",".join(user_ids)})
+    else:
+        kwargs.update({"user_ids": user_ids})
+    return self.api_call("conversations.inviteShared", http_verb="GET", params=kwargs)
+
+

Sends an invitation to a Slack Connect channel. +https://docs.slack.dev/reference/methods/conversations.inviteShared

+
+
+def conversations_join(self, *, channel: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def conversations_join(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> SlackResponse:
+    """Joins an existing conversation.
+    https://docs.slack.dev/reference/methods/conversations.join
+    """
+    kwargs.update({"channel": channel})
+    return self.api_call("conversations.join", params=kwargs)
+
+ +
+
+def conversations_kick(self, *, channel: str, user: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def conversations_kick(
+    self,
+    *,
+    channel: str,
+    user: str,
+    **kwargs,
+) -> SlackResponse:
+    """Removes a user from a conversation.
+    https://docs.slack.dev/reference/methods/conversations.kick
+    """
+    kwargs.update({"channel": channel, "user": user})
+    return self.api_call("conversations.kick", params=kwargs)
+
+ +
+
+def conversations_leave(self, *, channel: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def conversations_leave(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> SlackResponse:
+    """Leaves a conversation.
+    https://docs.slack.dev/reference/methods/conversations.leave
+    """
+    kwargs.update({"channel": channel})
+    return self.api_call("conversations.leave", params=kwargs)
+
+ +
+
+def conversations_list(self,
*,
cursor: str | None = None,
exclude_archived: bool | None = None,
limit: int | None = None,
team_id: str | None = None,
types: str | Sequence[str] | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def conversations_list(
+    self,
+    *,
+    cursor: Optional[str] = None,
+    exclude_archived: Optional[bool] = None,
+    limit: Optional[int] = None,
+    team_id: Optional[str] = None,
+    types: Optional[Union[str, Sequence[str]]] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Lists all channels in a Slack team.
+    https://docs.slack.dev/reference/methods/conversations.list
+    """
+    kwargs.update(
+        {
+            "cursor": cursor,
+            "exclude_archived": exclude_archived,
+            "limit": limit,
+            "team_id": team_id,
+        }
+    )
+    if isinstance(types, (list, tuple)):
+        kwargs.update({"types": ",".join(types)})
+    else:
+        kwargs.update({"types": types})
+    return self.api_call("conversations.list", http_verb="GET", params=kwargs)
+
+ +
+
+def conversations_listConnectInvites(self,
*,
count: int | None = None,
cursor: str | None = None,
team_id: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def conversations_listConnectInvites(
+    self,
+    *,
+    count: Optional[int] = None,
+    cursor: Optional[str] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """List shared channel invites that have been generated
+    or received but have not yet been approved by all parties.
+    https://docs.slack.dev/reference/methods/conversations.listConnectInvites
+    """
+    kwargs.update({"count": count, "cursor": cursor, "team_id": team_id})
+    return self.api_call("conversations.listConnectInvites", params=kwargs)
+
+

List shared channel invites that have been generated +or received but have not yet been approved by all parties. +https://docs.slack.dev/reference/methods/conversations.listConnectInvites

+
+
+def conversations_mark(self, *, channel: str, ts: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def conversations_mark(
+    self,
+    *,
+    channel: str,
+    ts: str,
+    **kwargs,
+) -> SlackResponse:
+    """Sets the read cursor in a channel.
+    https://docs.slack.dev/reference/methods/conversations.mark
+    """
+    kwargs.update({"channel": channel, "ts": ts})
+    return self.api_call("conversations.mark", params=kwargs)
+
+ +
+
+def conversations_members(self, *, channel: str, cursor: str | None = None, limit: int | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def conversations_members(
+    self,
+    *,
+    channel: str,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Retrieve members of a conversation.
+    https://docs.slack.dev/reference/methods/conversations.members
+    """
+    kwargs.update({"channel": channel, "cursor": cursor, "limit": limit})
+    return self.api_call("conversations.members", http_verb="GET", params=kwargs)
+
+ +
+
+def conversations_open(self,
*,
channel: str | None = None,
return_im: bool | None = None,
users: str | Sequence[str] | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def conversations_open(
+    self,
+    *,
+    channel: Optional[str] = None,
+    return_im: Optional[bool] = None,
+    users: Optional[Union[str, Sequence[str]]] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Opens or resumes a direct message or multi-person direct message.
+    https://docs.slack.dev/reference/methods/conversations.open
+    """
+    if channel is None and users is None:
+        raise e.SlackRequestError("Either channel or users must be provided.")
+    kwargs.update({"channel": channel, "return_im": return_im})
+    if isinstance(users, (list, tuple)):
+        kwargs.update({"users": ",".join(users)})
+    else:
+        kwargs.update({"users": users})
+    return self.api_call("conversations.open", params=kwargs)
+
+

Opens or resumes a direct message or multi-person direct message. +https://docs.slack.dev/reference/methods/conversations.open

+
+
+def conversations_rename(self, *, channel: str, name: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def conversations_rename(
+    self,
+    *,
+    channel: str,
+    name: str,
+    **kwargs,
+) -> SlackResponse:
+    """Renames a conversation.
+    https://docs.slack.dev/reference/methods/conversations.rename
+    """
+    kwargs.update({"channel": channel, "name": name})
+    return self.api_call("conversations.rename", params=kwargs)
+
+ +
+
+def conversations_replies(self,
*,
channel: str,
ts: str,
cursor: str | None = None,
inclusive: bool | None = None,
include_all_metadata: bool | None = None,
latest: str | None = None,
limit: int | None = None,
oldest: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def conversations_replies(
+    self,
+    *,
+    channel: str,
+    ts: str,
+    cursor: Optional[str] = None,
+    inclusive: Optional[bool] = None,
+    include_all_metadata: Optional[bool] = None,
+    latest: Optional[str] = None,
+    limit: Optional[int] = None,
+    oldest: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Retrieve a thread of messages posted to a conversation
+    https://docs.slack.dev/reference/methods/conversations.replies
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "ts": ts,
+            "cursor": cursor,
+            "inclusive": inclusive,
+            "include_all_metadata": include_all_metadata,
+            "limit": limit,
+            "latest": latest,
+            "oldest": oldest,
+        }
+    )
+    return self.api_call("conversations.replies", http_verb="GET", params=kwargs)
+
+

Retrieve a thread of messages posted to a conversation +https://docs.slack.dev/reference/methods/conversations.replies

+
+
+def conversations_requestSharedInvite_approve(self,
*,
invite_id: str,
channel_id: str | None = None,
is_external_limited: str | None = None,
message: Dict[str, Any] | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def conversations_requestSharedInvite_approve(
+    self,
+    *,
+    invite_id: str,
+    channel_id: Optional[str] = None,
+    is_external_limited: Optional[str] = None,
+    message: Optional[Dict[str, Any]] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Approve a request to add an external user to a channel. This also sends them a Slack Connect invite.
+    https://docs.slack.dev/reference/methods/conversations.requestSharedInvite.approve
+    """
+    kwargs.update(
+        {
+            "invite_id": invite_id,
+            "channel_id": channel_id,
+            "is_external_limited": is_external_limited,
+        }
+    )
+    if message is not None:
+        kwargs.update({"message": json.dumps(message)})
+    return self.api_call("conversations.requestSharedInvite.approve", params=kwargs)
+
+

Approve a request to add an external user to a channel. This also sends them a Slack Connect invite. +https://docs.slack.dev/reference/methods/conversations.requestSharedInvite.approve

+
+
+def conversations_requestSharedInvite_deny(self, *, invite_id: str, message: str | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def conversations_requestSharedInvite_deny(
+    self,
+    *,
+    invite_id: str,
+    message: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Deny a request to invite an external user to a channel.
+    https://docs.slack.dev/reference/methods/conversations.requestSharedInvite.deny
+    """
+    kwargs.update({"invite_id": invite_id, "message": message})
+    return self.api_call("conversations.requestSharedInvite.deny", params=kwargs)
+
+

Deny a request to invite an external user to a channel. +https://docs.slack.dev/reference/methods/conversations.requestSharedInvite.deny

+
+
+def conversations_requestSharedInvite_list(self,
*,
cursor: str | None = None,
include_approved: bool | None = None,
include_denied: bool | None = None,
include_expired: bool | None = None,
invite_ids: str | Sequence[str] | None = None,
limit: int | None = None,
user_id: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def conversations_requestSharedInvite_list(
+    self,
+    *,
+    cursor: Optional[str] = None,
+    include_approved: Optional[bool] = None,
+    include_denied: Optional[bool] = None,
+    include_expired: Optional[bool] = None,
+    invite_ids: Optional[Union[str, Sequence[str]]] = None,
+    limit: Optional[int] = None,
+    user_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Lists requests to add external users to channels with ability to filter.
+    https://docs.slack.dev/reference/methods/conversations.requestSharedInvite.list
+    """
+    kwargs.update(
+        {
+            "cursor": cursor,
+            "include_approved": include_approved,
+            "include_denied": include_denied,
+            "include_expired": include_expired,
+            "limit": limit,
+            "user_id": user_id,
+        }
+    )
+    if invite_ids is not None:
+        if isinstance(invite_ids, (list, tuple)):
+            kwargs.update({"invite_ids": ",".join(invite_ids)})
+        else:
+            kwargs.update({"invite_ids": invite_ids})
+    return self.api_call("conversations.requestSharedInvite.list", params=kwargs)
+
+

Lists requests to add external users to channels with ability to filter. +https://docs.slack.dev/reference/methods/conversations.requestSharedInvite.list

+
+
+def conversations_setPurpose(self, *, channel: str, purpose: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def conversations_setPurpose(
+    self,
+    *,
+    channel: str,
+    purpose: str,
+    **kwargs,
+) -> SlackResponse:
+    """Sets the purpose for a conversation.
+    https://docs.slack.dev/reference/methods/conversations.setPurpose
+    """
+    kwargs.update({"channel": channel, "purpose": purpose})
+    return self.api_call("conversations.setPurpose", params=kwargs)
+
+ +
+
+def conversations_setTopic(self, *, channel: str, topic: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def conversations_setTopic(
+    self,
+    *,
+    channel: str,
+    topic: str,
+    **kwargs,
+) -> SlackResponse:
+    """Sets the topic for a conversation.
+    https://docs.slack.dev/reference/methods/conversations.setTopic
+    """
+    kwargs.update({"channel": channel, "topic": topic})
+    return self.api_call("conversations.setTopic", params=kwargs)
+
+ +
+
+def conversations_unarchive(self, *, channel: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def conversations_unarchive(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> SlackResponse:
+    """Reverses conversation archival.
+    https://docs.slack.dev/reference/methods/conversations.unarchive
+    """
+    kwargs.update({"channel": channel})
+    return self.api_call("conversations.unarchive", params=kwargs)
+
+ +
+
+def dialog_open(self, *, dialog: Dict[str, Any], trigger_id: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def dialog_open(
+    self,
+    *,
+    dialog: Dict[str, Any],
+    trigger_id: str,
+    **kwargs,
+) -> SlackResponse:
+    """Open a dialog with a user.
+    https://docs.slack.dev/reference/methods/dialog.open
+    """
+    kwargs.update({"dialog": dialog, "trigger_id": trigger_id})
+    kwargs = _remove_none_values(kwargs)
+    # NOTE: As the dialog can be a dict, this API call works only with json format.
+    return self.api_call("dialog.open", json=kwargs)
+
+ +
+
+def dnd_endDnd(self, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def dnd_endDnd(
+    self,
+    **kwargs,
+) -> SlackResponse:
+    """Ends the current user's Do Not Disturb session immediately.
+    https://docs.slack.dev/reference/methods/dnd.endDnd
+    """
+    return self.api_call("dnd.endDnd", params=kwargs)
+
+

Ends the current user's Do Not Disturb session immediately. +https://docs.slack.dev/reference/methods/dnd.endDnd

+
+
+def dnd_endSnooze(self, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def dnd_endSnooze(
+    self,
+    **kwargs,
+) -> SlackResponse:
+    """Ends the current user's snooze mode immediately.
+    https://docs.slack.dev/reference/methods/dnd.endSnooze
+    """
+    return self.api_call("dnd.endSnooze", params=kwargs)
+
+

Ends the current user's snooze mode immediately. +https://docs.slack.dev/reference/methods/dnd.endSnooze

+
+
+def dnd_info(self, *, team_id: str | None = None, user: str | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def dnd_info(
+    self,
+    *,
+    team_id: Optional[str] = None,
+    user: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Retrieves a user's current Do Not Disturb status.
+    https://docs.slack.dev/reference/methods/dnd.info
+    """
+    kwargs.update({"team_id": team_id, "user": user})
+    return self.api_call("dnd.info", http_verb="GET", params=kwargs)
+
+

Retrieves a user's current Do Not Disturb status. +https://docs.slack.dev/reference/methods/dnd.info

+
+
+def dnd_setSnooze(self, *, num_minutes: str | int, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def dnd_setSnooze(
+    self,
+    *,
+    num_minutes: Union[int, str],
+    **kwargs,
+) -> SlackResponse:
+    """Turns on Do Not Disturb mode for the current user, or changes its duration.
+    https://docs.slack.dev/reference/methods/dnd.setSnooze
+    """
+    kwargs.update({"num_minutes": num_minutes})
+    return self.api_call("dnd.setSnooze", http_verb="GET", params=kwargs)
+
+

Turns on Do Not Disturb mode for the current user, or changes its duration. +https://docs.slack.dev/reference/methods/dnd.setSnooze

+
+
+def dnd_teamInfo(self, users: str | Sequence[str], team_id: str | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def dnd_teamInfo(
+    self,
+    users: Union[str, Sequence[str]],
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Retrieves the Do Not Disturb status for users on a team.
+    https://docs.slack.dev/reference/methods/dnd.teamInfo
+    """
+    if isinstance(users, (list, tuple)):
+        kwargs.update({"users": ",".join(users)})
+    else:
+        kwargs.update({"users": users})
+    kwargs.update({"team_id": team_id})
+    return self.api_call("dnd.teamInfo", http_verb="GET", params=kwargs)
+
+

Retrieves the Do Not Disturb status for users on a team. +https://docs.slack.dev/reference/methods/dnd.teamInfo

+
+
+def emoji_list(self, include_categories: bool | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def emoji_list(
+    self,
+    include_categories: Optional[bool] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Lists custom emoji for a team.
+    https://docs.slack.dev/reference/methods/emoji.list
+    """
+    kwargs.update({"include_categories": include_categories})
+    return self.api_call("emoji.list", http_verb="GET", params=kwargs)
+
+ +
+
+def entity_presentDetails(self,
trigger_id: str,
metadata: Dict | EntityMetadata | None = None,
user_auth_required: bool | None = None,
user_auth_url: str | None = None,
error: Dict[str, Any] | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def entity_presentDetails(
+    self,
+    trigger_id: str,
+    metadata: Optional[Union[Dict, EntityMetadata]] = None,
+    user_auth_required: Optional[bool] = None,
+    user_auth_url: Optional[str] = None,
+    error: Optional[Dict[str, Any]] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Provides entity details for the flexpane.
+    https://docs.slack.dev/reference/methods/entity.presentDetails/
+    """
+    kwargs.update({"trigger_id": trigger_id})
+    if metadata is not None:
+        kwargs.update({"metadata": metadata})
+    if user_auth_required is not None:
+        kwargs.update({"user_auth_required": user_auth_required})
+    if user_auth_url is not None:
+        kwargs.update({"user_auth_url": user_auth_url})
+    if error is not None:
+        kwargs.update({"error": error})
+    _parse_web_class_objects(kwargs)
+    return self.api_call("entity.presentDetails", json=kwargs)
+
+

Provides entity details for the flexpane. +https://docs.slack.dev/reference/methods/entity.presentDetails/

+
+
+def files_comments_delete(self, *, file: str, id: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def files_comments_delete(
+    self,
+    *,
+    file: str,
+    id: str,
+    **kwargs,
+) -> SlackResponse:
+    """Deletes an existing comment on a file.
+    https://docs.slack.dev/reference/methods/files.comments.delete
+    """
+    kwargs.update({"file": file, "id": id})
+    return self.api_call("files.comments.delete", params=kwargs)
+
+ +
+
+def files_completeUploadExternal(self,
*,
files: List[Dict[str, str]],
channel_id: str | None = None,
channels: List[str] | None = None,
initial_comment: str | None = None,
thread_ts: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def files_completeUploadExternal(
+    self,
+    *,
+    files: List[Dict[str, str]],
+    channel_id: Optional[str] = None,
+    channels: Optional[List[str]] = None,
+    initial_comment: Optional[str] = None,
+    thread_ts: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Finishes an upload started with files.getUploadURLExternal.
+    https://docs.slack.dev/reference/methods/files.completeUploadExternal
+    """
+    _files = [{k: v for k, v in f.items() if v is not None} for f in files]
+    kwargs.update(
+        {
+            "files": json.dumps(_files),
+            "channel_id": channel_id,
+            "initial_comment": initial_comment,
+            "thread_ts": thread_ts,
+        }
+    )
+    if channels:
+        kwargs["channels"] = ",".join(channels)
+    return self.api_call("files.completeUploadExternal", params=kwargs)
+
+

Finishes an upload started with files.getUploadURLExternal. +https://docs.slack.dev/reference/methods/files.completeUploadExternal

+
+
+def files_delete(self, *, file: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def files_delete(
+    self,
+    *,
+    file: str,
+    **kwargs,
+) -> SlackResponse:
+    """Deletes a file.
+    https://docs.slack.dev/reference/methods/files.delete
+    """
+    kwargs.update({"file": file})
+    return self.api_call("files.delete", params=kwargs)
+
+ +
+
+def files_getUploadURLExternal(self,
*,
filename: str,
length: int,
alt_txt: str | None = None,
snippet_type: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def files_getUploadURLExternal(
+    self,
+    *,
+    filename: str,
+    length: int,
+    alt_txt: Optional[str] = None,
+    snippet_type: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Gets a URL for an edge external upload.
+    https://docs.slack.dev/reference/methods/files.getUploadURLExternal
+    """
+    kwargs.update(
+        {
+            "filename": filename,
+            "length": length,
+            "alt_txt": alt_txt,
+            "snippet_type": snippet_type,
+        }
+    )
+    return self.api_call("files.getUploadURLExternal", params=kwargs)
+
+ +
+
+def files_info(self,
*,
file: str,
count: int | None = None,
cursor: str | None = None,
limit: int | None = None,
page: int | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def files_info(
+    self,
+    *,
+    file: str,
+    count: Optional[int] = None,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    page: Optional[int] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Gets information about a team file.
+    https://docs.slack.dev/reference/methods/files.info
+    """
+    kwargs.update(
+        {
+            "file": file,
+            "count": count,
+            "cursor": cursor,
+            "limit": limit,
+            "page": page,
+        }
+    )
+    return self.api_call("files.info", http_verb="GET", params=kwargs)
+
+

Gets information about a team file. +https://docs.slack.dev/reference/methods/files.info

+
+
+def files_list(self,
*,
channel: str | None = None,
count: int | None = None,
page: int | None = None,
show_files_hidden_by_limit: bool | None = None,
team_id: str | None = None,
ts_from: str | None = None,
ts_to: str | None = None,
types: str | Sequence[str] | None = None,
user: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def files_list(
+    self,
+    *,
+    channel: Optional[str] = None,
+    count: Optional[int] = None,
+    page: Optional[int] = None,
+    show_files_hidden_by_limit: Optional[bool] = None,
+    team_id: Optional[str] = None,
+    ts_from: Optional[str] = None,
+    ts_to: Optional[str] = None,
+    types: Optional[Union[str, Sequence[str]]] = None,
+    user: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Lists & filters team files.
+    https://docs.slack.dev/reference/methods/files.list
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "count": count,
+            "page": page,
+            "show_files_hidden_by_limit": show_files_hidden_by_limit,
+            "team_id": team_id,
+            "ts_from": ts_from,
+            "ts_to": ts_to,
+            "user": user,
+        }
+    )
+    if isinstance(types, (list, tuple)):
+        kwargs.update({"types": ",".join(types)})
+    else:
+        kwargs.update({"types": types})
+    return self.api_call("files.list", http_verb="GET", params=kwargs)
+
+ +
+
+def files_remote_add(self,
*,
external_id: str,
external_url: str,
title: str,
filetype: str | None = None,
indexable_file_contents: str | bytes | io.IOBase | None = None,
preview_image: str | bytes | io.IOBase | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def files_remote_add(
+    self,
+    *,
+    external_id: str,
+    external_url: str,
+    title: str,
+    filetype: Optional[str] = None,
+    indexable_file_contents: Optional[Union[str, bytes, IOBase]] = None,
+    preview_image: Optional[Union[str, bytes, IOBase]] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Adds a file from a remote service.
+    https://docs.slack.dev/reference/methods/files.remote.add
+    """
+    kwargs.update(
+        {
+            "external_id": external_id,
+            "external_url": external_url,
+            "title": title,
+            "filetype": filetype,
+        }
+    )
+    files = None
+    # preview_image (file): Preview of the document via multipart/form-data.
+    if preview_image is not None or indexable_file_contents is not None:
+        files = {
+            "preview_image": preview_image,
+            "indexable_file_contents": indexable_file_contents,
+        }
+
+    return self.api_call(
+        # Intentionally using "POST" method over "GET" here
+        "files.remote.add",
+        http_verb="POST",
+        data=kwargs,
+        files=files,
+    )
+
+ +
+
+def files_remote_info(self, *, external_id: str | None = None, file: str | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def files_remote_info(
+    self,
+    *,
+    external_id: Optional[str] = None,
+    file: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Retrieve information about a remote file added to Slack.
+    https://docs.slack.dev/reference/methods/files.remote.info
+    """
+    kwargs.update({"external_id": external_id, "file": file})
+    return self.api_call("files.remote.info", http_verb="GET", params=kwargs)
+
+

Retrieve information about a remote file added to Slack. +https://docs.slack.dev/reference/methods/files.remote.info

+
+
+def files_remote_list(self,
*,
channel: str | None = None,
cursor: str | None = None,
limit: int | None = None,
ts_from: str | None = None,
ts_to: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def files_remote_list(
+    self,
+    *,
+    channel: Optional[str] = None,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    ts_from: Optional[str] = None,
+    ts_to: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Retrieve information about a remote file added to Slack.
+    https://docs.slack.dev/reference/methods/files.remote.list
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "cursor": cursor,
+            "limit": limit,
+            "ts_from": ts_from,
+            "ts_to": ts_to,
+        }
+    )
+    return self.api_call("files.remote.list", http_verb="GET", params=kwargs)
+
+

Retrieve information about a remote file added to Slack. +https://docs.slack.dev/reference/methods/files.remote.list

+
+
+def files_remote_remove(self, *, external_id: str | None = None, file: str | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def files_remote_remove(
+    self,
+    *,
+    external_id: Optional[str] = None,
+    file: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Remove a remote file.
+    https://docs.slack.dev/reference/methods/files.remote.remove
+    """
+    kwargs.update({"external_id": external_id, "file": file})
+    return self.api_call("files.remote.remove", http_verb="POST", params=kwargs)
+
+ +
+
+def files_remote_share(self,
*,
channels: str | Sequence[str],
external_id: str | None = None,
file: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def files_remote_share(
+    self,
+    *,
+    channels: Union[str, Sequence[str]],
+    external_id: Optional[str] = None,
+    file: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Share a remote file into a channel.
+    https://docs.slack.dev/reference/methods/files.remote.share
+    """
+    if external_id is None and file is None:
+        raise e.SlackRequestError("Either external_id or file must be provided.")
+    if isinstance(channels, (list, tuple)):
+        kwargs.update({"channels": ",".join(channels)})
+    else:
+        kwargs.update({"channels": channels})
+    kwargs.update({"external_id": external_id, "file": file})
+    return self.api_call("files.remote.share", http_verb="GET", params=kwargs)
+
+ +
+
+def files_remote_update(self,
*,
external_id: str | None = None,
external_url: str | None = None,
file: str | None = None,
title: str | None = None,
filetype: str | None = None,
indexable_file_contents: str | None = None,
preview_image: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def files_remote_update(
+    self,
+    *,
+    external_id: Optional[str] = None,
+    external_url: Optional[str] = None,
+    file: Optional[str] = None,
+    title: Optional[str] = None,
+    filetype: Optional[str] = None,
+    indexable_file_contents: Optional[str] = None,
+    preview_image: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Updates an existing remote file.
+    https://docs.slack.dev/reference/methods/files.remote.update
+    """
+    kwargs.update(
+        {
+            "external_id": external_id,
+            "external_url": external_url,
+            "file": file,
+            "title": title,
+            "filetype": filetype,
+        }
+    )
+    files = None
+    # preview_image (file): Preview of the document via multipart/form-data.
+    if preview_image is not None or indexable_file_contents is not None:
+        files = {
+            "preview_image": preview_image,
+            "indexable_file_contents": indexable_file_contents,
+        }
+
+    return self.api_call(
+        # Intentionally using "POST" method over "GET" here
+        "files.remote.update",
+        http_verb="POST",
+        data=kwargs,
+        files=files,
+    )
+
+ +
+
+def files_revokePublicURL(self, *, file: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def files_revokePublicURL(
+    self,
+    *,
+    file: str,
+    **kwargs,
+) -> SlackResponse:
+    """Revokes public/external sharing access for a file
+    https://docs.slack.dev/reference/methods/files.revokePublicURL
+    """
+    kwargs.update({"file": file})
+    return self.api_call("files.revokePublicURL", params=kwargs)
+
+

Revokes public/external sharing access for a file +https://docs.slack.dev/reference/methods/files.revokePublicURL

+
+
+def files_sharedPublicURL(self, *, file: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def files_sharedPublicURL(
+    self,
+    *,
+    file: str,
+    **kwargs,
+) -> SlackResponse:
+    """Enables a file for public/external sharing.
+    https://docs.slack.dev/reference/methods/files.sharedPublicURL
+    """
+    kwargs.update({"file": file})
+    return self.api_call("files.sharedPublicURL", params=kwargs)
+
+

Enables a file for public/external sharing. +https://docs.slack.dev/reference/methods/files.sharedPublicURL

+
+
+def files_upload(self,
*,
file: str | bytes | io.IOBase | None = None,
content: str | bytes | None = None,
filename: str | None = None,
filetype: str | None = None,
initial_comment: str | None = None,
thread_ts: str | None = None,
title: str | None = None,
channels: str | Sequence[str] | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def files_upload(
+    self,
+    *,
+    file: Optional[Union[str, bytes, IOBase]] = None,
+    content: Optional[Union[str, bytes]] = None,
+    filename: Optional[str] = None,
+    filetype: Optional[str] = None,
+    initial_comment: Optional[str] = None,
+    thread_ts: Optional[str] = None,
+    title: Optional[str] = None,
+    channels: Optional[Union[str, Sequence[str]]] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Uploads or creates a file.
+    https://docs.slack.dev/reference/methods/files.upload
+    """
+    _print_files_upload_v2_suggestion()
+
+    if file is None and content is None:
+        raise e.SlackRequestError("The file or content argument must be specified.")
+    if file is not None and content is not None:
+        raise e.SlackRequestError("You cannot specify both the file and the content argument.")
+
+    if isinstance(channels, (list, tuple)):
+        kwargs.update({"channels": ",".join(channels)})
+    else:
+        kwargs.update({"channels": channels})
+    kwargs.update(
+        {
+            "filename": filename,
+            "filetype": filetype,
+            "initial_comment": initial_comment,
+            "thread_ts": thread_ts,
+            "title": title,
+        }
+    )
+    if file:
+        if kwargs.get("filename") is None and isinstance(file, str):
+            # use the local filename if filename is missing
+            if kwargs.get("filename") is None:
+                kwargs["filename"] = file.split(os.path.sep)[-1]
+        return self.api_call("files.upload", files={"file": file}, data=kwargs)
+    else:
+        kwargs["content"] = content
+        return self.api_call("files.upload", data=kwargs)
+
+ +
+
+def files_upload_v2(self,
*,
filename: str | None = None,
file: str | bytes | io.IOBase | os.PathLike | None = None,
content: str | bytes | None = None,
title: str | None = None,
alt_txt: str | None = None,
snippet_type: str | None = None,
file_uploads: List[Dict[str, Any]] | None = None,
channel: str | None = None,
channels: List[str] | None = None,
initial_comment: str | None = None,
thread_ts: str | None = None,
request_file_info: bool = True,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def files_upload_v2(
+    self,
+    *,
+    # for sending a single file
+    filename: Optional[str] = None,  # you can skip this only when sending along with content parameter
+    file: Optional[Union[str, bytes, IOBase, os.PathLike]] = None,
+    content: Optional[Union[str, bytes]] = None,
+    title: Optional[str] = None,
+    alt_txt: Optional[str] = None,
+    snippet_type: Optional[str] = None,
+    # To upload multiple files at a time
+    file_uploads: Optional[List[Dict[str, Any]]] = None,
+    channel: Optional[str] = None,
+    channels: Optional[List[str]] = None,
+    initial_comment: Optional[str] = None,
+    thread_ts: Optional[str] = None,
+    request_file_info: bool = True,  # since v3.23, this flag is no longer necessary
+    **kwargs,
+) -> SlackResponse:
+    """This wrapper method provides an easy way to upload files using the following endpoints:
+
+    - step1: https://docs.slack.dev/reference/methods/files.getUploadURLExternal
+
+    - step2: "https://files.slack.com/upload/v1/..." URLs returned from files.getUploadURLExternal API
+
+    - step3: https://docs.slack.dev/reference/methods/files.completeUploadExternal
+        and https://docs.slack.dev/reference/methods/files.info
+
+    """
+    if file is None and content is None and file_uploads is None:
+        raise e.SlackRequestError("Any of file, content, and file_uploads must be specified.")
+    if file is not None and content is not None:
+        raise e.SlackRequestError("You cannot specify both the file and the content argument.")
+
+    # deprecated arguments:
+    filetype = kwargs.get("filetype")
+
+    if filetype is not None:
+        warnings.warn("The filetype parameter is no longer supported. Please remove it from the arguments.")
+
+    # step1: files.getUploadURLExternal per file
+    files: List[Dict[str, Any]] = []
+    if file_uploads is not None:
+        for f in file_uploads:
+            files.append(_to_v2_file_upload_item(f))
+    else:
+        f = _to_v2_file_upload_item(
+            {
+                "filename": filename,
+                "file": file,
+                "content": content,
+                "title": title,
+                "alt_txt": alt_txt,
+                "snippet_type": snippet_type,
+            }
+        )
+        files.append(f)
+
+    for f in files:
+        url_response = self.files_getUploadURLExternal(
+            filename=f.get("filename"),  # type: ignore[arg-type]
+            length=f.get("length"),  # type: ignore[arg-type]
+            alt_txt=f.get("alt_txt"),
+            snippet_type=f.get("snippet_type"),
+            token=kwargs.get("token"),
+        )
+        _validate_for_legacy_client(url_response)
+        f["file_id"] = url_response.get("file_id")  # type: ignore[union-attr, unused-ignore]
+        f["upload_url"] = url_response.get("upload_url")  # type: ignore[union-attr, unused-ignore]
+
+    # step2: "https://files.slack.com/upload/v1/..." per file
+    for f in files:
+        upload_result = self._upload_file(
+            url=f["upload_url"],
+            data=f["data"],
+            logger=self._logger,
+            timeout=self.timeout,
+            proxy=self.proxy,
+            ssl=self.ssl,
+        )
+        if upload_result.status != 200:
+            status = upload_result.status
+            body = upload_result.body
+            message = (
+                "Failed to upload a file "
+                f"(status: {status}, body: {body}, filename: {f.get('filename')}, title: {f.get('title')})"
+            )
+            raise e.SlackRequestError(message)
+
+    # step3: files.completeUploadExternal with all the sets of (file_id + title)
+    completion = self.files_completeUploadExternal(
+        files=[{"id": f["file_id"], "title": f["title"]} for f in files],
+        channel_id=channel,
+        channels=channels,
+        initial_comment=initial_comment,
+        thread_ts=thread_ts,
+        **kwargs,
+    )
+    if len(completion.get("files")) == 1:  # type: ignore[arg-type, union-attr, unused-ignore]
+        completion.data["file"] = completion.get("files")[0]  # type: ignore[index, union-attr, unused-ignore]
+    return completion
+
+

This wrapper method provides an easy way to upload files using the following endpoints:

+
+
+
+def functions_completeError(self, *, function_execution_id: str, error: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def functions_completeError(
+    self,
+    *,
+    function_execution_id: str,
+    error: str,
+    **kwargs,
+) -> SlackResponse:
+    """Signal the failure to execute a function
+    https://docs.slack.dev/reference/methods/functions.completeError
+    """
+    kwargs.update({"function_execution_id": function_execution_id, "error": error})
+    return self.api_call("functions.completeError", params=kwargs)
+
+ +
+
+def functions_completeSuccess(self, *, function_execution_id: str, outputs: Dict[str, Any], **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def functions_completeSuccess(
+    self,
+    *,
+    function_execution_id: str,
+    outputs: Dict[str, Any],
+    **kwargs,
+) -> SlackResponse:
+    """Signal the successful completion of a function
+    https://docs.slack.dev/reference/methods/functions.completeSuccess
+    """
+    kwargs.update({"function_execution_id": function_execution_id, "outputs": json.dumps(outputs)})
+    return self.api_call("functions.completeSuccess", params=kwargs)
+
+

Signal the successful completion of a function +https://docs.slack.dev/reference/methods/functions.completeSuccess

+
+
+def groups_archive(self, *, channel: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def groups_archive(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> SlackResponse:
+    """Archives a private channel."""
+    kwargs.update({"channel": channel})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("groups.archive", json=kwargs)
+
+

Archives a private channel.

+
+
+def groups_create(self, *, name: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def groups_create(
+    self,
+    *,
+    name: str,
+    **kwargs,
+) -> SlackResponse:
+    """Creates a private channel."""
+    kwargs.update({"name": name})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("groups.create", json=kwargs)
+
+

Creates a private channel.

+
+
+def groups_createChild(self, *, channel: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def groups_createChild(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> SlackResponse:
+    """Clones and archives a private channel."""
+    kwargs.update({"channel": channel})
+    return self.api_call("groups.createChild", http_verb="GET", params=kwargs)
+
+

Clones and archives a private channel.

+
+
+def groups_history(self, *, channel: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def groups_history(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> SlackResponse:
+    """Fetches history of messages and events from a private channel."""
+    kwargs.update({"channel": channel})
+    return self.api_call("groups.history", http_verb="GET", params=kwargs)
+
+

Fetches history of messages and events from a private channel.

+
+
+def groups_info(self, *, channel: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def groups_info(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> SlackResponse:
+    """Gets information about a private channel."""
+    kwargs.update({"channel": channel})
+    return self.api_call("groups.info", http_verb="GET", params=kwargs)
+
+

Gets information about a private channel.

+
+
+def groups_invite(self, *, channel: str, user: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def groups_invite(
+    self,
+    *,
+    channel: str,
+    user: str,
+    **kwargs,
+) -> SlackResponse:
+    """Invites a user to a private channel."""
+    kwargs.update({"channel": channel, "user": user})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("groups.invite", json=kwargs)
+
+

Invites a user to a private channel.

+
+
+def groups_kick(self, *, channel: str, user: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def groups_kick(
+    self,
+    *,
+    channel: str,
+    user: str,
+    **kwargs,
+) -> SlackResponse:
+    """Removes a user from a private channel."""
+    kwargs.update({"channel": channel, "user": user})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("groups.kick", json=kwargs)
+
+

Removes a user from a private channel.

+
+
+def groups_leave(self, *, channel: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def groups_leave(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> SlackResponse:
+    """Leaves a private channel."""
+    kwargs.update({"channel": channel})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("groups.leave", json=kwargs)
+
+

Leaves a private channel.

+
+
+def groups_list(self, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def groups_list(
+    self,
+    **kwargs,
+) -> SlackResponse:
+    """Lists private channels that the calling user has access to."""
+    return self.api_call("groups.list", http_verb="GET", params=kwargs)
+
+

Lists private channels that the calling user has access to.

+
+
+def groups_mark(self, *, channel: str, ts: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def groups_mark(
+    self,
+    *,
+    channel: str,
+    ts: str,
+    **kwargs,
+) -> SlackResponse:
+    """Sets the read cursor in a private channel."""
+    kwargs.update({"channel": channel, "ts": ts})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("groups.mark", json=kwargs)
+
+

Sets the read cursor in a private channel.

+
+
+def groups_open(self, *, channel: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def groups_open(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> SlackResponse:
+    """Opens a private channel."""
+    kwargs.update({"channel": channel})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("groups.open", json=kwargs)
+
+

Opens a private channel.

+
+
+def groups_rename(self, *, channel: str, name: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def groups_rename(
+    self,
+    *,
+    channel: str,
+    name: str,
+    **kwargs,
+) -> SlackResponse:
+    """Renames a private channel."""
+    kwargs.update({"channel": channel, "name": name})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("groups.rename", json=kwargs)
+
+

Renames a private channel.

+
+
+def groups_replies(self, *, channel: str, thread_ts: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def groups_replies(
+    self,
+    *,
+    channel: str,
+    thread_ts: str,
+    **kwargs,
+) -> SlackResponse:
+    """Retrieve a thread of messages posted to a private channel"""
+    kwargs.update({"channel": channel, "thread_ts": thread_ts})
+    return self.api_call("groups.replies", http_verb="GET", params=kwargs)
+
+

Retrieve a thread of messages posted to a private channel

+
+
+def groups_setPurpose(self, *, channel: str, purpose: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def groups_setPurpose(
+    self,
+    *,
+    channel: str,
+    purpose: str,
+    **kwargs,
+) -> SlackResponse:
+    """Sets the purpose for a private channel."""
+    kwargs.update({"channel": channel, "purpose": purpose})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("groups.setPurpose", json=kwargs)
+
+

Sets the purpose for a private channel.

+
+
+def groups_setTopic(self, *, channel: str, topic: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def groups_setTopic(
+    self,
+    *,
+    channel: str,
+    topic: str,
+    **kwargs,
+) -> SlackResponse:
+    """Sets the topic for a private channel."""
+    kwargs.update({"channel": channel, "topic": topic})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("groups.setTopic", json=kwargs)
+
+

Sets the topic for a private channel.

+
+
+def groups_unarchive(self, *, channel: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def groups_unarchive(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> SlackResponse:
+    """Unarchives a private channel."""
+    kwargs.update({"channel": channel})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("groups.unarchive", json=kwargs)
+
+

Unarchives a private channel.

+
+
+def im_close(self, *, channel: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def im_close(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> SlackResponse:
+    """Close a direct message channel."""
+    kwargs.update({"channel": channel})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("im.close", json=kwargs)
+
+

Close a direct message channel.

+
+
+def im_history(self, *, channel: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def im_history(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> SlackResponse:
+    """Fetches history of messages and events from direct message channel."""
+    kwargs.update({"channel": channel})
+    return self.api_call("im.history", http_verb="GET", params=kwargs)
+
+

Fetches history of messages and events from direct message channel.

+
+
+def im_list(self, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def im_list(
+    self,
+    **kwargs,
+) -> SlackResponse:
+    """Lists direct message channels for the calling user."""
+    return self.api_call("im.list", http_verb="GET", params=kwargs)
+
+

Lists direct message channels for the calling user.

+
+
+def im_mark(self, *, channel: str, ts: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def im_mark(
+    self,
+    *,
+    channel: str,
+    ts: str,
+    **kwargs,
+) -> SlackResponse:
+    """Sets the read cursor in a direct message channel."""
+    kwargs.update({"channel": channel, "ts": ts})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("im.mark", json=kwargs)
+
+

Sets the read cursor in a direct message channel.

+
+
+def im_open(self, *, user: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def im_open(
+    self,
+    *,
+    user: str,
+    **kwargs,
+) -> SlackResponse:
+    """Opens a direct message channel."""
+    kwargs.update({"user": user})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("im.open", json=kwargs)
+
+

Opens a direct message channel.

+
+
+def im_replies(self, *, channel: str, thread_ts: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def im_replies(
+    self,
+    *,
+    channel: str,
+    thread_ts: str,
+    **kwargs,
+) -> SlackResponse:
+    """Retrieve a thread of messages posted to a direct message conversation"""
+    kwargs.update({"channel": channel, "thread_ts": thread_ts})
+    return self.api_call("im.replies", http_verb="GET", params=kwargs)
+
+

Retrieve a thread of messages posted to a direct message conversation

+
+
+def migration_exchange(self,
*,
users: str | Sequence[str],
team_id: str | None = None,
to_old: bool | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def migration_exchange(
+    self,
+    *,
+    users: Union[str, Sequence[str]],
+    team_id: Optional[str] = None,
+    to_old: Optional[bool] = None,
+    **kwargs,
+) -> SlackResponse:
+    """For Enterprise Grid workspaces, map local user IDs to global user IDs
+    https://docs.slack.dev/reference/methods/migration.exchange
+    """
+    if isinstance(users, (list, tuple)):
+        kwargs.update({"users": ",".join(users)})
+    else:
+        kwargs.update({"users": users})
+    kwargs.update({"team_id": team_id, "to_old": to_old})
+    return self.api_call("migration.exchange", http_verb="GET", params=kwargs)
+
+

For Enterprise Grid workspaces, map local user IDs to global user IDs +https://docs.slack.dev/reference/methods/migration.exchange

+
+
+def mpim_close(self, *, channel: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def mpim_close(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> SlackResponse:
+    """Closes a multiparty direct message channel."""
+    kwargs.update({"channel": channel})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("mpim.close", json=kwargs)
+
+

Closes a multiparty direct message channel.

+
+
+def mpim_history(self, *, channel: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def mpim_history(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> SlackResponse:
+    """Fetches history of messages and events from a multiparty direct message."""
+    kwargs.update({"channel": channel})
+    return self.api_call("mpim.history", http_verb="GET", params=kwargs)
+
+

Fetches history of messages and events from a multiparty direct message.

+
+
+def mpim_list(self, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def mpim_list(
+    self,
+    **kwargs,
+) -> SlackResponse:
+    """Lists multiparty direct message channels for the calling user."""
+    return self.api_call("mpim.list", http_verb="GET", params=kwargs)
+
+

Lists multiparty direct message channels for the calling user.

+
+
+def mpim_mark(self, *, channel: str, ts: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def mpim_mark(
+    self,
+    *,
+    channel: str,
+    ts: str,
+    **kwargs,
+) -> SlackResponse:
+    """Sets the read cursor in a multiparty direct message channel."""
+    kwargs.update({"channel": channel, "ts": ts})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("mpim.mark", json=kwargs)
+
+

Sets the read cursor in a multiparty direct message channel.

+
+
+def mpim_open(self, *, users: str | Sequence[str], **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def mpim_open(
+    self,
+    *,
+    users: Union[str, Sequence[str]],
+    **kwargs,
+) -> SlackResponse:
+    """This method opens a multiparty direct message."""
+    if isinstance(users, (list, tuple)):
+        kwargs.update({"users": ",".join(users)})
+    else:
+        kwargs.update({"users": users})
+    return self.api_call("mpim.open", params=kwargs)
+
+

This method opens a multiparty direct message.

+
+
+def mpim_replies(self, *, channel: str, thread_ts: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def mpim_replies(
+    self,
+    *,
+    channel: str,
+    thread_ts: str,
+    **kwargs,
+) -> SlackResponse:
+    """Retrieve a thread of messages posted to a direct message conversation from a
+    multiparty direct message.
+    """
+    kwargs.update({"channel": channel, "thread_ts": thread_ts})
+    return self.api_call("mpim.replies", http_verb="GET", params=kwargs)
+
+

Retrieve a thread of messages posted to a direct message conversation from a +multiparty direct message.

+
+
+def oauth_access(self,
*,
client_id: str,
client_secret: str,
code: str,
redirect_uri: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def oauth_access(
+    self,
+    *,
+    client_id: str,
+    client_secret: str,
+    code: str,
+    redirect_uri: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Exchanges a temporary OAuth verifier code for an access token.
+    https://docs.slack.dev/reference/methods/oauth.access
+    """
+    if redirect_uri is not None:
+        kwargs.update({"redirect_uri": redirect_uri})
+    kwargs.update({"code": code})
+    return self.api_call(
+        "oauth.access",
+        data=kwargs,
+        auth={"client_id": client_id, "client_secret": client_secret},
+    )
+
+

Exchanges a temporary OAuth verifier code for an access token. +https://docs.slack.dev/reference/methods/oauth.access

+
+
+def oauth_v2_access(self,
*,
client_id: str,
client_secret: str,
code: str | None = None,
redirect_uri: str | None = None,
grant_type: str | None = None,
refresh_token: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def oauth_v2_access(
+    self,
+    *,
+    client_id: str,
+    client_secret: str,
+    # This field is required when processing the OAuth redirect URL requests
+    # while it's absent for token rotation
+    code: Optional[str] = None,
+    redirect_uri: Optional[str] = None,
+    # This field is required for token rotation
+    grant_type: Optional[str] = None,
+    # This field is required for token rotation
+    refresh_token: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Exchanges a temporary OAuth verifier code for an access token.
+    https://docs.slack.dev/reference/methods/oauth.v2.access
+    """
+    if redirect_uri is not None:
+        kwargs.update({"redirect_uri": redirect_uri})
+    if code is not None:
+        kwargs.update({"code": code})
+    if grant_type is not None:
+        kwargs.update({"grant_type": grant_type})
+    if refresh_token is not None:
+        kwargs.update({"refresh_token": refresh_token})
+    return self.api_call(
+        "oauth.v2.access",
+        data=kwargs,
+        auth={"client_id": client_id, "client_secret": client_secret},
+    )
+
+

Exchanges a temporary OAuth verifier code for an access token. +https://docs.slack.dev/reference/methods/oauth.v2.access

+
+
+def oauth_v2_exchange(self, *, token: str, client_id: str, client_secret: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def oauth_v2_exchange(
+    self,
+    *,
+    token: str,
+    client_id: str,
+    client_secret: str,
+    **kwargs,
+) -> SlackResponse:
+    """Exchanges a legacy access token for a new expiring access token and refresh token
+    https://docs.slack.dev/reference/methods/oauth.v2.exchange
+    """
+    kwargs.update({"client_id": client_id, "client_secret": client_secret, "token": token})
+    return self.api_call("oauth.v2.exchange", params=kwargs)
+
+

Exchanges a legacy access token for a new expiring access token and refresh token +https://docs.slack.dev/reference/methods/oauth.v2.exchange

+
+
+def openid_connect_token(self,
client_id: str,
client_secret: str,
code: str | None = None,
redirect_uri: str | None = None,
grant_type: str | None = None,
refresh_token: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def openid_connect_token(
+    self,
+    client_id: str,
+    client_secret: str,
+    code: Optional[str] = None,
+    redirect_uri: Optional[str] = None,
+    grant_type: Optional[str] = None,
+    refresh_token: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Exchanges a temporary OAuth verifier code for an access token for Sign in with Slack.
+    https://docs.slack.dev/reference/methods/openid.connect.token
+    """
+    if redirect_uri is not None:
+        kwargs.update({"redirect_uri": redirect_uri})
+    if code is not None:
+        kwargs.update({"code": code})
+    if grant_type is not None:
+        kwargs.update({"grant_type": grant_type})
+    if refresh_token is not None:
+        kwargs.update({"refresh_token": refresh_token})
+    return self.api_call(
+        "openid.connect.token",
+        data=kwargs,
+        auth={"client_id": client_id, "client_secret": client_secret},
+    )
+
+

Exchanges a temporary OAuth verifier code for an access token for Sign in with Slack. +https://docs.slack.dev/reference/methods/openid.connect.token

+
+
+def openid_connect_userInfo(self, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def openid_connect_userInfo(
+    self,
+    **kwargs,
+) -> SlackResponse:
+    """Get the identity of a user who has authorized Sign in with Slack.
+    https://docs.slack.dev/reference/methods/openid.connect.userInfo
+    """
+    return self.api_call("openid.connect.userInfo", params=kwargs)
+
+

Get the identity of a user who has authorized Sign in with Slack. +https://docs.slack.dev/reference/methods/openid.connect.userInfo

+
+
+def pins_add(self, *, channel: str, timestamp: str | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def pins_add(
+    self,
+    *,
+    channel: str,
+    timestamp: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Pins an item to a channel.
+    https://docs.slack.dev/reference/methods/pins.add
+    """
+    kwargs.update({"channel": channel, "timestamp": timestamp})
+    return self.api_call("pins.add", params=kwargs)
+
+ +
+
+def pins_list(self, *, channel: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def pins_list(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> SlackResponse:
+    """Lists items pinned to a channel.
+    https://docs.slack.dev/reference/methods/pins.list
+    """
+    kwargs.update({"channel": channel})
+    return self.api_call("pins.list", http_verb="GET", params=kwargs)
+
+

Lists items pinned to a channel. +https://docs.slack.dev/reference/methods/pins.list

+
+
+def pins_remove(self, *, channel: str, timestamp: str | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def pins_remove(
+    self,
+    *,
+    channel: str,
+    timestamp: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Un-pins an item from a channel.
+    https://docs.slack.dev/reference/methods/pins.remove
+    """
+    kwargs.update({"channel": channel, "timestamp": timestamp})
+    return self.api_call("pins.remove", params=kwargs)
+
+ +
+
+def reactions_add(self, *, channel: str, name: str, timestamp: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def reactions_add(
+    self,
+    *,
+    channel: str,
+    name: str,
+    timestamp: str,
+    **kwargs,
+) -> SlackResponse:
+    """Adds a reaction to an item.
+    https://docs.slack.dev/reference/methods/reactions.add
+    """
+    kwargs.update({"channel": channel, "name": name, "timestamp": timestamp})
+    return self.api_call("reactions.add", params=kwargs)
+
+ +
+
+def reactions_get(self,
*,
channel: str | None = None,
file: str | None = None,
file_comment: str | None = None,
full: bool | None = None,
timestamp: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def reactions_get(
+    self,
+    *,
+    channel: Optional[str] = None,
+    file: Optional[str] = None,
+    file_comment: Optional[str] = None,
+    full: Optional[bool] = None,
+    timestamp: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Gets reactions for an item.
+    https://docs.slack.dev/reference/methods/reactions.get
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "file": file,
+            "file_comment": file_comment,
+            "full": full,
+            "timestamp": timestamp,
+        }
+    )
+    return self.api_call("reactions.get", http_verb="GET", params=kwargs)
+
+ +
+
+def reactions_list(self,
*,
count: int | None = None,
cursor: str | None = None,
full: bool | None = None,
limit: int | None = None,
page: int | None = None,
team_id: str | None = None,
user: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def reactions_list(
+    self,
+    *,
+    count: Optional[int] = None,
+    cursor: Optional[str] = None,
+    full: Optional[bool] = None,
+    limit: Optional[int] = None,
+    page: Optional[int] = None,
+    team_id: Optional[str] = None,
+    user: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Lists reactions made by a user.
+    https://docs.slack.dev/reference/methods/reactions.list
+    """
+    kwargs.update(
+        {
+            "count": count,
+            "cursor": cursor,
+            "full": full,
+            "limit": limit,
+            "page": page,
+            "team_id": team_id,
+            "user": user,
+        }
+    )
+    return self.api_call("reactions.list", http_verb="GET", params=kwargs)
+
+ +
+
+def reactions_remove(self,
*,
name: str,
channel: str | None = None,
file: str | None = None,
file_comment: str | None = None,
timestamp: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def reactions_remove(
+    self,
+    *,
+    name: str,
+    channel: Optional[str] = None,
+    file: Optional[str] = None,
+    file_comment: Optional[str] = None,
+    timestamp: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Removes a reaction from an item.
+    https://docs.slack.dev/reference/methods/reactions.remove
+    """
+    kwargs.update(
+        {
+            "name": name,
+            "channel": channel,
+            "file": file,
+            "file_comment": file_comment,
+            "timestamp": timestamp,
+        }
+    )
+    return self.api_call("reactions.remove", params=kwargs)
+
+ +
+
+def reminders_add(self,
*,
text: str,
time: str,
team_id: str | None = None,
user: str | None = None,
recurrence: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def reminders_add(
+    self,
+    *,
+    text: str,
+    time: str,
+    team_id: Optional[str] = None,
+    user: Optional[str] = None,
+    recurrence: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Creates a reminder.
+    https://docs.slack.dev/reference/methods/reminders.add
+    """
+    kwargs.update(
+        {
+            "text": text,
+            "time": time,
+            "team_id": team_id,
+            "user": user,
+            "recurrence": recurrence,
+        }
+    )
+    return self.api_call("reminders.add", params=kwargs)
+
+ +
+
+def reminders_complete(self, *, reminder: str, team_id: str | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def reminders_complete(
+    self,
+    *,
+    reminder: str,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Marks a reminder as complete.
+    https://docs.slack.dev/reference/methods/reminders.complete
+    """
+    kwargs.update({"reminder": reminder, "team_id": team_id})
+    return self.api_call("reminders.complete", params=kwargs)
+
+ +
+
+def reminders_delete(self, *, reminder: str, team_id: str | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def reminders_delete(
+    self,
+    *,
+    reminder: str,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Deletes a reminder.
+    https://docs.slack.dev/reference/methods/reminders.delete
+    """
+    kwargs.update({"reminder": reminder, "team_id": team_id})
+    return self.api_call("reminders.delete", params=kwargs)
+
+ +
+
+def reminders_info(self, *, reminder: str, team_id: str | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def reminders_info(
+    self,
+    *,
+    reminder: str,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Gets information about a reminder.
+    https://docs.slack.dev/reference/methods/reminders.info
+    """
+    kwargs.update({"reminder": reminder, "team_id": team_id})
+    return self.api_call("reminders.info", http_verb="GET", params=kwargs)
+
+ +
+
+def reminders_list(self, *, team_id: str | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def reminders_list(
+    self,
+    *,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Lists all reminders created by or for a given user.
+    https://docs.slack.dev/reference/methods/reminders.list
+    """
+    kwargs.update({"team_id": team_id})
+    return self.api_call("reminders.list", http_verb="GET", params=kwargs)
+
+

Lists all reminders created by or for a given user. +https://docs.slack.dev/reference/methods/reminders.list

+
+
+def rtm_connect(self,
*,
batch_presence_aware: bool | None = None,
presence_sub: bool | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def rtm_connect(
+    self,
+    *,
+    batch_presence_aware: Optional[bool] = None,
+    presence_sub: Optional[bool] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Starts a Real Time Messaging session.
+    https://docs.slack.dev/reference/methods/rtm.connect
+    """
+    kwargs.update({"batch_presence_aware": batch_presence_aware, "presence_sub": presence_sub})
+    return self.api_call("rtm.connect", http_verb="GET", params=kwargs)
+
+

Starts a Real Time Messaging session. +https://docs.slack.dev/reference/methods/rtm.connect

+
+
+def rtm_start(self,
*,
batch_presence_aware: bool | None = None,
include_locale: bool | None = None,
mpim_aware: bool | None = None,
no_latest: bool | None = None,
no_unreads: bool | None = None,
presence_sub: bool | None = None,
simple_latest: bool | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def rtm_start(
+    self,
+    *,
+    batch_presence_aware: Optional[bool] = None,
+    include_locale: Optional[bool] = None,
+    mpim_aware: Optional[bool] = None,
+    no_latest: Optional[bool] = None,
+    no_unreads: Optional[bool] = None,
+    presence_sub: Optional[bool] = None,
+    simple_latest: Optional[bool] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Starts a Real Time Messaging session.
+    https://docs.slack.dev/reference/methods/rtm.start
+    """
+    kwargs.update(
+        {
+            "batch_presence_aware": batch_presence_aware,
+            "include_locale": include_locale,
+            "mpim_aware": mpim_aware,
+            "no_latest": no_latest,
+            "no_unreads": no_unreads,
+            "presence_sub": presence_sub,
+            "simple_latest": simple_latest,
+        }
+    )
+    return self.api_call("rtm.start", http_verb="GET", params=kwargs)
+
+

Starts a Real Time Messaging session. +https://docs.slack.dev/reference/methods/rtm.start

+
+
+def search_all(self,
*,
query: str,
count: int | None = None,
highlight: bool | None = None,
page: int | None = None,
sort: str | None = None,
sort_dir: str | None = None,
team_id: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def search_all(
+    self,
+    *,
+    query: str,
+    count: Optional[int] = None,
+    highlight: Optional[bool] = None,
+    page: Optional[int] = None,
+    sort: Optional[str] = None,
+    sort_dir: Optional[str] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Searches for messages and files matching a query.
+    https://docs.slack.dev/reference/methods/search.all
+    """
+    kwargs.update(
+        {
+            "query": query,
+            "count": count,
+            "highlight": highlight,
+            "page": page,
+            "sort": sort,
+            "sort_dir": sort_dir,
+            "team_id": team_id,
+        }
+    )
+    return self.api_call("search.all", http_verb="GET", params=kwargs)
+
+

Searches for messages and files matching a query. +https://docs.slack.dev/reference/methods/search.all

+
+
+def search_files(self,
*,
query: str,
count: int | None = None,
highlight: bool | None = None,
page: int | None = None,
sort: str | None = None,
sort_dir: str | None = None,
team_id: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def search_files(
+    self,
+    *,
+    query: str,
+    count: Optional[int] = None,
+    highlight: Optional[bool] = None,
+    page: Optional[int] = None,
+    sort: Optional[str] = None,
+    sort_dir: Optional[str] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Searches for files matching a query.
+    https://docs.slack.dev/reference/methods/search.files
+    """
+    kwargs.update(
+        {
+            "query": query,
+            "count": count,
+            "highlight": highlight,
+            "page": page,
+            "sort": sort,
+            "sort_dir": sort_dir,
+            "team_id": team_id,
+        }
+    )
+    return self.api_call("search.files", http_verb="GET", params=kwargs)
+
+

Searches for files matching a query. +https://docs.slack.dev/reference/methods/search.files

+
+
+def search_messages(self,
*,
query: str,
count: int | None = None,
cursor: str | None = None,
highlight: bool | None = None,
page: int | None = None,
sort: str | None = None,
sort_dir: str | None = None,
team_id: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def search_messages(
+    self,
+    *,
+    query: str,
+    count: Optional[int] = None,
+    cursor: Optional[str] = None,
+    highlight: Optional[bool] = None,
+    page: Optional[int] = None,
+    sort: Optional[str] = None,
+    sort_dir: Optional[str] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Searches for messages matching a query.
+    https://docs.slack.dev/reference/methods/search.messages
+    """
+    kwargs.update(
+        {
+            "query": query,
+            "count": count,
+            "cursor": cursor,
+            "highlight": highlight,
+            "page": page,
+            "sort": sort,
+            "sort_dir": sort_dir,
+            "team_id": team_id,
+        }
+    )
+    return self.api_call("search.messages", http_verb="GET", params=kwargs)
+
+

Searches for messages matching a query. +https://docs.slack.dev/reference/methods/search.messages

+
+
+def slackLists_access_delete(self,
*,
list_id: str,
channel_ids: List[str] | None = None,
user_ids: List[str] | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def slackLists_access_delete(
+    self,
+    *,
+    list_id: str,
+    channel_ids: Optional[List[str]] = None,
+    user_ids: Optional[List[str]] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Revoke access to a List for specified entities.
+    https://docs.slack.dev/reference/methods/slackLists.access.delete
+    """
+    kwargs.update({"list_id": list_id, "channel_ids": channel_ids, "user_ids": user_ids})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("slackLists.access.delete", json=kwargs)
+
+

Revoke access to a List for specified entities. +https://docs.slack.dev/reference/methods/slackLists.access.delete

+
+
+def slackLists_access_set(self,
*,
list_id: str,
access_level: str,
channel_ids: List[str] | None = None,
user_ids: List[str] | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def slackLists_access_set(
+    self,
+    *,
+    list_id: str,
+    access_level: str,
+    channel_ids: Optional[List[str]] = None,
+    user_ids: Optional[List[str]] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Set the access level to a List for specified entities.
+    https://docs.slack.dev/reference/methods/slackLists.access.set
+    """
+    kwargs.update({"list_id": list_id, "access_level": access_level, "channel_ids": channel_ids, "user_ids": user_ids})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("slackLists.access.set", json=kwargs)
+
+

Set the access level to a List for specified entities. +https://docs.slack.dev/reference/methods/slackLists.access.set

+
+
+def slackLists_create(self,
*,
name: str,
description_blocks: str | Sequence[Dict | RichTextBlock] | None = None,
schema: List[Dict[str, Any]] | None = None,
copy_from_list_id: str | None = None,
include_copied_list_records: bool | None = None,
todo_mode: bool | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def slackLists_create(
+    self,
+    *,
+    name: str,
+    description_blocks: Optional[Union[str, Sequence[Union[Dict, RichTextBlock]]]] = None,
+    schema: Optional[List[Dict[str, Any]]] = None,
+    copy_from_list_id: Optional[str] = None,
+    include_copied_list_records: Optional[bool] = None,
+    todo_mode: Optional[bool] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Creates a List.
+    https://docs.slack.dev/reference/methods/slackLists.create
+    """
+    kwargs.update(
+        {
+            "name": name,
+            "description_blocks": description_blocks,
+            "schema": schema,
+            "copy_from_list_id": copy_from_list_id,
+            "include_copied_list_records": include_copied_list_records,
+            "todo_mode": todo_mode,
+        }
+    )
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("slackLists.create", json=kwargs)
+
+ +
+
+def slackLists_download_get(self, *, list_id: str, job_id: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def slackLists_download_get(
+    self,
+    *,
+    list_id: str,
+    job_id: str,
+    **kwargs,
+) -> SlackResponse:
+    """Retrieve List download URL from an export job to download List contents.
+    https://docs.slack.dev/reference/methods/slackLists.download.get
+    """
+    kwargs.update(
+        {
+            "list_id": list_id,
+            "job_id": job_id,
+        }
+    )
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("slackLists.download.get", json=kwargs)
+
+

Retrieve List download URL from an export job to download List contents. +https://docs.slack.dev/reference/methods/slackLists.download.get

+
+
+def slackLists_download_start(self, *, list_id: str, include_archived: bool | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def slackLists_download_start(
+    self,
+    *,
+    list_id: str,
+    include_archived: Optional[bool] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Initiate a job to export List contents.
+    https://docs.slack.dev/reference/methods/slackLists.download.start
+    """
+    kwargs.update(
+        {
+            "list_id": list_id,
+            "include_archived": include_archived,
+        }
+    )
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("slackLists.download.start", json=kwargs)
+
+ +
+
+def slackLists_items_create(self,
*,
list_id: str,
duplicated_item_id: str | None = None,
parent_item_id: str | None = None,
initial_fields: List[Dict[str, Any]] | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def slackLists_items_create(
+    self,
+    *,
+    list_id: str,
+    duplicated_item_id: Optional[str] = None,
+    parent_item_id: Optional[str] = None,
+    initial_fields: Optional[List[Dict[str, Any]]] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Add a new item to an existing List.
+    https://docs.slack.dev/reference/methods/slackLists.items.create
+    """
+    kwargs.update(
+        {
+            "list_id": list_id,
+            "duplicated_item_id": duplicated_item_id,
+            "parent_item_id": parent_item_id,
+            "initial_fields": initial_fields,
+        }
+    )
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("slackLists.items.create", json=kwargs)
+
+ +
+
+def slackLists_items_delete(self, *, list_id: str, id: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def slackLists_items_delete(
+    self,
+    *,
+    list_id: str,
+    id: str,
+    **kwargs,
+) -> SlackResponse:
+    """Deletes an item from an existing List.
+    https://docs.slack.dev/reference/methods/slackLists.items.delete
+    """
+    kwargs.update(
+        {
+            "list_id": list_id,
+            "id": id,
+        }
+    )
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("slackLists.items.delete", json=kwargs)
+
+ +
+
+def slackLists_items_deleteMultiple(self, *, list_id: str, ids: List[str], **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def slackLists_items_deleteMultiple(
+    self,
+    *,
+    list_id: str,
+    ids: List[str],
+    **kwargs,
+) -> SlackResponse:
+    """Deletes multiple items from an existing List.
+    https://docs.slack.dev/reference/methods/slackLists.items.deleteMultiple
+    """
+    kwargs.update(
+        {
+            "list_id": list_id,
+            "ids": ids,
+        }
+    )
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("slackLists.items.deleteMultiple", json=kwargs)
+
+ +
+
+def slackLists_items_info(self, *, list_id: str, id: str, include_is_subscribed: bool | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def slackLists_items_info(
+    self,
+    *,
+    list_id: str,
+    id: str,
+    include_is_subscribed: Optional[bool] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Get a row from a List.
+    https://docs.slack.dev/reference/methods/slackLists.items.info
+    """
+    kwargs.update(
+        {
+            "list_id": list_id,
+            "id": id,
+            "include_is_subscribed": include_is_subscribed,
+        }
+    )
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("slackLists.items.info", json=kwargs)
+
+ +
+
+def slackLists_items_list(self,
*,
list_id: str,
limit: int | None = None,
cursor: str | None = None,
archived: bool | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def slackLists_items_list(
+    self,
+    *,
+    list_id: str,
+    limit: Optional[int] = None,
+    cursor: Optional[str] = None,
+    archived: Optional[bool] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Get records from a List.
+    https://docs.slack.dev/reference/methods/slackLists.items.list
+    """
+    kwargs.update(
+        {
+            "list_id": list_id,
+            "limit": limit,
+            "cursor": cursor,
+            "archived": archived,
+        }
+    )
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("slackLists.items.list", json=kwargs)
+
+ +
+
+def slackLists_items_update(self, *, list_id: str, cells: List[Dict[str, Any]], **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def slackLists_items_update(
+    self,
+    *,
+    list_id: str,
+    cells: List[Dict[str, Any]],
+    **kwargs,
+) -> SlackResponse:
+    """Updates cells in a List.
+    https://docs.slack.dev/reference/methods/slackLists.items.update
+    """
+    kwargs.update(
+        {
+            "list_id": list_id,
+            "cells": cells,
+        }
+    )
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("slackLists.items.update", json=kwargs)
+
+ +
+
+def slackLists_update(self,
*,
id: str,
name: str | None = None,
description_blocks: str | Sequence[Dict | RichTextBlock] | None = None,
todo_mode: bool | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def slackLists_update(
+    self,
+    *,
+    id: str,
+    name: Optional[str] = None,
+    description_blocks: Optional[Union[str, Sequence[Union[Dict, RichTextBlock]]]] = None,
+    todo_mode: Optional[bool] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Update a List.
+    https://docs.slack.dev/reference/methods/slackLists.update
+    """
+    kwargs.update(
+        {
+            "id": id,
+            "name": name,
+            "description_blocks": description_blocks,
+            "todo_mode": todo_mode,
+        }
+    )
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("slackLists.update", json=kwargs)
+
+ +
+
+def stars_add(self,
*,
channel: str | None = None,
file: str | None = None,
file_comment: str | None = None,
timestamp: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def stars_add(
+    self,
+    *,
+    channel: Optional[str] = None,
+    file: Optional[str] = None,
+    file_comment: Optional[str] = None,
+    timestamp: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Adds a star to an item.
+    https://docs.slack.dev/reference/methods/stars.add
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "file": file,
+            "file_comment": file_comment,
+            "timestamp": timestamp,
+        }
+    )
+    return self.api_call("stars.add", params=kwargs)
+
+ +
+
+def stars_list(self,
*,
count: int | None = None,
cursor: str | None = None,
limit: int | None = None,
page: int | None = None,
team_id: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def stars_list(
+    self,
+    *,
+    count: Optional[int] = None,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    page: Optional[int] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Lists stars for a user.
+    https://docs.slack.dev/reference/methods/stars.list
+    """
+    kwargs.update(
+        {
+            "count": count,
+            "cursor": cursor,
+            "limit": limit,
+            "page": page,
+            "team_id": team_id,
+        }
+    )
+    return self.api_call("stars.list", http_verb="GET", params=kwargs)
+
+ +
+
+def stars_remove(self,
*,
channel: str | None = None,
file: str | None = None,
file_comment: str | None = None,
timestamp: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def stars_remove(
+    self,
+    *,
+    channel: Optional[str] = None,
+    file: Optional[str] = None,
+    file_comment: Optional[str] = None,
+    timestamp: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Removes a star from an item.
+    https://docs.slack.dev/reference/methods/stars.remove
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "file": file,
+            "file_comment": file_comment,
+            "timestamp": timestamp,
+        }
+    )
+    return self.api_call("stars.remove", params=kwargs)
+
+ +
+
+def team_accessLogs(self,
*,
before: str | int | None = None,
count: str | int | None = None,
page: str | int | None = None,
team_id: str | None = None,
cursor: str | None = None,
limit: int | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def team_accessLogs(
+    self,
+    *,
+    before: Optional[Union[int, str]] = None,
+    count: Optional[Union[int, str]] = None,
+    page: Optional[Union[int, str]] = None,
+    team_id: Optional[str] = None,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Gets the access logs for the current team.
+    https://docs.slack.dev/reference/methods/team.accessLogs
+    """
+    kwargs.update(
+        {
+            "before": before,
+            "count": count,
+            "page": page,
+            "team_id": team_id,
+            "cursor": cursor,
+            "limit": limit,
+        }
+    )
+    return self.api_call("team.accessLogs", http_verb="GET", params=kwargs)
+
+

Gets the access logs for the current team. +https://docs.slack.dev/reference/methods/team.accessLogs

+
+
+def team_billableInfo(self, *, team_id: str | None = None, user: str | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def team_billableInfo(
+    self,
+    *,
+    team_id: Optional[str] = None,
+    user: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Gets billable users information for the current team.
+    https://docs.slack.dev/reference/methods/team.billableInfo
+    """
+    kwargs.update({"team_id": team_id, "user": user})
+    return self.api_call("team.billableInfo", http_verb="GET", params=kwargs)
+
+

Gets billable users information for the current team. +https://docs.slack.dev/reference/methods/team.billableInfo

+
+
+def team_billing_info(self, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def team_billing_info(
+    self,
+    **kwargs,
+) -> SlackResponse:
+    """Reads a workspace's billing plan information.
+    https://docs.slack.dev/reference/methods/team.billing.info
+    """
+    return self.api_call("team.billing.info", params=kwargs)
+
+

Reads a workspace's billing plan information. +https://docs.slack.dev/reference/methods/team.billing.info

+
+
+def team_externalTeams_disconnect(self, *, target_team: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def team_externalTeams_disconnect(
+    self,
+    *,
+    target_team: str,
+    **kwargs,
+) -> SlackResponse:
+    """Disconnects an external organization.
+    https://docs.slack.dev/reference/methods/team.externalTeams.disconnect
+    """
+    kwargs.update(
+        {
+            "target_team": target_team,
+        }
+    )
+    return self.api_call("team.externalTeams.disconnect", params=kwargs)
+
+ +
+
+def team_externalTeams_list(self,
*,
connection_status_filter: str | None = None,
slack_connect_pref_filter: Sequence[str] | None = None,
sort_direction: str | None = None,
sort_field: str | None = None,
workspace_filter: Sequence[str] | None = None,
cursor: str | None = None,
limit: int | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def team_externalTeams_list(
+    self,
+    *,
+    connection_status_filter: Optional[str] = None,
+    slack_connect_pref_filter: Optional[Sequence[str]] = None,
+    sort_direction: Optional[str] = None,
+    sort_field: Optional[str] = None,
+    workspace_filter: Optional[Sequence[str]] = None,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Returns a list of all the external teams connected and details about the connection.
+    https://docs.slack.dev/reference/methods/team.externalTeams.list
+    """
+    kwargs.update(
+        {
+            "connection_status_filter": connection_status_filter,
+            "sort_direction": sort_direction,
+            "sort_field": sort_field,
+            "cursor": cursor,
+            "limit": limit,
+        }
+    )
+    if slack_connect_pref_filter is not None:
+        if isinstance(slack_connect_pref_filter, (list, tuple)):
+            kwargs.update({"slack_connect_pref_filter": ",".join(slack_connect_pref_filter)})
+        else:
+            kwargs.update({"slack_connect_pref_filter": slack_connect_pref_filter})
+    if workspace_filter is not None:
+        if isinstance(workspace_filter, (list, tuple)):
+            kwargs.update({"workspace_filter": ",".join(workspace_filter)})
+        else:
+            kwargs.update({"workspace_filter": workspace_filter})
+    return self.api_call("team.externalTeams.list", http_verb="GET", params=kwargs)
+
+

Returns a list of all the external teams connected and details about the connection. +https://docs.slack.dev/reference/methods/team.externalTeams.list

+
+
+def team_info(self, *, team: str | None = None, domain: str | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def team_info(
+    self,
+    *,
+    team: Optional[str] = None,
+    domain: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Gets information about the current team.
+    https://docs.slack.dev/reference/methods/team.info
+    """
+    kwargs.update({"team": team, "domain": domain})
+    return self.api_call("team.info", http_verb="GET", params=kwargs)
+
+

Gets information about the current team. +https://docs.slack.dev/reference/methods/team.info

+
+
+def team_integrationLogs(self,
*,
app_id: str | None = None,
change_type: str | None = None,
count: str | int | None = None,
page: str | int | None = None,
service_id: str | None = None,
team_id: str | None = None,
user: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def team_integrationLogs(
+    self,
+    *,
+    app_id: Optional[str] = None,
+    change_type: Optional[str] = None,
+    count: Optional[Union[int, str]] = None,
+    page: Optional[Union[int, str]] = None,
+    service_id: Optional[str] = None,
+    team_id: Optional[str] = None,
+    user: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Gets the integration logs for the current team.
+    https://docs.slack.dev/reference/methods/team.integrationLogs
+    """
+    kwargs.update(
+        {
+            "app_id": app_id,
+            "change_type": change_type,
+            "count": count,
+            "page": page,
+            "service_id": service_id,
+            "team_id": team_id,
+            "user": user,
+        }
+    )
+    return self.api_call("team.integrationLogs", http_verb="GET", params=kwargs)
+
+

Gets the integration logs for the current team. +https://docs.slack.dev/reference/methods/team.integrationLogs

+
+
+def team_preferences_list(self, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def team_preferences_list(
+    self,
+    **kwargs,
+) -> SlackResponse:
+    """Retrieve a list of a workspace's team preferences.
+    https://docs.slack.dev/reference/methods/team.preferences.list
+    """
+    return self.api_call("team.preferences.list", params=kwargs)
+
+

Retrieve a list of a workspace's team preferences. +https://docs.slack.dev/reference/methods/team.preferences.list

+
+
+def team_profile_get(self, *, visibility: str | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def team_profile_get(
+    self,
+    *,
+    visibility: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Retrieve a team's profile.
+    https://docs.slack.dev/reference/methods/team.profile.get
+    """
+    kwargs.update({"visibility": visibility})
+    return self.api_call("team.profile.get", http_verb="GET", params=kwargs)
+
+ +
+
+def tooling_tokens_rotate(self, *, refresh_token: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def tooling_tokens_rotate(
+    self,
+    *,
+    refresh_token: str,
+    **kwargs,
+) -> SlackResponse:
+    """Exchanges a refresh token for a new app configuration token
+    https://docs.slack.dev/reference/methods/tooling.tokens.rotate
+    """
+    kwargs.update({"refresh_token": refresh_token})
+    return self.api_call("tooling.tokens.rotate", params=kwargs)
+
+

Exchanges a refresh token for a new app configuration token +https://docs.slack.dev/reference/methods/tooling.tokens.rotate

+
+
+def usergroups_create(self,
*,
name: str,
channels: str | Sequence[str] | None = None,
description: str | None = None,
handle: str | None = None,
include_count: bool | None = None,
team_id: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def usergroups_create(
+    self,
+    *,
+    name: str,
+    channels: Optional[Union[str, Sequence[str]]] = None,
+    description: Optional[str] = None,
+    handle: Optional[str] = None,
+    include_count: Optional[bool] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Create a User Group
+    https://docs.slack.dev/reference/methods/usergroups.create
+    """
+    kwargs.update(
+        {
+            "name": name,
+            "description": description,
+            "handle": handle,
+            "include_count": include_count,
+            "team_id": team_id,
+        }
+    )
+    if isinstance(channels, (list, tuple)):
+        kwargs.update({"channels": ",".join(channels)})
+    else:
+        kwargs.update({"channels": channels})
+    return self.api_call("usergroups.create", params=kwargs)
+
+ +
+
+def usergroups_disable(self,
*,
usergroup: str,
include_count: bool | None = None,
team_id: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def usergroups_disable(
+    self,
+    *,
+    usergroup: str,
+    include_count: Optional[bool] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Disable an existing User Group
+    https://docs.slack.dev/reference/methods/usergroups.disable
+    """
+    kwargs.update({"usergroup": usergroup, "include_count": include_count, "team_id": team_id})
+    return self.api_call("usergroups.disable", params=kwargs)
+
+ +
+
+def usergroups_enable(self,
*,
usergroup: str,
include_count: bool | None = None,
team_id: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def usergroups_enable(
+    self,
+    *,
+    usergroup: str,
+    include_count: Optional[bool] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Enable a User Group
+    https://docs.slack.dev/reference/methods/usergroups.enable
+    """
+    kwargs.update({"usergroup": usergroup, "include_count": include_count, "team_id": team_id})
+    return self.api_call("usergroups.enable", params=kwargs)
+
+ +
+
+def usergroups_list(self,
*,
include_count: bool | None = None,
include_disabled: bool | None = None,
include_users: bool | None = None,
team_id: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def usergroups_list(
+    self,
+    *,
+    include_count: Optional[bool] = None,
+    include_disabled: Optional[bool] = None,
+    include_users: Optional[bool] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """List all User Groups for a team
+    https://docs.slack.dev/reference/methods/usergroups.list
+    """
+    kwargs.update(
+        {
+            "include_count": include_count,
+            "include_disabled": include_disabled,
+            "include_users": include_users,
+            "team_id": team_id,
+        }
+    )
+    return self.api_call("usergroups.list", http_verb="GET", params=kwargs)
+
+ +
+
+def usergroups_update(self,
*,
usergroup: str,
channels: str | Sequence[str] | None = None,
description: str | None = None,
handle: str | None = None,
include_count: bool | None = None,
name: str | None = None,
team_id: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def usergroups_update(
+    self,
+    *,
+    usergroup: str,
+    channels: Optional[Union[str, Sequence[str]]] = None,
+    description: Optional[str] = None,
+    handle: Optional[str] = None,
+    include_count: Optional[bool] = None,
+    name: Optional[str] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Update an existing User Group
+    https://docs.slack.dev/reference/methods/usergroups.update
+    """
+    kwargs.update(
+        {
+            "usergroup": usergroup,
+            "description": description,
+            "handle": handle,
+            "include_count": include_count,
+            "name": name,
+            "team_id": team_id,
+        }
+    )
+    if isinstance(channels, (list, tuple)):
+        kwargs.update({"channels": ",".join(channels)})
+    else:
+        kwargs.update({"channels": channels})
+    return self.api_call("usergroups.update", params=kwargs)
+
+ +
+
+def usergroups_users_list(self,
*,
usergroup: str,
include_disabled: bool | None = None,
team_id: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def usergroups_users_list(
+    self,
+    *,
+    usergroup: str,
+    include_disabled: Optional[bool] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """List all users in a User Group
+    https://docs.slack.dev/reference/methods/usergroups.users.list
+    """
+    kwargs.update(
+        {
+            "usergroup": usergroup,
+            "include_disabled": include_disabled,
+            "team_id": team_id,
+        }
+    )
+    return self.api_call("usergroups.users.list", http_verb="GET", params=kwargs)
+
+ +
+
+def usergroups_users_update(self,
*,
usergroup: str,
users: str | Sequence[str],
include_count: bool | None = None,
team_id: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def usergroups_users_update(
+    self,
+    *,
+    usergroup: str,
+    users: Union[str, Sequence[str]],
+    include_count: Optional[bool] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Update the list of users for a User Group
+    https://docs.slack.dev/reference/methods/usergroups.users.update
+    """
+    kwargs.update(
+        {
+            "usergroup": usergroup,
+            "include_count": include_count,
+            "team_id": team_id,
+        }
+    )
+    if isinstance(users, (list, tuple)):
+        kwargs.update({"users": ",".join(users)})
+    else:
+        kwargs.update({"users": users})
+    return self.api_call("usergroups.users.update", params=kwargs)
+
+

Update the list of users for a User Group +https://docs.slack.dev/reference/methods/usergroups.users.update

+
+
+def users_conversations(self,
*,
cursor: str | None = None,
exclude_archived: bool | None = None,
limit: int | None = None,
team_id: str | None = None,
types: str | Sequence[str] | None = None,
user: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def users_conversations(
+    self,
+    *,
+    cursor: Optional[str] = None,
+    exclude_archived: Optional[bool] = None,
+    limit: Optional[int] = None,
+    team_id: Optional[str] = None,
+    types: Optional[Union[str, Sequence[str]]] = None,
+    user: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """List conversations the calling user may access.
+    https://docs.slack.dev/reference/methods/users.conversations
+    """
+    kwargs.update(
+        {
+            "cursor": cursor,
+            "exclude_archived": exclude_archived,
+            "limit": limit,
+            "team_id": team_id,
+            "user": user,
+        }
+    )
+    if isinstance(types, (list, tuple)):
+        kwargs.update({"types": ",".join(types)})
+    else:
+        kwargs.update({"types": types})
+    return self.api_call("users.conversations", http_verb="GET", params=kwargs)
+
+

List conversations the calling user may access. +https://docs.slack.dev/reference/methods/users.conversations

+
+
+def users_deletePhoto(self, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def users_deletePhoto(
+    self,
+    **kwargs,
+) -> SlackResponse:
+    """Delete the user profile photo
+    https://docs.slack.dev/reference/methods/users.deletePhoto
+    """
+    return self.api_call("users.deletePhoto", http_verb="GET", params=kwargs)
+
+ +
+
+def users_discoverableContacts_lookup(self, email: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def users_discoverableContacts_lookup(
+    self,
+    email: str,
+    **kwargs,
+) -> SlackResponse:
+    """Lookup an email address to see if someone is on Slack
+    https://docs.slack.dev/reference/methods/users.discoverableContacts.lookup
+    """
+    kwargs.update({"email": email})
+    return self.api_call("users.discoverableContacts.lookup", params=kwargs)
+
+

Lookup an email address to see if someone is on Slack +https://docs.slack.dev/reference/methods/users.discoverableContacts.lookup

+
+
+def users_getPresence(self, *, user: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def users_getPresence(
+    self,
+    *,
+    user: str,
+    **kwargs,
+) -> SlackResponse:
+    """Gets user presence information.
+    https://docs.slack.dev/reference/methods/users.getPresence
+    """
+    kwargs.update({"user": user})
+    return self.api_call("users.getPresence", http_verb="GET", params=kwargs)
+
+ +
+
+def users_identity(self, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def users_identity(
+    self,
+    **kwargs,
+) -> SlackResponse:
+    """Get a user's identity.
+    https://docs.slack.dev/reference/methods/users.identity
+    """
+    return self.api_call("users.identity", http_verb="GET", params=kwargs)
+
+ +
+
+def users_info(self, *, user: str, include_locale: bool | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def users_info(
+    self,
+    *,
+    user: str,
+    include_locale: Optional[bool] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Gets information about a user.
+    https://docs.slack.dev/reference/methods/users.info
+    """
+    kwargs.update({"user": user, "include_locale": include_locale})
+    return self.api_call("users.info", http_verb="GET", params=kwargs)
+
+ +
+
+def users_list(self,
*,
cursor: str | None = None,
include_locale: bool | None = None,
limit: int | None = None,
team_id: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def users_list(
+    self,
+    *,
+    cursor: Optional[str] = None,
+    include_locale: Optional[bool] = None,
+    limit: Optional[int] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Lists all users in a Slack team.
+    https://docs.slack.dev/reference/methods/users.list
+    """
+    kwargs.update(
+        {
+            "cursor": cursor,
+            "include_locale": include_locale,
+            "limit": limit,
+            "team_id": team_id,
+        }
+    )
+    return self.api_call("users.list", http_verb="GET", params=kwargs)
+
+

Lists all users in a Slack team. +https://docs.slack.dev/reference/methods/users.list

+
+
+def users_lookupByEmail(self, *, email: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def users_lookupByEmail(
+    self,
+    *,
+    email: str,
+    **kwargs,
+) -> SlackResponse:
+    """Find a user with an email address.
+    https://docs.slack.dev/reference/methods/users.lookupByEmail
+    """
+    kwargs.update({"email": email})
+    return self.api_call("users.lookupByEmail", http_verb="GET", params=kwargs)
+
+ +
+
+def users_profile_get(self, *, user: str | None = None, include_labels: bool | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def users_profile_get(
+    self,
+    *,
+    user: Optional[str] = None,
+    include_labels: Optional[bool] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Retrieves a user's profile information.
+    https://docs.slack.dev/reference/methods/users.profile.get
+    """
+    kwargs.update({"user": user, "include_labels": include_labels})
+    return self.api_call("users.profile.get", http_verb="GET", params=kwargs)
+
+

Retrieves a user's profile information. +https://docs.slack.dev/reference/methods/users.profile.get

+
+
+def users_profile_set(self,
*,
name: str | None = None,
value: str | None = None,
user: str | None = None,
profile: Dict | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def users_profile_set(
+    self,
+    *,
+    name: Optional[str] = None,
+    value: Optional[str] = None,
+    user: Optional[str] = None,
+    profile: Optional[Dict] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Set the profile information for a user.
+    https://docs.slack.dev/reference/methods/users.profile.set
+    """
+    kwargs.update(
+        {
+            "name": name,
+            "profile": profile,
+            "user": user,
+            "value": value,
+        }
+    )
+    kwargs = _remove_none_values(kwargs)
+    # NOTE: Intentionally using json for the "profile" parameter
+    return self.api_call("users.profile.set", json=kwargs)
+
+

Set the profile information for a user. +https://docs.slack.dev/reference/methods/users.profile.set

+
+
+def users_setPhoto(self,
*,
image: str | io.IOBase,
crop_w: str | int | None = None,
crop_x: str | int | None = None,
crop_y: str | int | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def users_setPhoto(
+    self,
+    *,
+    image: Union[str, IOBase],
+    crop_w: Optional[Union[int, str]] = None,
+    crop_x: Optional[Union[int, str]] = None,
+    crop_y: Optional[Union[int, str]] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Set the user profile photo
+    https://docs.slack.dev/reference/methods/users.setPhoto
+    """
+    kwargs.update({"crop_w": crop_w, "crop_x": crop_x, "crop_y": crop_y})
+    return self.api_call("users.setPhoto", files={"image": image}, data=kwargs)
+
+ +
+
+def users_setPresence(self, *, presence: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def users_setPresence(
+    self,
+    *,
+    presence: str,
+    **kwargs,
+) -> SlackResponse:
+    """Manually sets user presence.
+    https://docs.slack.dev/reference/methods/users.setPresence
+    """
+    kwargs.update({"presence": presence})
+    return self.api_call("users.setPresence", params=kwargs)
+
+ +
+
+def views_open(self,
*,
trigger_id: str | None = None,
interactivity_pointer: str | None = None,
view: dict | View,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def views_open(
+    self,
+    *,
+    trigger_id: Optional[str] = None,
+    interactivity_pointer: Optional[str] = None,
+    view: Union[dict, View],
+    **kwargs,
+) -> SlackResponse:
+    """Open a view for a user.
+    https://docs.slack.dev/reference/methods/views.open
+    See https://docs.slack.dev/surfaces/modals/ for details.
+    """
+    kwargs.update({"trigger_id": trigger_id, "interactivity_pointer": interactivity_pointer})
+    if isinstance(view, View):
+        kwargs.update({"view": view.to_dict()})
+    else:
+        kwargs.update({"view": view})
+    kwargs = _remove_none_values(kwargs)
+    # NOTE: Intentionally using json for the "view" parameter
+    return self.api_call("views.open", json=kwargs)
+
+ +
+
+def views_publish(self,
*,
user_id: str,
view: dict | View,
hash: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def views_publish(
+    self,
+    *,
+    user_id: str,
+    view: Union[dict, View],
+    hash: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Publish a static view for a User.
+    Create or update the view that comprises an
+    app's Home tab (https://docs.slack.dev/surfaces/app-home/)
+    https://docs.slack.dev/reference/methods/views.publish
+    """
+    kwargs.update({"user_id": user_id, "hash": hash})
+    if isinstance(view, View):
+        kwargs.update({"view": view.to_dict()})
+    else:
+        kwargs.update({"view": view})
+    kwargs = _remove_none_values(kwargs)
+    # NOTE: Intentionally using json for the "view" parameter
+    return self.api_call("views.publish", json=kwargs)
+
+

Publish a static view for a User. +Create or update the view that comprises an +app's Home tab (https://docs.slack.dev/surfaces/app-home/) +https://docs.slack.dev/reference/methods/views.publish

+
+
+def views_push(self,
*,
trigger_id: str | None = None,
interactivity_pointer: str | None = None,
view: dict | View,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def views_push(
+    self,
+    *,
+    trigger_id: Optional[str] = None,
+    interactivity_pointer: Optional[str] = None,
+    view: Union[dict, View],
+    **kwargs,
+) -> SlackResponse:
+    """Push a view onto the stack of a root view.
+    Push a new view onto the existing view stack by passing a view
+    payload and a valid trigger_id generated from an interaction
+    within the existing modal.
+    Read the modals documentation (https://docs.slack.dev/surfaces/modals/)
+    to learn more about the lifecycle and intricacies of views.
+    https://docs.slack.dev/reference/methods/views.push
+    """
+    kwargs.update({"trigger_id": trigger_id, "interactivity_pointer": interactivity_pointer})
+    if isinstance(view, View):
+        kwargs.update({"view": view.to_dict()})
+    else:
+        kwargs.update({"view": view})
+    kwargs = _remove_none_values(kwargs)
+    # NOTE: Intentionally using json for the "view" parameter
+    return self.api_call("views.push", json=kwargs)
+
+

Push a view onto the stack of a root view. +Push a new view onto the existing view stack by passing a view +payload and a valid trigger_id generated from an interaction +within the existing modal. +Read the modals documentation (https://docs.slack.dev/surfaces/modals/) +to learn more about the lifecycle and intricacies of views. +https://docs.slack.dev/reference/methods/views.push

+
+
+def views_update(self,
*,
view: dict | View,
external_id: str | None = None,
view_id: str | None = None,
hash: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def views_update(
+    self,
+    *,
+    view: Union[dict, View],
+    external_id: Optional[str] = None,
+    view_id: Optional[str] = None,
+    hash: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Update an existing view.
+    Update a view by passing a new view definition along with the
+    view_id returned in views.open or the external_id.
+    See the modals documentation (https://docs.slack.dev/surfaces/modals/#updating_views)
+    to learn more about updating views and avoiding race conditions with the hash argument.
+    https://docs.slack.dev/reference/methods/views.update
+    """
+    if isinstance(view, View):
+        kwargs.update({"view": view.to_dict()})
+    else:
+        kwargs.update({"view": view})
+    if external_id:
+        kwargs.update({"external_id": external_id})
+    elif view_id:
+        kwargs.update({"view_id": view_id})
+    else:
+        raise e.SlackRequestError("Either view_id or external_id is required.")
+    kwargs.update({"hash": hash})
+    kwargs = _remove_none_values(kwargs)
+    # NOTE: Intentionally using json for the "view" parameter
+    return self.api_call("views.update", json=kwargs)
+
+

Update an existing view. +Update a view by passing a new view definition along with the +view_id returned in views.open or the external_id. +See the modals documentation (https://docs.slack.dev/surfaces/modals/#updating_views) +to learn more about updating views and avoiding race conditions with the hash argument. +https://docs.slack.dev/reference/methods/views.update

+
+ +
+
+ +Expand source code + +
def workflows_featured_add(
+    self,
+    *,
+    channel_id: str,
+    trigger_ids: Union[str, Sequence[str]],
+    **kwargs,
+) -> SlackResponse:
+    """Add featured workflows to a channel.
+    https://docs.slack.dev/reference/methods/workflows.featured.add
+    """
+    kwargs.update({"channel_id": channel_id})
+    if isinstance(trigger_ids, (list, tuple)):
+        kwargs.update({"trigger_ids": ",".join(trigger_ids)})
+    else:
+        kwargs.update({"trigger_ids": trigger_ids})
+    return self.api_call("workflows.featured.add", params=kwargs)
+
+ +
+ +
+
+ +Expand source code + +
def workflows_featured_list(
+    self,
+    *,
+    channel_ids: Union[str, Sequence[str]],
+    **kwargs,
+) -> SlackResponse:
+    """List the featured workflows for specified channels.
+    https://docs.slack.dev/reference/methods/workflows.featured.list
+    """
+    if isinstance(channel_ids, (list, tuple)):
+        kwargs.update({"channel_ids": ",".join(channel_ids)})
+    else:
+        kwargs.update({"channel_ids": channel_ids})
+    return self.api_call("workflows.featured.list", params=kwargs)
+
+

List the featured workflows for specified channels. +https://docs.slack.dev/reference/methods/workflows.featured.list

+
+ +
+
+ +Expand source code + +
def workflows_featured_remove(
+    self,
+    *,
+    channel_id: str,
+    trigger_ids: Union[str, Sequence[str]],
+    **kwargs,
+) -> SlackResponse:
+    """Remove featured workflows from a channel.
+    https://docs.slack.dev/reference/methods/workflows.featured.remove
+    """
+    kwargs.update({"channel_id": channel_id})
+    if isinstance(trigger_ids, (list, tuple)):
+        kwargs.update({"trigger_ids": ",".join(trigger_ids)})
+    else:
+        kwargs.update({"trigger_ids": trigger_ids})
+    return self.api_call("workflows.featured.remove", params=kwargs)
+
+ +
+ +
+
+ +Expand source code + +
def workflows_featured_set(
+    self,
+    *,
+    channel_id: str,
+    trigger_ids: Union[str, Sequence[str]],
+    **kwargs,
+) -> SlackResponse:
+    """Set featured workflows for a channel.
+    https://docs.slack.dev/reference/methods/workflows.featured.set
+    """
+    kwargs.update({"channel_id": channel_id})
+    if isinstance(trigger_ids, (list, tuple)):
+        kwargs.update({"trigger_ids": ",".join(trigger_ids)})
+    else:
+        kwargs.update({"trigger_ids": trigger_ids})
+    return self.api_call("workflows.featured.set", params=kwargs)
+
+ +
+
+def workflows_stepCompleted(self, *, workflow_step_execute_id: str, outputs: dict | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def workflows_stepCompleted(
+    self,
+    *,
+    workflow_step_execute_id: str,
+    outputs: Optional[dict] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Indicate a successful outcome of a workflow step's execution.
+    https://docs.slack.dev/reference/methods/workflows.stepCompleted
+    """
+    kwargs.update({"workflow_step_execute_id": workflow_step_execute_id})
+    if outputs is not None:
+        kwargs.update({"outputs": outputs})
+    kwargs = _remove_none_values(kwargs)
+    # NOTE: Intentionally using json for the "outputs" parameter
+    return self.api_call("workflows.stepCompleted", json=kwargs)
+
+

Indicate a successful outcome of a workflow step's execution. +https://docs.slack.dev/reference/methods/workflows.stepCompleted

+
+
+def workflows_stepFailed(self, *, workflow_step_execute_id: str, error: Dict[str, str], **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def workflows_stepFailed(
+    self,
+    *,
+    workflow_step_execute_id: str,
+    error: Dict[str, str],
+    **kwargs,
+) -> SlackResponse:
+    """Indicate an unsuccessful outcome of a workflow step's execution.
+    https://docs.slack.dev/reference/methods/workflows.stepFailed
+    """
+    kwargs.update(
+        {
+            "workflow_step_execute_id": workflow_step_execute_id,
+            "error": error,
+        }
+    )
+    kwargs = _remove_none_values(kwargs)
+    # NOTE: Intentionally using json for the "error" parameter
+    return self.api_call("workflows.stepFailed", json=kwargs)
+
+

Indicate an unsuccessful outcome of a workflow step's execution. +https://docs.slack.dev/reference/methods/workflows.stepFailed

+
+
+def workflows_updateStep(self,
*,
workflow_step_edit_id: str,
inputs: Dict[str, Any] | None = None,
outputs: List[Dict[str, str]] | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def workflows_updateStep(
+    self,
+    *,
+    workflow_step_edit_id: str,
+    inputs: Optional[Dict[str, Any]] = None,
+    outputs: Optional[List[Dict[str, str]]] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Update the configuration for a workflow extension step.
+    https://docs.slack.dev/reference/methods/workflows.updateStep
+    """
+    kwargs.update({"workflow_step_edit_id": workflow_step_edit_id})
+    if inputs is not None:
+        kwargs.update({"inputs": inputs})
+    if outputs is not None:
+        kwargs.update({"outputs": outputs})
+    kwargs = _remove_none_values(kwargs)
+    # NOTE: Intentionally using json for the "inputs" / "outputs" parameters
+    return self.api_call("workflows.updateStep", json=kwargs)
+
+

Update the configuration for a workflow extension step. +https://docs.slack.dev/reference/methods/workflows.updateStep

+
+
+

Inherited members

+ +
+
+
+
+ +
+ + + diff --git a/docs/reference/web/deprecation.html b/docs/reference/web/deprecation.html new file mode 100644 index 000000000..0b68d0c84 --- /dev/null +++ b/docs/reference/web/deprecation.html @@ -0,0 +1,121 @@ + + + + + + +slack_sdk.web.deprecation API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.web.deprecation

+
+
+
+
+
+
+
+
+

Functions

+
+
+def show_deprecation_warning_if_any(method_name: str) +
+
+
+ +Expand source code + +
def show_deprecation_warning_if_any(method_name: str):
+    """Prints a warning if the given method is deprecated"""
+
+    skip_deprecation = os.environ.get("SLACKCLIENT_SKIP_DEPRECATION")  # for unit tests etc.
+    if skip_deprecation:
+        return
+    if not method_name:
+        return
+
+    # 2020/01 conversations API deprecation
+    matched_prefixes = [prefix for prefix in deprecated_method_prefixes_2020_01 if method_name.startswith(prefix)]
+    if len(matched_prefixes) > 0:
+        message = (
+            f"{method_name} is deprecated. Please use the Conversations API instead. "
+            "For more info, go to "
+            "https://docs.slack.dev/changelog/2020-01-deprecating-antecedents-to-the-conversations-api/"
+        )
+        warnings.warn(message)
+
+    # 2023/07 stars API deprecation
+    matched_prefixes = [prefix for prefix in deprecated_method_prefixes_2023_07 if method_name.startswith(prefix)]
+    if len(matched_prefixes) > 0:
+        message = (
+            f"{method_name} is deprecated. For more info, go to "
+            "https://docs.slack.dev/changelog/2023-07-its-later-already-for-stars-and-reminders/"
+        )
+        warnings.warn(message)
+
+    # 2024/09 workflow steps API deprecation
+    matched_prefixes = [prefix for prefix in deprecated_method_prefixes_2024_09 if method_name.startswith(prefix)]
+    if len(matched_prefixes) > 0:
+        message = (
+            f"{method_name} is deprecated. For more info, go to "
+            "https://docs.slack.dev/changelog/2023-08-workflow-steps-from-apps-step-back/"
+        )
+        warnings.warn(message)
+
+

Prints a warning if the given method is deprecated

+
+
+
+
+
+
+ +
+ + + diff --git a/docs/reference/web/file_upload_v2_result.html b/docs/reference/web/file_upload_v2_result.html new file mode 100644 index 000000000..6cd74fcb3 --- /dev/null +++ b/docs/reference/web/file_upload_v2_result.html @@ -0,0 +1,110 @@ + + + + + + +slack_sdk.web.file_upload_v2_result API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.web.file_upload_v2_result

+
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class FileUploadV2Result +(status: int, body: str) +
+
+
+ +Expand source code + +
class FileUploadV2Result:
+    status: int
+    body: str
+
+    def __init__(self, status: int, body: str):
+        self.status = status
+        self.body = body
+
+
+

Class variables

+
+
var body : str
+
+

The type of the None singleton.

+
+
var status : int
+
+

The type of the None singleton.

+
+
+
+
+
+
+ +
+ + + diff --git a/docs/reference/web/index.html b/docs/reference/web/index.html new file mode 100644 index 000000000..611a26b3b --- /dev/null +++ b/docs/reference/web/index.html @@ -0,0 +1,16182 @@ + + + + + + +slack_sdk.web API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.web

+
+
+

The Slack Web API allows you to build applications that interact with Slack +in more complex ways than the integrations we provide out of the box.

+
+
+

Sub-modules

+
+
slack_sdk.web.async_base_client
+
+
+
+
slack_sdk.web.async_chat_stream
+
+
+
+
slack_sdk.web.async_client
+
+

A Python module for interacting with Slack's Web API.

+
+
slack_sdk.web.async_internal_utils
+
+
+
+
slack_sdk.web.async_slack_response
+
+

A Python module for interacting and consuming responses from Slack.

+
+
slack_sdk.web.base_client
+
+

A Python module for interacting with Slack's Web API.

+
+
slack_sdk.web.chat_stream
+
+
+
+
slack_sdk.web.client
+
+

A Python module for interacting with Slack's Web API.

+
+
slack_sdk.web.deprecation
+
+
+
+
slack_sdk.web.file_upload_v2_result
+
+
+
+
slack_sdk.web.internal_utils
+
+
+
+
slack_sdk.web.legacy_base_client
+
+

A Python module for interacting with Slack's Web API.

+
+
slack_sdk.web.legacy_client
+
+
+
+
slack_sdk.web.legacy_slack_response
+
+

A Python module for interacting and consuming responses from Slack.

+
+
slack_sdk.web.slack_response
+
+

A Python module for interacting and consuming responses from Slack.

+
+
+
+
+
+
+
+
+

Classes

+
+
+class SlackResponse +(*,
client,
http_verb: str,
api_url: str,
req_args: dict,
data: dict | bytes,
headers: dict,
status_code: int)
+
+
+
+ +Expand source code + +
class SlackResponse:
+    """An iterable container of response data.
+
+    Attributes:
+        data (dict): The json-encoded content of the response. Along
+            with the headers and status code information.
+
+    Methods:
+        validate: Check if the response from Slack was successful.
+        get: Retrieves any key from the response data.
+        next: Retrieves the next portion of results,
+            if 'next_cursor' is present.
+
+    Example:
+    ```python
+    import os
+    import slack
+
+    client = slack.WebClient(token=os.environ['SLACK_API_TOKEN'])
+
+    response1 = client.auth_revoke(test='true')
+    assert not response1['revoked']
+
+    response2 = client.auth_test()
+    assert response2.get('ok', False)
+
+    users = []
+    for page in client.users_list(limit=2):
+        users = users + page['members']
+    ```
+
+    Note:
+        Some responses return collections of information
+        like channel and user lists. If they do it's likely
+        that you'll only receive a portion of results. This
+        object allows you to iterate over the response which
+        makes subsequent API requests until your code hits
+        'break' or there are no more results to be found.
+
+        Any attributes or methods prefixed with _underscores are
+        intended to be "private" internal use only. They may be changed or
+        removed at anytime.
+    """
+
+    def __init__(
+        self,
+        *,
+        client,
+        http_verb: str,
+        api_url: str,
+        req_args: dict,
+        data: Union[dict, bytes],  # data can be binary data
+        headers: dict,
+        status_code: int,
+    ):
+        self.http_verb = http_verb
+        self.api_url = api_url
+        self.req_args = req_args
+        self.data = data
+        self.headers = headers
+        self.status_code = status_code
+        self._initial_data = data
+        self._iteration = None  # for __iter__ & __next__
+        self._client = client
+        self._logger = logging.getLogger(__name__)
+
+    def __str__(self):
+        """Return the Response data if object is converted to a string."""
+        if isinstance(self.data, bytes):
+            raise ValueError("As the response.data is binary data, this operation is unsupported")
+        return f"{self.data}"
+
+    def __contains__(self, key: str) -> bool:
+        return self.get(key) is not None
+
+    def __getitem__(self, key):
+        """Retrieves any key from the data store.
+
+        Note:
+            This is implemented so users can reference the
+            SlackResponse object like a dictionary.
+            e.g. response["ok"]
+
+        Returns:
+            The value from data or None.
+        """
+        if isinstance(self.data, bytes):
+            raise ValueError("As the response.data is binary data, this operation is unsupported")
+        if self.data is None:
+            raise ValueError("As the response.data is empty, this operation is unsupported")
+        return self.data.get(key, None)
+
+    def __iter__(self):
+        """Enables the ability to iterate over the response.
+        It's required for the iterator protocol.
+
+        Note:
+            This enables Slack cursor-based pagination.
+
+        Returns:
+            (SlackResponse) self
+        """
+        self._iteration = 0
+        self.data = self._initial_data
+        return self
+
+    def __next__(self):
+        """Retrieves the next portion of results, if 'next_cursor' is present.
+
+        Note:
+            Some responses return collections of information
+            like channel and user lists. If they do it's likely
+            that you'll only receive a portion of results. This
+            method allows you to iterate over the response until
+            your code hits 'break' or there are no more results
+            to be found.
+
+        Returns:
+            (SlackResponse) self
+                With the new response data now attached to this object.
+
+        Raises:
+            SlackApiError: If the request to the Slack API failed.
+            StopIteration: If 'next_cursor' is not present or empty.
+        """
+        if isinstance(self.data, bytes):
+            raise ValueError("As the response.data is binary data, this operation is unsupported")
+        self._iteration += 1
+        if self._iteration == 1:
+            return self
+        if _next_cursor_is_present(self.data):
+            params = self.req_args.get("params", {})
+            if params is None:
+                params = {}
+            next_cursor = self.data.get("response_metadata", {}).get("next_cursor") or self.data.get("next_cursor")
+            params.update({"cursor": next_cursor})
+            self.req_args.update({"params": params})
+
+            # This method sends a request in a synchronous way
+            response = self._client._request_for_pagination(api_url=self.api_url, req_args=self.req_args)
+            self.data = response["data"]
+            self.headers = response["headers"]
+            self.status_code = response["status_code"]
+            return self.validate()
+        else:
+            raise StopIteration
+
+    @overload
+    def get(self, key: str, default: None = None) -> Optional[Any]:
+        ...
+
+    @overload
+    def get(self, key: str, default: T) -> T:
+        ...
+
+    def get(self, key, default=None):
+        """Retrieves any key from the response data.
+
+        Note:
+            This is implemented so users can reference the
+            SlackResponse object like a dictionary.
+            e.g. response.get("ok", False)
+
+        Returns:
+            The value from data or the specified default.
+        """
+        if isinstance(self.data, bytes):
+            raise ValueError("As the response.data is binary data, this operation is unsupported")
+        if self.data is None:
+            return None
+        return self.data.get(key, default)
+
+    def validate(self):
+        """Check if the response from Slack was successful.
+
+        Returns:
+            (SlackResponse)
+                This method returns it's own object. e.g. 'self'
+
+        Raises:
+            SlackApiError: The request to the Slack API failed.
+        """
+        if self.status_code == 200 and self.data and (isinstance(self.data, bytes) or self.data.get("ok", False)):
+            return self
+        msg = f"The request to the Slack API failed. (url: {self.api_url})"
+        raise e.SlackApiError(message=msg, response=self)
+
+

An iterable container of response data.

+

Attributes

+
+
data : dict
+
The json-encoded content of the response. Along +with the headers and status code information.
+
+

Methods

+

validate: Check if the response from Slack was successful. +get: Retrieves any key from the response data. +next: Retrieves the next portion of results, +if 'next_cursor' is present.

+

Example:

+
import os
+import slack
+
+client = slack.WebClient(token=os.environ['SLACK_API_TOKEN'])
+
+response1 = client.auth_revoke(test='true')
+assert not response1['revoked']
+
+response2 = client.auth_test()
+assert response2.get('ok', False)
+
+users = []
+for page in client.users_list(limit=2):
+    users = users + page['members']
+
+

Note

+

Some responses return collections of information +like channel and user lists. If they do it's likely +that you'll only receive a portion of results. This +object allows you to iterate over the response which +makes subsequent API requests until your code hits +'break' or there are no more results to be found.

+

Any attributes or methods prefixed with _underscores are +intended to be "private" internal use only. They may be changed or +removed at anytime.

+

Methods

+
+
+def get(self, key, default=None) +
+
+
+ +Expand source code + +
def get(self, key, default=None):
+    """Retrieves any key from the response data.
+
+    Note:
+        This is implemented so users can reference the
+        SlackResponse object like a dictionary.
+        e.g. response.get("ok", False)
+
+    Returns:
+        The value from data or the specified default.
+    """
+    if isinstance(self.data, bytes):
+        raise ValueError("As the response.data is binary data, this operation is unsupported")
+    if self.data is None:
+        return None
+    return self.data.get(key, default)
+
+

Retrieves any key from the response data.

+

Note

+

This is implemented so users can reference the +SlackResponse object like a dictionary. +e.g. response.get("ok", False)

+

Returns

+

The value from data or the specified default.

+
+
+def validate(self) +
+
+
+ +Expand source code + +
def validate(self):
+    """Check if the response from Slack was successful.
+
+    Returns:
+        (SlackResponse)
+            This method returns it's own object. e.g. 'self'
+
+    Raises:
+        SlackApiError: The request to the Slack API failed.
+    """
+    if self.status_code == 200 and self.data and (isinstance(self.data, bytes) or self.data.get("ok", False)):
+        return self
+    msg = f"The request to the Slack API failed. (url: {self.api_url})"
+    raise e.SlackApiError(message=msg, response=self)
+
+

Check if the response from Slack was successful.

+

Returns

+

(SlackResponse) +This method returns it's own object. e.g. 'self'

+

Raises

+
+
SlackApiError
+
The request to the Slack API failed.
+
+
+
+
+
+class WebClient +(token: str | None = None,
base_url: str = 'https://slack.com/api/',
timeout: int = 30,
ssl: ssl.SSLContext | None = None,
proxy: str | None = None,
headers: dict | None = None,
user_agent_prefix: str | None = None,
user_agent_suffix: str | None = None,
team_id: str | None = None,
logger: logging.Logger | None = None,
retry_handlers: List[RetryHandler] | None = None)
+
+
+
+ +Expand source code + +
class WebClient(BaseClient):
+    """A WebClient allows apps to communicate with the Slack Platform's Web API.
+
+    https://docs.slack.dev/reference/methods
+
+    The Slack Web API is an interface for querying information from
+    and enacting change in a Slack workspace.
+
+    This client handles constructing and sending HTTP requests to Slack
+    as well as parsing any responses received into a `SlackResponse`.
+
+    Attributes:
+        token (str): A string specifying an `xoxp-*` or `xoxb-*` token.
+        base_url (str): A string representing the Slack API base URL.
+            Default is `'https://slack.com/api/'`
+        timeout (int): The maximum number of seconds the client will wait
+            to connect and receive a response from Slack.
+            Default is 30 seconds.
+        ssl (SSLContext): An [`ssl.SSLContext`][1] instance, helpful for specifying
+            your own custom certificate chain.
+        proxy (str): String representing a fully-qualified URL to a proxy through
+            which to route all requests to the Slack API. Even if this parameter
+            is not specified, if any of the following environment variables are
+            present, they will be loaded into this parameter: `HTTPS_PROXY`,
+            `https_proxy`, `HTTP_PROXY` or `http_proxy`.
+        headers (dict): Additional request headers to attach to all requests.
+
+    Methods:
+        `api_call`: Constructs a request and executes the API call to Slack.
+
+    Example of recommended usage:
+    ```python
+        import os
+        from slack_sdk import WebClient
+
+        client = WebClient(token=os.environ['SLACK_API_TOKEN'])
+        response = client.chat_postMessage(
+            channel='#random',
+            text="Hello world!")
+        assert response["ok"]
+        assert response["message"]["text"] == "Hello world!"
+    ```
+
+    Example manually creating an API request:
+    ```python
+        import os
+        from slack_sdk import WebClient
+
+        client = WebClient(token=os.environ['SLACK_API_TOKEN'])
+        response = client.api_call(
+            api_method='chat.postMessage',
+            json={'channel': '#random','text': "Hello world!"}
+        )
+        assert response["ok"]
+        assert response["message"]["text"] == "Hello world!"
+    ```
+
+    Note:
+        Any attributes or methods prefixed with _underscores are
+        intended to be "private" internal use only. They may be changed or
+        removed at anytime.
+
+    [1]: https://docs.python.org/3/library/ssl.html#ssl.SSLContext
+    """
+
+    def admin_analytics_getFile(
+        self,
+        *,
+        type: str,
+        date: Optional[str] = None,
+        metadata_only: Optional[bool] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Retrieve analytics data for a given date, presented as a compressed JSON file
+        https://docs.slack.dev/reference/methods/admin.analytics.getFile
+        """
+        kwargs.update({"type": type})
+        if date is not None:
+            kwargs.update({"date": date})
+        if metadata_only is not None:
+            kwargs.update({"metadata_only": metadata_only})
+        return self.api_call("admin.analytics.getFile", params=kwargs)
+
+    def admin_apps_approve(
+        self,
+        *,
+        app_id: Optional[str] = None,
+        request_id: Optional[str] = None,
+        enterprise_id: Optional[str] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Approve an app for installation on a workspace.
+        Either app_id or request_id is required.
+        These IDs can be obtained either directly via the app_requested event,
+        or by the admin.apps.requests.list method.
+        https://docs.slack.dev/reference/methods/admin.apps.approve
+        """
+        if app_id:
+            kwargs.update({"app_id": app_id})
+        elif request_id:
+            kwargs.update({"request_id": request_id})
+        else:
+            raise e.SlackRequestError("The app_id or request_id argument must be specified.")
+
+        kwargs.update(
+            {
+                "enterprise_id": enterprise_id,
+                "team_id": team_id,
+            }
+        )
+        return self.api_call("admin.apps.approve", params=kwargs)
+
+    def admin_apps_approved_list(
+        self,
+        *,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        enterprise_id: Optional[str] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """List approved apps for an org or workspace.
+        https://docs.slack.dev/reference/methods/admin.apps.approved.list
+        """
+        kwargs.update(
+            {
+                "cursor": cursor,
+                "limit": limit,
+                "enterprise_id": enterprise_id,
+                "team_id": team_id,
+            }
+        )
+        return self.api_call("admin.apps.approved.list", http_verb="GET", params=kwargs)
+
+    def admin_apps_clearResolution(
+        self,
+        *,
+        app_id: str,
+        enterprise_id: Optional[str] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Clear an app resolution
+        https://docs.slack.dev/reference/methods/admin.apps.clearResolution
+        """
+        kwargs.update(
+            {
+                "app_id": app_id,
+                "enterprise_id": enterprise_id,
+                "team_id": team_id,
+            }
+        )
+        return self.api_call("admin.apps.clearResolution", http_verb="POST", params=kwargs)
+
+    def admin_apps_requests_cancel(
+        self,
+        *,
+        request_id: str,
+        enterprise_id: Optional[str] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """List app requests for a team/workspace.
+        https://docs.slack.dev/reference/methods/admin.apps.requests.cancel
+        """
+        kwargs.update(
+            {
+                "request_id": request_id,
+                "enterprise_id": enterprise_id,
+                "team_id": team_id,
+            }
+        )
+        return self.api_call("admin.apps.requests.cancel", http_verb="POST", params=kwargs)
+
+    def admin_apps_requests_list(
+        self,
+        *,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """List app requests for a team/workspace.
+        https://docs.slack.dev/reference/methods/admin.apps.requests.list
+        """
+        kwargs.update(
+            {
+                "cursor": cursor,
+                "limit": limit,
+                "team_id": team_id,
+            }
+        )
+        return self.api_call("admin.apps.requests.list", http_verb="GET", params=kwargs)
+
+    def admin_apps_restrict(
+        self,
+        *,
+        app_id: Optional[str] = None,
+        request_id: Optional[str] = None,
+        enterprise_id: Optional[str] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Restrict an app for installation on a workspace.
+        Exactly one of the team_id or enterprise_id arguments is required, not both.
+        Either app_id or request_id is required. These IDs can be obtained either directly
+        via the app_requested event, or by the admin.apps.requests.list method.
+        https://docs.slack.dev/reference/methods/admin.apps.restrict
+        """
+        if app_id:
+            kwargs.update({"app_id": app_id})
+        elif request_id:
+            kwargs.update({"request_id": request_id})
+        else:
+            raise e.SlackRequestError("The app_id or request_id argument must be specified.")
+
+        kwargs.update(
+            {
+                "enterprise_id": enterprise_id,
+                "team_id": team_id,
+            }
+        )
+        return self.api_call("admin.apps.restrict", params=kwargs)
+
+    def admin_apps_restricted_list(
+        self,
+        *,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        enterprise_id: Optional[str] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """List restricted apps for an org or workspace.
+        https://docs.slack.dev/reference/methods/admin.apps.restricted.list
+        """
+        kwargs.update(
+            {
+                "cursor": cursor,
+                "limit": limit,
+                "enterprise_id": enterprise_id,
+                "team_id": team_id,
+            }
+        )
+        return self.api_call("admin.apps.restricted.list", http_verb="GET", params=kwargs)
+
+    def admin_apps_uninstall(
+        self,
+        *,
+        app_id: str,
+        enterprise_id: Optional[str] = None,
+        team_ids: Optional[Union[str, Sequence[str]]] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Uninstall an app from one or many workspaces, or an entire enterprise organization.
+        With an org-level token, enterprise_id or team_ids is required.
+        https://docs.slack.dev/reference/methods/admin.apps.uninstall
+        """
+        kwargs.update({"app_id": app_id})
+        if enterprise_id is not None:
+            kwargs.update({"enterprise_id": enterprise_id})
+        if team_ids is not None:
+            if isinstance(team_ids, (list, tuple)):
+                kwargs.update({"team_ids": ",".join(team_ids)})
+            else:
+                kwargs.update({"team_ids": team_ids})
+        return self.api_call("admin.apps.uninstall", http_verb="POST", params=kwargs)
+
+    def admin_apps_activities_list(
+        self,
+        *,
+        app_id: Optional[str] = None,
+        component_id: Optional[str] = None,
+        component_type: Optional[str] = None,
+        log_event_type: Optional[str] = None,
+        max_date_created: Optional[int] = None,
+        min_date_created: Optional[int] = None,
+        min_log_level: Optional[str] = None,
+        sort_direction: Optional[str] = None,
+        source: Optional[str] = None,
+        team_id: Optional[str] = None,
+        trace_id: Optional[str] = None,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Get logs for a specified team/org
+        https://docs.slack.dev/reference/methods/admin.apps.activities.list
+        """
+        kwargs.update(
+            {
+                "app_id": app_id,
+                "component_id": component_id,
+                "component_type": component_type,
+                "log_event_type": log_event_type,
+                "max_date_created": max_date_created,
+                "min_date_created": min_date_created,
+                "min_log_level": min_log_level,
+                "sort_direction": sort_direction,
+                "source": source,
+                "team_id": team_id,
+                "trace_id": trace_id,
+                "cursor": cursor,
+                "limit": limit,
+            }
+        )
+        return self.api_call("admin.apps.activities.list", params=kwargs)
+
+    def admin_apps_config_lookup(
+        self,
+        *,
+        app_ids: Union[str, Sequence[str]],
+        **kwargs,
+    ) -> SlackResponse:
+        """Look up the app config for connectors by their IDs
+        https://docs.slack.dev/reference/methods/admin.apps.config.lookup
+        """
+        if isinstance(app_ids, (list, tuple)):
+            kwargs.update({"app_ids": ",".join(app_ids)})
+        else:
+            kwargs.update({"app_ids": app_ids})
+        return self.api_call("admin.apps.config.lookup", params=kwargs)
+
+    def admin_apps_config_set(
+        self,
+        *,
+        app_id: str,
+        domain_restrictions: Optional[Dict[str, Any]] = None,
+        workflow_auth_strategy: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Set the app config for a connector
+        https://docs.slack.dev/reference/methods/admin.apps.config.set
+        """
+        kwargs.update(
+            {
+                "app_id": app_id,
+                "workflow_auth_strategy": workflow_auth_strategy,
+            }
+        )
+        if domain_restrictions is not None:
+            kwargs.update({"domain_restrictions": json.dumps(domain_restrictions)})
+        return self.api_call("admin.apps.config.set", params=kwargs)
+
+    def admin_auth_policy_getEntities(
+        self,
+        *,
+        policy_name: str,
+        cursor: Optional[str] = None,
+        entity_type: Optional[str] = None,
+        limit: Optional[int] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Fetch all the entities assigned to a particular authentication policy by name.
+        https://docs.slack.dev/reference/methods/admin.auth.policy.getEntities
+        """
+        kwargs.update({"policy_name": policy_name})
+        if cursor is not None:
+            kwargs.update({"cursor": cursor})
+        if entity_type is not None:
+            kwargs.update({"entity_type": entity_type})
+        if limit is not None:
+            kwargs.update({"limit": limit})
+        return self.api_call("admin.auth.policy.getEntities", http_verb="POST", params=kwargs)
+
+    def admin_auth_policy_assignEntities(
+        self,
+        *,
+        entity_ids: Union[str, Sequence[str]],
+        policy_name: str,
+        entity_type: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Assign entities to a particular authentication policy.
+        https://docs.slack.dev/reference/methods/admin.auth.policy.assignEntities
+        """
+        if isinstance(entity_ids, (list, tuple)):
+            kwargs.update({"entity_ids": ",".join(entity_ids)})
+        else:
+            kwargs.update({"entity_ids": entity_ids})
+        kwargs.update({"policy_name": policy_name})
+        kwargs.update({"entity_type": entity_type})
+        return self.api_call("admin.auth.policy.assignEntities", http_verb="POST", params=kwargs)
+
+    def admin_auth_policy_removeEntities(
+        self,
+        *,
+        entity_ids: Union[str, Sequence[str]],
+        policy_name: str,
+        entity_type: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Remove specified entities from a specified authentication policy.
+        https://docs.slack.dev/reference/methods/admin.auth.policy.removeEntities
+        """
+        if isinstance(entity_ids, (list, tuple)):
+            kwargs.update({"entity_ids": ",".join(entity_ids)})
+        else:
+            kwargs.update({"entity_ids": entity_ids})
+        kwargs.update({"policy_name": policy_name})
+        kwargs.update({"entity_type": entity_type})
+        return self.api_call("admin.auth.policy.removeEntities", http_verb="POST", params=kwargs)
+
+    def admin_conversations_createForObjects(
+        self,
+        *,
+        object_id: str,
+        salesforce_org_id: str,
+        invite_object_team: Optional[bool] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Create a Salesforce channel for the corresponding object provided.
+        https://docs.slack.dev/reference/methods/admin.conversations.createForObjects
+        """
+        kwargs.update(
+            {"object_id": object_id, "salesforce_org_id": salesforce_org_id, "invite_object_team": invite_object_team}
+        )
+        return self.api_call("admin.conversations.createForObjects", params=kwargs)
+
+    def admin_conversations_linkObjects(
+        self,
+        *,
+        channel: str,
+        record_id: str,
+        salesforce_org_id: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Link a Salesforce record to a channel.
+        https://docs.slack.dev/reference/methods/admin.conversations.linkObjects
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "record_id": record_id,
+                "salesforce_org_id": salesforce_org_id,
+            }
+        )
+        return self.api_call("admin.conversations.linkObjects", params=kwargs)
+
+    def admin_conversations_unlinkObjects(
+        self,
+        *,
+        channel: str,
+        new_name: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Unlink a Salesforce record from a channel.
+        https://docs.slack.dev/reference/methods/admin.conversations.unlinkObjects
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "new_name": new_name,
+            }
+        )
+        return self.api_call("admin.conversations.unlinkObjects", params=kwargs)
+
+    def admin_barriers_create(
+        self,
+        *,
+        barriered_from_usergroup_ids: Union[str, Sequence[str]],
+        primary_usergroup_id: str,
+        restricted_subjects: Union[str, Sequence[str]],
+        **kwargs,
+    ) -> SlackResponse:
+        """Create an Information Barrier
+        https://docs.slack.dev/reference/methods/admin.barriers.create
+        """
+        kwargs.update({"primary_usergroup_id": primary_usergroup_id})
+        if isinstance(barriered_from_usergroup_ids, (list, tuple)):
+            kwargs.update({"barriered_from_usergroup_ids": ",".join(barriered_from_usergroup_ids)})
+        else:
+            kwargs.update({"barriered_from_usergroup_ids": barriered_from_usergroup_ids})
+        if isinstance(restricted_subjects, (list, tuple)):
+            kwargs.update({"restricted_subjects": ",".join(restricted_subjects)})
+        else:
+            kwargs.update({"restricted_subjects": restricted_subjects})
+        return self.api_call("admin.barriers.create", http_verb="POST", params=kwargs)
+
+    def admin_barriers_delete(
+        self,
+        *,
+        barrier_id: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Delete an existing Information Barrier
+        https://docs.slack.dev/reference/methods/admin.barriers.delete
+        """
+        kwargs.update({"barrier_id": barrier_id})
+        return self.api_call("admin.barriers.delete", http_verb="POST", params=kwargs)
+
+    def admin_barriers_update(
+        self,
+        *,
+        barrier_id: str,
+        barriered_from_usergroup_ids: Union[str, Sequence[str]],
+        primary_usergroup_id: str,
+        restricted_subjects: Union[str, Sequence[str]],
+        **kwargs,
+    ) -> SlackResponse:
+        """Update an existing Information Barrier
+        https://docs.slack.dev/reference/methods/admin.barriers.update
+        """
+        kwargs.update({"barrier_id": barrier_id, "primary_usergroup_id": primary_usergroup_id})
+        if isinstance(barriered_from_usergroup_ids, (list, tuple)):
+            kwargs.update({"barriered_from_usergroup_ids": ",".join(barriered_from_usergroup_ids)})
+        else:
+            kwargs.update({"barriered_from_usergroup_ids": barriered_from_usergroup_ids})
+        if isinstance(restricted_subjects, (list, tuple)):
+            kwargs.update({"restricted_subjects": ",".join(restricted_subjects)})
+        else:
+            kwargs.update({"restricted_subjects": restricted_subjects})
+        return self.api_call("admin.barriers.update", http_verb="POST", params=kwargs)
+
+    def admin_barriers_list(
+        self,
+        *,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Get all Information Barriers for your organization
+        https://docs.slack.dev/reference/methods/admin.barriers.list"""
+        kwargs.update(
+            {
+                "cursor": cursor,
+                "limit": limit,
+            }
+        )
+        return self.api_call("admin.barriers.list", http_verb="GET", params=kwargs)
+
+    def admin_conversations_create(
+        self,
+        *,
+        is_private: bool,
+        name: str,
+        description: Optional[str] = None,
+        org_wide: Optional[bool] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Create a public or private channel-based conversation.
+        https://docs.slack.dev/reference/methods/admin.conversations.create
+        """
+        kwargs.update(
+            {
+                "is_private": is_private,
+                "name": name,
+                "description": description,
+                "org_wide": org_wide,
+                "team_id": team_id,
+            }
+        )
+        return self.api_call("admin.conversations.create", params=kwargs)
+
+    def admin_conversations_delete(
+        self,
+        *,
+        channel_id: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Delete a public or private channel.
+        https://docs.slack.dev/reference/methods/admin.conversations.delete
+        """
+        kwargs.update({"channel_id": channel_id})
+        return self.api_call("admin.conversations.delete", params=kwargs)
+
+    def admin_conversations_invite(
+        self,
+        *,
+        channel_id: str,
+        user_ids: Union[str, Sequence[str]],
+        **kwargs,
+    ) -> SlackResponse:
+        """Invite a user to a public or private channel.
+        https://docs.slack.dev/reference/methods/admin.conversations.invite
+        """
+        kwargs.update({"channel_id": channel_id})
+        if isinstance(user_ids, (list, tuple)):
+            kwargs.update({"user_ids": ",".join(user_ids)})
+        else:
+            kwargs.update({"user_ids": user_ids})
+        # NOTE: the endpoint is unable to handle Content-Type: application/json as of Sep 3, 2020.
+        return self.api_call("admin.conversations.invite", params=kwargs)
+
+    def admin_conversations_archive(
+        self,
+        *,
+        channel_id: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Archive a public or private channel.
+        https://docs.slack.dev/reference/methods/admin.conversations.archive
+        """
+        kwargs.update({"channel_id": channel_id})
+        return self.api_call("admin.conversations.archive", params=kwargs)
+
+    def admin_conversations_unarchive(
+        self,
+        *,
+        channel_id: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Unarchive a public or private channel.
+        https://docs.slack.dev/reference/methods/admin.conversations.archive
+        """
+        kwargs.update({"channel_id": channel_id})
+        return self.api_call("admin.conversations.unarchive", params=kwargs)
+
+    def admin_conversations_rename(
+        self,
+        *,
+        channel_id: str,
+        name: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Rename a public or private channel.
+        https://docs.slack.dev/reference/methods/admin.conversations.rename
+        """
+        kwargs.update({"channel_id": channel_id, "name": name})
+        return self.api_call("admin.conversations.rename", params=kwargs)
+
+    def admin_conversations_search(
+        self,
+        *,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        query: Optional[str] = None,
+        search_channel_types: Optional[Union[str, Sequence[str]]] = None,
+        sort: Optional[str] = None,
+        sort_dir: Optional[str] = None,
+        team_ids: Optional[Union[str, Sequence[str]]] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Search for public or private channels in an Enterprise organization.
+        https://docs.slack.dev/reference/methods/admin.conversations.search
+        """
+        kwargs.update(
+            {
+                "cursor": cursor,
+                "limit": limit,
+                "query": query,
+                "sort": sort,
+                "sort_dir": sort_dir,
+            }
+        )
+
+        if isinstance(search_channel_types, (list, tuple)):
+            kwargs.update({"search_channel_types": ",".join(search_channel_types)})
+        else:
+            kwargs.update({"search_channel_types": search_channel_types})
+
+        if isinstance(team_ids, (list, tuple)):
+            kwargs.update({"team_ids": ",".join(team_ids)})
+        else:
+            kwargs.update({"team_ids": team_ids})
+
+        return self.api_call("admin.conversations.search", params=kwargs)
+
+    def admin_conversations_convertToPrivate(
+        self,
+        *,
+        channel_id: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Convert a public channel to a private channel.
+        https://docs.slack.dev/reference/methods/admin.conversations.convertToPrivate
+        """
+        kwargs.update({"channel_id": channel_id})
+        return self.api_call("admin.conversations.convertToPrivate", params=kwargs)
+
+    def admin_conversations_convertToPublic(
+        self,
+        *,
+        channel_id: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Convert a privte channel to a public channel.
+        https://docs.slack.dev/reference/methods/admin.conversations.convertToPublic
+        """
+        kwargs.update({"channel_id": channel_id})
+        return self.api_call("admin.conversations.convertToPublic", params=kwargs)
+
+    def admin_conversations_setConversationPrefs(
+        self,
+        *,
+        channel_id: str,
+        prefs: Union[str, Dict[str, str]],
+        **kwargs,
+    ) -> SlackResponse:
+        """Set the posting permissions for a public or private channel.
+        https://docs.slack.dev/reference/methods/admin.conversations.setConversationPrefs
+        """
+        kwargs.update({"channel_id": channel_id})
+        if isinstance(prefs, dict):
+            kwargs.update({"prefs": json.dumps(prefs)})
+        else:
+            kwargs.update({"prefs": prefs})
+        return self.api_call("admin.conversations.setConversationPrefs", params=kwargs)
+
+    def admin_conversations_getConversationPrefs(
+        self,
+        *,
+        channel_id: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Get conversation preferences for a public or private channel.
+        https://docs.slack.dev/reference/methods/admin.conversations.getConversationPrefs
+        """
+        kwargs.update({"channel_id": channel_id})
+        return self.api_call("admin.conversations.getConversationPrefs", params=kwargs)
+
+    def admin_conversations_disconnectShared(
+        self,
+        *,
+        channel_id: str,
+        leaving_team_ids: Optional[Union[str, Sequence[str]]] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Disconnect a connected channel from one or more workspaces.
+        https://docs.slack.dev/reference/methods/admin.conversations.disconnectShared
+        """
+        kwargs.update({"channel_id": channel_id})
+        if isinstance(leaving_team_ids, (list, tuple)):
+            kwargs.update({"leaving_team_ids": ",".join(leaving_team_ids)})
+        else:
+            kwargs.update({"leaving_team_ids": leaving_team_ids})
+        return self.api_call("admin.conversations.disconnectShared", params=kwargs)
+
+    def admin_conversations_lookup(
+        self,
+        *,
+        last_message_activity_before: int,
+        team_ids: Union[str, Sequence[str]],
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        max_member_count: Optional[int] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Returns channels on the given team using the filters.
+        https://docs.slack.dev/reference/methods/admin.conversations.lookup
+        """
+        kwargs.update(
+            {
+                "last_message_activity_before": last_message_activity_before,
+                "cursor": cursor,
+                "limit": limit,
+                "max_member_count": max_member_count,
+            }
+        )
+        if isinstance(team_ids, (list, tuple)):
+            kwargs.update({"team_ids": ",".join(team_ids)})
+        else:
+            kwargs.update({"team_ids": team_ids})
+        return self.api_call("admin.conversations.lookup", params=kwargs)
+
+    def admin_conversations_ekm_listOriginalConnectedChannelInfo(
+        self,
+        *,
+        channel_ids: Optional[Union[str, Sequence[str]]] = None,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        team_ids: Optional[Union[str, Sequence[str]]] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """List all disconnected channels—i.e.,
+        channels that were once connected to other workspaces and then disconnected—and
+        the corresponding original channel IDs for key revocation with EKM.
+        https://docs.slack.dev/reference/methods/admin.conversations.ekm.listOriginalConnectedChannelInfo
+        """
+        kwargs.update(
+            {
+                "cursor": cursor,
+                "limit": limit,
+            }
+        )
+        if isinstance(channel_ids, (list, tuple)):
+            kwargs.update({"channel_ids": ",".join(channel_ids)})
+        else:
+            kwargs.update({"channel_ids": channel_ids})
+        if isinstance(team_ids, (list, tuple)):
+            kwargs.update({"team_ids": ",".join(team_ids)})
+        else:
+            kwargs.update({"team_ids": team_ids})
+        return self.api_call("admin.conversations.ekm.listOriginalConnectedChannelInfo", params=kwargs)
+
+    def admin_conversations_restrictAccess_addGroup(
+        self,
+        *,
+        channel_id: str,
+        group_id: str,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Add an allowlist of IDP groups for accessing a channel.
+        https://docs.slack.dev/reference/methods/admin.conversations.restrictAccess.addGroup
+        """
+        kwargs.update(
+            {
+                "channel_id": channel_id,
+                "group_id": group_id,
+                "team_id": team_id,
+            }
+        )
+        return self.api_call(
+            "admin.conversations.restrictAccess.addGroup",
+            http_verb="GET",
+            params=kwargs,
+        )
+
+    def admin_conversations_restrictAccess_listGroups(
+        self,
+        *,
+        channel_id: str,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """List all IDP Groups linked to a channel.
+        https://docs.slack.dev/reference/methods/admin.conversations.restrictAccess.listGroups
+        """
+        kwargs.update(
+            {
+                "channel_id": channel_id,
+                "team_id": team_id,
+            }
+        )
+        return self.api_call(
+            "admin.conversations.restrictAccess.listGroups",
+            http_verb="GET",
+            params=kwargs,
+        )
+
+    def admin_conversations_restrictAccess_removeGroup(
+        self,
+        *,
+        channel_id: str,
+        group_id: str,
+        team_id: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Remove a linked IDP group linked from a private channel.
+        https://docs.slack.dev/reference/methods/admin.conversations.restrictAccess.removeGroup
+        """
+        kwargs.update(
+            {
+                "channel_id": channel_id,
+                "group_id": group_id,
+                "team_id": team_id,
+            }
+        )
+        return self.api_call(
+            "admin.conversations.restrictAccess.removeGroup",
+            http_verb="GET",
+            params=kwargs,
+        )
+
+    def admin_conversations_setTeams(
+        self,
+        *,
+        channel_id: str,
+        org_channel: Optional[bool] = None,
+        target_team_ids: Optional[Union[str, Sequence[str]]] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Set the workspaces in an Enterprise grid org that connect to a public or private channel.
+        https://docs.slack.dev/reference/methods/admin.conversations.setTeams
+        """
+        kwargs.update(
+            {
+                "channel_id": channel_id,
+                "org_channel": org_channel,
+                "team_id": team_id,
+            }
+        )
+        if isinstance(target_team_ids, (list, tuple)):
+            kwargs.update({"target_team_ids": ",".join(target_team_ids)})
+        else:
+            kwargs.update({"target_team_ids": target_team_ids})
+        return self.api_call("admin.conversations.setTeams", params=kwargs)
+
+    def admin_conversations_getTeams(
+        self,
+        *,
+        channel_id: str,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Set the workspaces in an Enterprise grid org that connect to a channel.
+        https://docs.slack.dev/reference/methods/admin.conversations.getTeams
+        """
+        kwargs.update(
+            {
+                "channel_id": channel_id,
+                "cursor": cursor,
+                "limit": limit,
+            }
+        )
+        return self.api_call("admin.conversations.getTeams", params=kwargs)
+
+    def admin_conversations_getCustomRetention(
+        self,
+        *,
+        channel_id: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Get a channel's retention policy
+        https://docs.slack.dev/reference/methods/admin.conversations.getCustomRetention
+        """
+        kwargs.update({"channel_id": channel_id})
+        return self.api_call("admin.conversations.getCustomRetention", params=kwargs)
+
+    def admin_conversations_removeCustomRetention(
+        self,
+        *,
+        channel_id: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Remove a channel's retention policy
+        https://docs.slack.dev/reference/methods/admin.conversations.removeCustomRetention
+        """
+        kwargs.update({"channel_id": channel_id})
+        return self.api_call("admin.conversations.removeCustomRetention", params=kwargs)
+
+    def admin_conversations_setCustomRetention(
+        self,
+        *,
+        channel_id: str,
+        duration_days: int,
+        **kwargs,
+    ) -> SlackResponse:
+        """Set a channel's retention policy
+        https://docs.slack.dev/reference/methods/admin.conversations.setCustomRetention
+        """
+        kwargs.update({"channel_id": channel_id, "duration_days": duration_days})
+        return self.api_call("admin.conversations.setCustomRetention", params=kwargs)
+
+    def admin_conversations_bulkArchive(
+        self,
+        *,
+        channel_ids: Union[Sequence[str], str],
+        **kwargs,
+    ) -> SlackResponse:
+        """Archive public or private channels in bulk.
+        https://docs.slack.dev/reference/methods/admin.conversations.bulkArchive
+        """
+        kwargs.update({"channel_ids": ",".join(channel_ids) if isinstance(channel_ids, (list, tuple)) else channel_ids})
+        return self.api_call("admin.conversations.bulkArchive", params=kwargs)
+
+    def admin_conversations_bulkDelete(
+        self,
+        *,
+        channel_ids: Union[Sequence[str], str],
+        **kwargs,
+    ) -> SlackResponse:
+        """Delete public or private channels in bulk.
+        https://slack.com/api/admin.conversations.bulkDelete
+        """
+        kwargs.update({"channel_ids": ",".join(channel_ids) if isinstance(channel_ids, (list, tuple)) else channel_ids})
+        return self.api_call("admin.conversations.bulkDelete", params=kwargs)
+
+    def admin_conversations_bulkMove(
+        self,
+        *,
+        channel_ids: Union[Sequence[str], str],
+        target_team_id: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Move public or private channels in bulk.
+        https://docs.slack.dev/reference/methods/admin.conversations.bulkMove
+        """
+        kwargs.update(
+            {
+                "target_team_id": target_team_id,
+                "channel_ids": ",".join(channel_ids) if isinstance(channel_ids, (list, tuple)) else channel_ids,
+            }
+        )
+        return self.api_call("admin.conversations.bulkMove", params=kwargs)
+
+    def admin_emoji_add(
+        self,
+        *,
+        name: str,
+        url: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Add an emoji.
+        https://docs.slack.dev/reference/methods/admin.emoji.add
+        """
+        kwargs.update({"name": name, "url": url})
+        return self.api_call("admin.emoji.add", http_verb="GET", params=kwargs)
+
+    def admin_emoji_addAlias(
+        self,
+        *,
+        alias_for: str,
+        name: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Add an emoji alias.
+        https://docs.slack.dev/reference/methods/admin.emoji.addAlias
+        """
+        kwargs.update({"alias_for": alias_for, "name": name})
+        return self.api_call("admin.emoji.addAlias", http_verb="GET", params=kwargs)
+
+    def admin_emoji_list(
+        self,
+        *,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """List emoji for an Enterprise Grid organization.
+        https://docs.slack.dev/reference/methods/admin.emoji.list
+        """
+        kwargs.update({"cursor": cursor, "limit": limit})
+        return self.api_call("admin.emoji.list", http_verb="GET", params=kwargs)
+
+    def admin_emoji_remove(
+        self,
+        *,
+        name: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Remove an emoji across an Enterprise Grid organization.
+        https://docs.slack.dev/reference/methods/admin.emoji.remove
+        """
+        kwargs.update({"name": name})
+        return self.api_call("admin.emoji.remove", http_verb="GET", params=kwargs)
+
+    def admin_emoji_rename(
+        self,
+        *,
+        name: str,
+        new_name: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Rename an emoji.
+        https://docs.slack.dev/reference/methods/admin.emoji.rename
+        """
+        kwargs.update({"name": name, "new_name": new_name})
+        return self.api_call("admin.emoji.rename", http_verb="GET", params=kwargs)
+
+    def admin_functions_list(
+        self,
+        *,
+        app_ids: Union[str, Sequence[str]],
+        team_id: Optional[str] = None,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Look up functions by a set of apps
+        https://docs.slack.dev/reference/methods/admin.functions.list
+        """
+        if isinstance(app_ids, (list, tuple)):
+            kwargs.update({"app_ids": ",".join(app_ids)})
+        else:
+            kwargs.update({"app_ids": app_ids})
+        kwargs.update(
+            {
+                "team_id": team_id,
+                "cursor": cursor,
+                "limit": limit,
+            }
+        )
+        return self.api_call("admin.functions.list", params=kwargs)
+
+    def admin_functions_permissions_lookup(
+        self,
+        *,
+        function_ids: Union[str, Sequence[str]],
+        **kwargs,
+    ) -> SlackResponse:
+        """Lookup the visibility of multiple Slack functions
+        and include the users if it is limited to particular named entities.
+        https://docs.slack.dev/reference/methods/admin.functions.permissions.lookup
+        """
+        if isinstance(function_ids, (list, tuple)):
+            kwargs.update({"function_ids": ",".join(function_ids)})
+        else:
+            kwargs.update({"function_ids": function_ids})
+        return self.api_call("admin.functions.permissions.lookup", params=kwargs)
+
+    def admin_functions_permissions_set(
+        self,
+        *,
+        function_id: str,
+        visibility: str,
+        user_ids: Optional[Union[str, Sequence[str]]] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Set the visibility of a Slack function
+        and define the users or workspaces if it is set to named_entities
+        https://docs.slack.dev/reference/methods/admin.functions.permissions.set
+        """
+        kwargs.update(
+            {
+                "function_id": function_id,
+                "visibility": visibility,
+            }
+        )
+        if user_ids is not None:
+            if isinstance(user_ids, (list, tuple)):
+                kwargs.update({"user_ids": ",".join(user_ids)})
+            else:
+                kwargs.update({"user_ids": user_ids})
+        return self.api_call("admin.functions.permissions.set", params=kwargs)
+
+    def admin_roles_addAssignments(
+        self,
+        *,
+        role_id: str,
+        entity_ids: Union[str, Sequence[str]],
+        user_ids: Union[str, Sequence[str]],
+        **kwargs,
+    ) -> SlackResponse:
+        """Adds members to the specified role with the specified scopes
+        https://docs.slack.dev/reference/methods/admin.roles.addAssignments
+        """
+        kwargs.update({"role_id": role_id})
+        if isinstance(entity_ids, (list, tuple)):
+            kwargs.update({"entity_ids": ",".join(entity_ids)})
+        else:
+            kwargs.update({"entity_ids": entity_ids})
+        if isinstance(user_ids, (list, tuple)):
+            kwargs.update({"user_ids": ",".join(user_ids)})
+        else:
+            kwargs.update({"user_ids": user_ids})
+        return self.api_call("admin.roles.addAssignments", params=kwargs)
+
+    def admin_roles_listAssignments(
+        self,
+        *,
+        role_ids: Optional[Union[str, Sequence[str]]] = None,
+        entity_ids: Optional[Union[str, Sequence[str]]] = None,
+        cursor: Optional[str] = None,
+        limit: Optional[Union[str, int]] = None,
+        sort_dir: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Lists assignments for all roles across entities.
+            Options to scope results by any combination of roles or entities
+        https://docs.slack.dev/reference/methods/admin.roles.listAssignments
+        """
+        kwargs.update({"cursor": cursor, "limit": limit, "sort_dir": sort_dir})
+        if isinstance(entity_ids, (list, tuple)):
+            kwargs.update({"entity_ids": ",".join(entity_ids)})
+        else:
+            kwargs.update({"entity_ids": entity_ids})
+        if isinstance(role_ids, (list, tuple)):
+            kwargs.update({"role_ids": ",".join(role_ids)})
+        else:
+            kwargs.update({"role_ids": role_ids})
+        return self.api_call("admin.roles.listAssignments", params=kwargs)
+
+    def admin_roles_removeAssignments(
+        self,
+        *,
+        role_id: str,
+        entity_ids: Union[str, Sequence[str]],
+        user_ids: Union[str, Sequence[str]],
+        **kwargs,
+    ) -> SlackResponse:
+        """Removes a set of users from a role for the given scopes and entities
+        https://docs.slack.dev/reference/methods/admin.roles.removeAssignments
+        """
+        kwargs.update({"role_id": role_id})
+        if isinstance(entity_ids, (list, tuple)):
+            kwargs.update({"entity_ids": ",".join(entity_ids)})
+        else:
+            kwargs.update({"entity_ids": entity_ids})
+        if isinstance(user_ids, (list, tuple)):
+            kwargs.update({"user_ids": ",".join(user_ids)})
+        else:
+            kwargs.update({"user_ids": user_ids})
+        return self.api_call("admin.roles.removeAssignments", params=kwargs)
+
+    def admin_users_session_reset(
+        self,
+        *,
+        user_id: str,
+        mobile_only: Optional[bool] = None,
+        web_only: Optional[bool] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Wipes all valid sessions on all devices for a given user.
+        https://docs.slack.dev/reference/methods/admin.users.session.reset
+        """
+        kwargs.update(
+            {
+                "user_id": user_id,
+                "mobile_only": mobile_only,
+                "web_only": web_only,
+            }
+        )
+        return self.api_call("admin.users.session.reset", params=kwargs)
+
+    def admin_users_session_resetBulk(
+        self,
+        *,
+        user_ids: Union[str, Sequence[str]],
+        mobile_only: Optional[bool] = None,
+        web_only: Optional[bool] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Enqueues an asynchronous job to wipe all valid sessions on all devices for a given list of users
+        https://docs.slack.dev/reference/methods/admin.users.session.resetBulk
+        """
+        if isinstance(user_ids, (list, tuple)):
+            kwargs.update({"user_ids": ",".join(user_ids)})
+        else:
+            kwargs.update({"user_ids": user_ids})
+        kwargs.update(
+            {
+                "mobile_only": mobile_only,
+                "web_only": web_only,
+            }
+        )
+        return self.api_call("admin.users.session.resetBulk", params=kwargs)
+
+    def admin_users_session_invalidate(
+        self,
+        *,
+        session_id: str,
+        team_id: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Invalidate a single session for a user by session_id.
+        https://docs.slack.dev/reference/methods/admin.users.session.invalidate
+        """
+        kwargs.update({"session_id": session_id, "team_id": team_id})
+        return self.api_call("admin.users.session.invalidate", params=kwargs)
+
+    def admin_users_session_list(
+        self,
+        *,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        team_id: Optional[str] = None,
+        user_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Lists all active user sessions for an organization
+        https://docs.slack.dev/reference/methods/admin.users.session.list
+        """
+        kwargs.update(
+            {
+                "cursor": cursor,
+                "limit": limit,
+                "team_id": team_id,
+                "user_id": user_id,
+            }
+        )
+        return self.api_call("admin.users.session.list", params=kwargs)
+
+    def admin_teams_settings_setDefaultChannels(
+        self,
+        *,
+        team_id: str,
+        channel_ids: Union[str, Sequence[str]],
+        **kwargs,
+    ) -> SlackResponse:
+        """Set the default channels of a workspace.
+        https://docs.slack.dev/reference/methods/admin.teams.settings.setDefaultChannels
+        """
+        kwargs.update({"team_id": team_id})
+        if isinstance(channel_ids, (list, tuple)):
+            kwargs.update({"channel_ids": ",".join(channel_ids)})
+        else:
+            kwargs.update({"channel_ids": channel_ids})
+        return self.api_call("admin.teams.settings.setDefaultChannels", http_verb="GET", params=kwargs)
+
+    def admin_users_session_getSettings(
+        self,
+        *,
+        user_ids: Union[str, Sequence[str]],
+        **kwargs,
+    ) -> SlackResponse:
+        """Get user-specific session settings—the session duration
+        and what happens when the client closes—given a list of users.
+        https://docs.slack.dev/reference/methods/admin.users.session.getSettings
+        """
+        if isinstance(user_ids, (list, tuple)):
+            kwargs.update({"user_ids": ",".join(user_ids)})
+        else:
+            kwargs.update({"user_ids": user_ids})
+        return self.api_call("admin.users.session.getSettings", params=kwargs)
+
+    def admin_users_session_setSettings(
+        self,
+        *,
+        user_ids: Union[str, Sequence[str]],
+        desktop_app_browser_quit: Optional[bool] = None,
+        duration: Optional[int] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Configure the user-level session settings—the session duration
+        and what happens when the client closes—for one or more users.
+        https://docs.slack.dev/reference/methods/admin.users.session.setSettings
+        """
+        if isinstance(user_ids, (list, tuple)):
+            kwargs.update({"user_ids": ",".join(user_ids)})
+        else:
+            kwargs.update({"user_ids": user_ids})
+        kwargs.update(
+            {
+                "desktop_app_browser_quit": desktop_app_browser_quit,
+                "duration": duration,
+            }
+        )
+        return self.api_call("admin.users.session.setSettings", params=kwargs)
+
+    def admin_users_session_clearSettings(
+        self,
+        *,
+        user_ids: Union[str, Sequence[str]],
+        **kwargs,
+    ) -> SlackResponse:
+        """Clear user-specific session settings—the session duration
+        and what happens when the client closes—for a list of users.
+        https://docs.slack.dev/reference/methods/admin.users.session.clearSettings
+        """
+        if isinstance(user_ids, (list, tuple)):
+            kwargs.update({"user_ids": ",".join(user_ids)})
+        else:
+            kwargs.update({"user_ids": user_ids})
+        return self.api_call("admin.users.session.clearSettings", params=kwargs)
+
+    def admin_users_unsupportedVersions_export(
+        self,
+        *,
+        date_end_of_support: Optional[Union[str, int]] = None,
+        date_sessions_started: Optional[Union[str, int]] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Ask Slackbot to send you an export listing all workspace members using unsupported software,
+        presented as a zipped CSV file.
+        https://docs.slack.dev/reference/methods/admin.users.unsupportedVersions.export
+        """
+        kwargs.update(
+            {
+                "date_end_of_support": date_end_of_support,
+                "date_sessions_started": date_sessions_started,
+            }
+        )
+        return self.api_call("admin.users.unsupportedVersions.export", params=kwargs)
+
+    def admin_inviteRequests_approve(
+        self,
+        *,
+        invite_request_id: str,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Approve a workspace invite request.
+        https://docs.slack.dev/reference/methods/admin.inviteRequests.approve
+        """
+        kwargs.update({"invite_request_id": invite_request_id, "team_id": team_id})
+        return self.api_call("admin.inviteRequests.approve", params=kwargs)
+
+    def admin_inviteRequests_approved_list(
+        self,
+        *,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """List all approved workspace invite requests.
+        https://docs.slack.dev/reference/methods/admin.inviteRequests.approved.list
+        """
+        kwargs.update(
+            {
+                "cursor": cursor,
+                "limit": limit,
+                "team_id": team_id,
+            }
+        )
+        return self.api_call("admin.inviteRequests.approved.list", params=kwargs)
+
+    def admin_inviteRequests_denied_list(
+        self,
+        *,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """List all denied workspace invite requests.
+        https://docs.slack.dev/reference/methods/admin.inviteRequests.denied.list
+        """
+        kwargs.update(
+            {
+                "cursor": cursor,
+                "limit": limit,
+                "team_id": team_id,
+            }
+        )
+        return self.api_call("admin.inviteRequests.denied.list", params=kwargs)
+
+    def admin_inviteRequests_deny(
+        self,
+        *,
+        invite_request_id: str,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Deny a workspace invite request.
+        https://docs.slack.dev/reference/methods/admin.inviteRequests.deny
+        """
+        kwargs.update({"invite_request_id": invite_request_id, "team_id": team_id})
+        return self.api_call("admin.inviteRequests.deny", params=kwargs)
+
+    def admin_inviteRequests_list(
+        self,
+        **kwargs,
+    ) -> SlackResponse:
+        """List all pending workspace invite requests."""
+        return self.api_call("admin.inviteRequests.list", params=kwargs)
+
+    def admin_teams_admins_list(
+        self,
+        *,
+        team_id: str,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """List all of the admins on a given workspace.
+        https://docs.slack.dev/reference/methods/admin.inviteRequests.list
+        """
+        kwargs.update(
+            {
+                "cursor": cursor,
+                "limit": limit,
+                "team_id": team_id,
+            }
+        )
+        return self.api_call("admin.teams.admins.list", http_verb="GET", params=kwargs)
+
+    def admin_teams_create(
+        self,
+        *,
+        team_domain: str,
+        team_name: str,
+        team_description: Optional[str] = None,
+        team_discoverability: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Create an Enterprise team.
+        https://docs.slack.dev/reference/methods/admin.teams.create
+        """
+        kwargs.update(
+            {
+                "team_domain": team_domain,
+                "team_name": team_name,
+                "team_description": team_description,
+                "team_discoverability": team_discoverability,
+            }
+        )
+        return self.api_call("admin.teams.create", params=kwargs)
+
+    def admin_teams_list(
+        self,
+        *,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """List all teams on an Enterprise organization.
+        https://docs.slack.dev/reference/methods/admin.teams.list
+        """
+        kwargs.update({"cursor": cursor, "limit": limit})
+        return self.api_call("admin.teams.list", params=kwargs)
+
+    def admin_teams_owners_list(
+        self,
+        *,
+        team_id: str,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """List all of the admins on a given workspace.
+        https://docs.slack.dev/reference/methods/admin.teams.owners.list
+        """
+        kwargs.update({"team_id": team_id, "cursor": cursor, "limit": limit})
+        return self.api_call("admin.teams.owners.list", http_verb="GET", params=kwargs)
+
+    def admin_teams_settings_info(
+        self,
+        *,
+        team_id: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Fetch information about settings in a workspace
+        https://docs.slack.dev/reference/methods/admin.teams.settings.info
+        """
+        kwargs.update({"team_id": team_id})
+        return self.api_call("admin.teams.settings.info", params=kwargs)
+
+    def admin_teams_settings_setDescription(
+        self,
+        *,
+        team_id: str,
+        description: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Set the description of a given workspace.
+        https://docs.slack.dev/reference/methods/admin.teams.settings.setDescription
+        """
+        kwargs.update({"team_id": team_id, "description": description})
+        return self.api_call("admin.teams.settings.setDescription", params=kwargs)
+
+    def admin_teams_settings_setDiscoverability(
+        self,
+        *,
+        team_id: str,
+        discoverability: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Sets the icon of a workspace.
+        https://docs.slack.dev/reference/methods/admin.teams.settings.setDiscoverability
+        """
+        kwargs.update({"team_id": team_id, "discoverability": discoverability})
+        return self.api_call("admin.teams.settings.setDiscoverability", params=kwargs)
+
+    def admin_teams_settings_setIcon(
+        self,
+        *,
+        team_id: str,
+        image_url: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Sets the icon of a workspace.
+        https://docs.slack.dev/reference/methods/admin.teams.settings.setIcon
+        """
+        kwargs.update({"team_id": team_id, "image_url": image_url})
+        return self.api_call("admin.teams.settings.setIcon", http_verb="GET", params=kwargs)
+
+    def admin_teams_settings_setName(
+        self,
+        *,
+        team_id: str,
+        name: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Sets the icon of a workspace.
+        https://docs.slack.dev/reference/methods/admin.teams.settings.setName
+        """
+        kwargs.update({"team_id": team_id, "name": name})
+        return self.api_call("admin.teams.settings.setName", params=kwargs)
+
+    def admin_usergroups_addChannels(
+        self,
+        *,
+        channel_ids: Union[str, Sequence[str]],
+        usergroup_id: str,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Add one or more default channels to an IDP group.
+        https://docs.slack.dev/reference/methods/admin.usergroups.addChannels
+        """
+        kwargs.update({"team_id": team_id, "usergroup_id": usergroup_id})
+        if isinstance(channel_ids, (list, tuple)):
+            kwargs.update({"channel_ids": ",".join(channel_ids)})
+        else:
+            kwargs.update({"channel_ids": channel_ids})
+        return self.api_call("admin.usergroups.addChannels", params=kwargs)
+
+    def admin_usergroups_addTeams(
+        self,
+        *,
+        usergroup_id: str,
+        team_ids: Union[str, Sequence[str]],
+        auto_provision: Optional[bool] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Associate one or more default workspaces with an organization-wide IDP group.
+        https://docs.slack.dev/reference/methods/admin.usergroups.addTeams
+        """
+        kwargs.update({"usergroup_id": usergroup_id, "auto_provision": auto_provision})
+        if isinstance(team_ids, (list, tuple)):
+            kwargs.update({"team_ids": ",".join(team_ids)})
+        else:
+            kwargs.update({"team_ids": team_ids})
+        return self.api_call("admin.usergroups.addTeams", params=kwargs)
+
+    def admin_usergroups_listChannels(
+        self,
+        *,
+        usergroup_id: str,
+        include_num_members: Optional[bool] = None,
+        team_id: Optional[bool] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Add one or more default channels to an IDP group.
+        https://docs.slack.dev/reference/methods/admin.usergroups.listChannels
+        """
+        kwargs.update(
+            {
+                "usergroup_id": usergroup_id,
+                "include_num_members": include_num_members,
+                "team_id": team_id,
+            }
+        )
+        return self.api_call("admin.usergroups.listChannels", params=kwargs)
+
+    def admin_usergroups_removeChannels(
+        self,
+        *,
+        usergroup_id: str,
+        channel_ids: Union[str, Sequence[str]],
+        **kwargs,
+    ) -> SlackResponse:
+        """Add one or more default channels to an IDP group.
+        https://docs.slack.dev/reference/methods/admin.usergroups.removeChannels
+        """
+        kwargs.update({"usergroup_id": usergroup_id})
+        if isinstance(channel_ids, (list, tuple)):
+            kwargs.update({"channel_ids": ",".join(channel_ids)})
+        else:
+            kwargs.update({"channel_ids": channel_ids})
+        return self.api_call("admin.usergroups.removeChannels", params=kwargs)
+
+    def admin_users_assign(
+        self,
+        *,
+        team_id: str,
+        user_id: str,
+        channel_ids: Optional[Union[str, Sequence[str]]] = None,
+        is_restricted: Optional[bool] = None,
+        is_ultra_restricted: Optional[bool] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Add an Enterprise user to a workspace.
+        https://docs.slack.dev/reference/methods/admin.users.assign
+        """
+        kwargs.update(
+            {
+                "team_id": team_id,
+                "user_id": user_id,
+                "is_restricted": is_restricted,
+                "is_ultra_restricted": is_ultra_restricted,
+            }
+        )
+        if isinstance(channel_ids, (list, tuple)):
+            kwargs.update({"channel_ids": ",".join(channel_ids)})
+        else:
+            kwargs.update({"channel_ids": channel_ids})
+        return self.api_call("admin.users.assign", params=kwargs)
+
+    def admin_users_invite(
+        self,
+        *,
+        team_id: str,
+        email: str,
+        channel_ids: Union[str, Sequence[str]],
+        custom_message: Optional[str] = None,
+        email_password_policy_enabled: Optional[bool] = None,
+        guest_expiration_ts: Optional[Union[str, float]] = None,
+        is_restricted: Optional[bool] = None,
+        is_ultra_restricted: Optional[bool] = None,
+        real_name: Optional[str] = None,
+        resend: Optional[bool] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Invite a user to a workspace.
+        https://docs.slack.dev/reference/methods/admin.users.invite
+        """
+        kwargs.update(
+            {
+                "team_id": team_id,
+                "email": email,
+                "custom_message": custom_message,
+                "email_password_policy_enabled": email_password_policy_enabled,
+                "guest_expiration_ts": str(guest_expiration_ts) if guest_expiration_ts is not None else None,
+                "is_restricted": is_restricted,
+                "is_ultra_restricted": is_ultra_restricted,
+                "real_name": real_name,
+                "resend": resend,
+            }
+        )
+        if isinstance(channel_ids, (list, tuple)):
+            kwargs.update({"channel_ids": ",".join(channel_ids)})
+        else:
+            kwargs.update({"channel_ids": channel_ids})
+        return self.api_call("admin.users.invite", params=kwargs)
+
+    def admin_users_list(
+        self,
+        *,
+        team_id: Optional[str] = None,
+        include_deactivated_user_workspaces: Optional[bool] = None,
+        is_active: Optional[bool] = None,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """List users on a workspace
+        https://docs.slack.dev/reference/methods/admin.users.list
+        """
+        kwargs.update(
+            {
+                "team_id": team_id,
+                "include_deactivated_user_workspaces": include_deactivated_user_workspaces,
+                "is_active": is_active,
+                "cursor": cursor,
+                "limit": limit,
+            }
+        )
+        return self.api_call("admin.users.list", params=kwargs)
+
+    def admin_users_remove(
+        self,
+        *,
+        team_id: str,
+        user_id: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Remove a user from a workspace.
+        https://docs.slack.dev/reference/methods/admin.users.remove
+        """
+        kwargs.update({"team_id": team_id, "user_id": user_id})
+        return self.api_call("admin.users.remove", params=kwargs)
+
+    def admin_users_setAdmin(
+        self,
+        *,
+        team_id: str,
+        user_id: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Set an existing guest, regular user, or owner to be an admin user.
+        https://docs.slack.dev/reference/methods/admin.users.setAdmin
+        """
+        kwargs.update({"team_id": team_id, "user_id": user_id})
+        return self.api_call("admin.users.setAdmin", params=kwargs)
+
+    def admin_users_setExpiration(
+        self,
+        *,
+        expiration_ts: int,
+        user_id: str,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Set an expiration for a guest user.
+        https://docs.slack.dev/reference/methods/admin.users.setExpiration
+        """
+        kwargs.update({"expiration_ts": expiration_ts, "team_id": team_id, "user_id": user_id})
+        return self.api_call("admin.users.setExpiration", params=kwargs)
+
+    def admin_users_setOwner(
+        self,
+        *,
+        team_id: str,
+        user_id: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Set an existing guest, regular user, or admin user to be a workspace owner.
+        https://docs.slack.dev/reference/methods/admin.users.setOwner
+        """
+        kwargs.update({"team_id": team_id, "user_id": user_id})
+        return self.api_call("admin.users.setOwner", params=kwargs)
+
+    def admin_users_setRegular(
+        self,
+        *,
+        team_id: str,
+        user_id: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Set an existing guest user, admin user, or owner to be a regular user.
+        https://docs.slack.dev/reference/methods/admin.users.setRegular
+        """
+        kwargs.update({"team_id": team_id, "user_id": user_id})
+        return self.api_call("admin.users.setRegular", params=kwargs)
+
+    def admin_workflows_search(
+        self,
+        *,
+        app_id: Optional[str] = None,
+        collaborator_ids: Optional[Union[str, Sequence[str]]] = None,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        no_collaborators: Optional[bool] = None,
+        num_trigger_ids: Optional[int] = None,
+        query: Optional[str] = None,
+        sort: Optional[str] = None,
+        sort_dir: Optional[str] = None,
+        source: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Search workflows within the team or enterprise
+        https://docs.slack.dev/reference/methods/admin.workflows.search
+        """
+        if collaborator_ids is not None:
+            if isinstance(collaborator_ids, (list, tuple)):
+                kwargs.update({"collaborator_ids": ",".join(collaborator_ids)})
+            else:
+                kwargs.update({"collaborator_ids": collaborator_ids})
+        kwargs.update(
+            {
+                "app_id": app_id,
+                "cursor": cursor,
+                "limit": limit,
+                "no_collaborators": no_collaborators,
+                "num_trigger_ids": num_trigger_ids,
+                "query": query,
+                "sort": sort,
+                "sort_dir": sort_dir,
+                "source": source,
+            }
+        )
+        return self.api_call("admin.workflows.search", params=kwargs)
+
+    def admin_workflows_permissions_lookup(
+        self,
+        *,
+        workflow_ids: Union[str, Sequence[str]],
+        max_workflow_triggers: Optional[int] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Look up the permissions for a set of workflows
+        https://docs.slack.dev/reference/methods/admin.workflows.permissions.lookup
+        """
+        if isinstance(workflow_ids, (list, tuple)):
+            kwargs.update({"workflow_ids": ",".join(workflow_ids)})
+        else:
+            kwargs.update({"workflow_ids": workflow_ids})
+        kwargs.update(
+            {
+                "max_workflow_triggers": max_workflow_triggers,
+            }
+        )
+        return self.api_call("admin.workflows.permissions.lookup", params=kwargs)
+
+    def admin_workflows_collaborators_add(
+        self,
+        *,
+        collaborator_ids: Union[str, Sequence[str]],
+        workflow_ids: Union[str, Sequence[str]],
+        **kwargs,
+    ) -> SlackResponse:
+        """Add collaborators to workflows within the team or enterprise
+        https://docs.slack.dev/reference/methods/admin.workflows.collaborators.add
+        """
+        if isinstance(collaborator_ids, (list, tuple)):
+            kwargs.update({"collaborator_ids": ",".join(collaborator_ids)})
+        else:
+            kwargs.update({"collaborator_ids": collaborator_ids})
+        if isinstance(workflow_ids, (list, tuple)):
+            kwargs.update({"workflow_ids": ",".join(workflow_ids)})
+        else:
+            kwargs.update({"workflow_ids": workflow_ids})
+        return self.api_call("admin.workflows.collaborators.add", params=kwargs)
+
+    def admin_workflows_collaborators_remove(
+        self,
+        *,
+        collaborator_ids: Union[str, Sequence[str]],
+        workflow_ids: Union[str, Sequence[str]],
+        **kwargs,
+    ) -> SlackResponse:
+        """Remove collaborators from workflows within the team or enterprise
+        https://docs.slack.dev/reference/methods/admin.workflows.collaborators.remove
+        """
+        if isinstance(collaborator_ids, (list, tuple)):
+            kwargs.update({"collaborator_ids": ",".join(collaborator_ids)})
+        else:
+            kwargs.update({"collaborator_ids": collaborator_ids})
+        if isinstance(workflow_ids, (list, tuple)):
+            kwargs.update({"workflow_ids": ",".join(workflow_ids)})
+        else:
+            kwargs.update({"workflow_ids": workflow_ids})
+        return self.api_call("admin.workflows.collaborators.remove", params=kwargs)
+
+    def admin_workflows_unpublish(
+        self,
+        *,
+        workflow_ids: Union[str, Sequence[str]],
+        **kwargs,
+    ) -> SlackResponse:
+        """Unpublish workflows within the team or enterprise
+        https://docs.slack.dev/reference/methods/admin.workflows.unpublish
+        """
+        if isinstance(workflow_ids, (list, tuple)):
+            kwargs.update({"workflow_ids": ",".join(workflow_ids)})
+        else:
+            kwargs.update({"workflow_ids": workflow_ids})
+        return self.api_call("admin.workflows.unpublish", params=kwargs)
+
+    def api_test(
+        self,
+        *,
+        error: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Checks API calling code.
+        https://docs.slack.dev/reference/methods/api.test
+        """
+        kwargs.update({"error": error})
+        return self.api_call("api.test", params=kwargs)
+
+    def apps_connections_open(
+        self,
+        *,
+        app_token: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Generate a temporary Socket Mode WebSocket URL that your app can connect to
+        in order to receive events and interactive payloads
+        https://docs.slack.dev/reference/methods/apps.connections.open
+        """
+        kwargs.update({"token": app_token})
+        return self.api_call("apps.connections.open", http_verb="POST", params=kwargs)
+
+    def apps_event_authorizations_list(
+        self,
+        *,
+        event_context: str,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Get a list of authorizations for the given event context.
+        Each authorization represents an app installation that the event is visible to.
+        https://docs.slack.dev/reference/methods/apps.event.authorizations.list
+        """
+        kwargs.update({"event_context": event_context, "cursor": cursor, "limit": limit})
+        return self.api_call("apps.event.authorizations.list", params=kwargs)
+
+    def apps_uninstall(
+        self,
+        *,
+        client_id: str,
+        client_secret: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Uninstalls your app from a workspace.
+        https://docs.slack.dev/reference/methods/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://docs.slack.dev/reference/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://docs.slack.dev/reference/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://docs.slack.dev/reference/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://docs.slack.dev/reference/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://docs.slack.dev/reference/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://docs.slack.dev/reference/methods/tooling.tokens.rotate
+        """
+        kwargs.update({"refresh_token": refresh_token})
+        return self.api_call("tooling.tokens.rotate", params=kwargs)
+
+    def assistant_threads_setStatus(
+        self,
+        *,
+        channel_id: str,
+        thread_ts: str,
+        status: str,
+        loading_messages: Optional[List[str]] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Set the status for an AI assistant thread.
+        https://docs.slack.dev/reference/methods/assistant.threads.setStatus
+        """
+        kwargs.update(
+            {"channel_id": channel_id, "thread_ts": thread_ts, "status": status, "loading_messages": loading_messages}
+        )
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("assistant.threads.setStatus", json=kwargs)
+
+    def assistant_threads_setTitle(
+        self,
+        *,
+        channel_id: str,
+        thread_ts: str,
+        title: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Set the title for the given assistant thread.
+        https://docs.slack.dev/reference/methods/assistant.threads.setTitle
+        """
+        kwargs.update({"channel_id": channel_id, "thread_ts": thread_ts, "title": title})
+        return self.api_call("assistant.threads.setTitle", params=kwargs)
+
+    def assistant_threads_setSuggestedPrompts(
+        self,
+        *,
+        channel_id: str,
+        thread_ts: str,
+        title: Optional[str] = None,
+        prompts: List[Dict[str, str]],
+        **kwargs,
+    ) -> SlackResponse:
+        """Set suggested prompts for the given assistant thread.
+        https://docs.slack.dev/reference/methods/assistant.threads.setSuggestedPrompts
+        """
+        kwargs.update({"channel_id": channel_id, "thread_ts": thread_ts, "prompts": prompts})
+        if title is not None:
+            kwargs.update({"title": title})
+        return self.api_call("assistant.threads.setSuggestedPrompts", json=kwargs)
+
+    def auth_revoke(
+        self,
+        *,
+        test: Optional[bool] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Revokes a token.
+        https://docs.slack.dev/reference/methods/auth.revoke
+        """
+        kwargs.update({"test": test})
+        return self.api_call("auth.revoke", http_verb="GET", params=kwargs)
+
+    def auth_test(
+        self,
+        **kwargs,
+    ) -> SlackResponse:
+        """Checks authentication & identity.
+        https://docs.slack.dev/reference/methods/auth.test
+        """
+        return self.api_call("auth.test", params=kwargs)
+
+    def auth_teams_list(
+        self,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        include_icon: Optional[bool] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """List the workspaces a token can access.
+        https://docs.slack.dev/reference/methods/auth.teams.list
+        """
+        kwargs.update({"cursor": cursor, "limit": limit, "include_icon": include_icon})
+        return self.api_call("auth.teams.list", params=kwargs)
+
+    def bookmarks_add(
+        self,
+        *,
+        channel_id: str,
+        title: str,
+        type: str,
+        emoji: Optional[str] = None,
+        entity_id: Optional[str] = None,
+        link: Optional[str] = None,  # include when type is 'link'
+        parent_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Add bookmark to a channel.
+        https://docs.slack.dev/reference/methods/bookmarks.add
+        """
+        kwargs.update(
+            {
+                "channel_id": channel_id,
+                "title": title,
+                "type": type,
+                "emoji": emoji,
+                "entity_id": entity_id,
+                "link": link,
+                "parent_id": parent_id,
+            }
+        )
+        return self.api_call("bookmarks.add", http_verb="POST", params=kwargs)
+
+    def bookmarks_edit(
+        self,
+        *,
+        bookmark_id: str,
+        channel_id: str,
+        emoji: Optional[str] = None,
+        link: Optional[str] = None,
+        title: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Edit bookmark.
+        https://docs.slack.dev/reference/methods/bookmarks.edit
+        """
+        kwargs.update(
+            {
+                "bookmark_id": bookmark_id,
+                "channel_id": channel_id,
+                "emoji": emoji,
+                "link": link,
+                "title": title,
+            }
+        )
+        return self.api_call("bookmarks.edit", http_verb="POST", params=kwargs)
+
+    def bookmarks_list(
+        self,
+        *,
+        channel_id: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """List bookmark for the channel.
+        https://docs.slack.dev/reference/methods/bookmarks.list
+        """
+        kwargs.update({"channel_id": channel_id})
+        return self.api_call("bookmarks.list", http_verb="POST", params=kwargs)
+
+    def bookmarks_remove(
+        self,
+        *,
+        bookmark_id: str,
+        channel_id: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Remove bookmark from the channel.
+        https://docs.slack.dev/reference/methods/bookmarks.remove
+        """
+        kwargs.update({"bookmark_id": bookmark_id, "channel_id": channel_id})
+        return self.api_call("bookmarks.remove", http_verb="POST", params=kwargs)
+
+    def bots_info(
+        self,
+        *,
+        bot: Optional[str] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Gets information about a bot user.
+        https://docs.slack.dev/reference/methods/bots.info
+        """
+        kwargs.update({"bot": bot, "team_id": team_id})
+        return self.api_call("bots.info", http_verb="GET", params=kwargs)
+
+    def calls_add(
+        self,
+        *,
+        external_unique_id: str,
+        join_url: str,
+        created_by: Optional[str] = None,
+        date_start: Optional[int] = None,
+        desktop_app_join_url: Optional[str] = None,
+        external_display_id: Optional[str] = None,
+        title: Optional[str] = None,
+        users: Optional[Union[str, Sequence[Dict[str, str]]]] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Registers a new Call.
+        https://docs.slack.dev/reference/methods/calls.add
+        """
+        kwargs.update(
+            {
+                "external_unique_id": external_unique_id,
+                "join_url": join_url,
+                "created_by": created_by,
+                "date_start": date_start,
+                "desktop_app_join_url": desktop_app_join_url,
+                "external_display_id": external_display_id,
+                "title": title,
+            }
+        )
+        _update_call_participants(
+            kwargs,
+            users if users is not None else kwargs.get("users"),  # type: ignore[arg-type]
+        )
+        return self.api_call("calls.add", http_verb="POST", params=kwargs)
+
+    def calls_end(
+        self,
+        *,
+        id: str,
+        duration: Optional[int] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Ends a Call.
+        https://docs.slack.dev/reference/methods/calls.end
+        """
+        kwargs.update({"id": id, "duration": duration})
+        return self.api_call("calls.end", http_verb="POST", params=kwargs)
+
+    def calls_info(
+        self,
+        *,
+        id: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Returns information about a Call.
+        https://docs.slack.dev/reference/methods/calls.info
+        """
+        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, Sequence[Dict[str, str]]],
+        **kwargs,
+    ) -> SlackResponse:
+        """Registers new participants added to a Call.
+        https://docs.slack.dev/reference/methods/calls.participants.add
+        """
+        kwargs.update({"id": id})
+        _update_call_participants(kwargs, users)
+        return self.api_call("calls.participants.add", http_verb="POST", params=kwargs)
+
+    def calls_participants_remove(
+        self,
+        *,
+        id: str,
+        users: Union[str, Sequence[Dict[str, str]]],
+        **kwargs,
+    ) -> SlackResponse:
+        """Registers participants removed from a Call.
+        https://docs.slack.dev/reference/methods/calls.participants.remove
+        """
+        kwargs.update({"id": id})
+        _update_call_participants(kwargs, users)
+        return self.api_call("calls.participants.remove", http_verb="POST", params=kwargs)
+
+    def calls_update(
+        self,
+        *,
+        id: str,
+        desktop_app_join_url: Optional[str] = None,
+        join_url: Optional[str] = None,
+        title: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Updates information about a Call.
+        https://docs.slack.dev/reference/methods/calls.update
+        """
+        kwargs.update(
+            {
+                "id": id,
+                "desktop_app_join_url": desktop_app_join_url,
+                "join_url": join_url,
+                "title": title,
+            }
+        )
+        return self.api_call("calls.update", http_verb="POST", params=kwargs)
+
+    def canvases_create(
+        self,
+        *,
+        title: Optional[str] = None,
+        document_content: Dict[str, str],
+        **kwargs,
+    ) -> SlackResponse:
+        """Create Canvas for a user
+        https://docs.slack.dev/reference/methods/canvases.create
+        """
+        kwargs.update({"title": title, "document_content": document_content})
+        return self.api_call("canvases.create", json=kwargs)
+
+    def canvases_edit(
+        self,
+        *,
+        canvas_id: str,
+        changes: Sequence[Dict[str, Any]],
+        **kwargs,
+    ) -> SlackResponse:
+        """Update an existing canvas
+        https://docs.slack.dev/reference/methods/canvases.edit
+        """
+        kwargs.update({"canvas_id": canvas_id, "changes": changes})
+        return self.api_call("canvases.edit", json=kwargs)
+
+    def canvases_delete(
+        self,
+        *,
+        canvas_id: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Deletes a canvas
+        https://docs.slack.dev/reference/methods/canvases.delete
+        """
+        kwargs.update({"canvas_id": canvas_id})
+        return self.api_call("canvases.delete", params=kwargs)
+
+    def canvases_access_set(
+        self,
+        *,
+        canvas_id: str,
+        access_level: str,
+        channel_ids: Optional[Union[Sequence[str], str]] = None,
+        user_ids: Optional[Union[Sequence[str], str]] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Sets the access level to a canvas for specified entities
+        https://docs.slack.dev/reference/methods/canvases.access.set
+        """
+        kwargs.update({"canvas_id": canvas_id, "access_level": access_level})
+        if channel_ids is not None:
+            if isinstance(channel_ids, (list, tuple)):
+                kwargs.update({"channel_ids": ",".join(channel_ids)})
+            else:
+                kwargs.update({"channel_ids": channel_ids})
+        if user_ids is not None:
+            if isinstance(user_ids, (list, tuple)):
+                kwargs.update({"user_ids": ",".join(user_ids)})
+            else:
+                kwargs.update({"user_ids": user_ids})
+
+        return self.api_call("canvases.access.set", params=kwargs)
+
+    def canvases_access_delete(
+        self,
+        *,
+        canvas_id: str,
+        channel_ids: Optional[Union[Sequence[str], str]] = None,
+        user_ids: Optional[Union[Sequence[str], str]] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Create a Channel Canvas for a channel
+        https://docs.slack.dev/reference/methods/canvases.access.delete
+        """
+        kwargs.update({"canvas_id": canvas_id})
+        if channel_ids is not None:
+            if isinstance(channel_ids, (list, tuple)):
+                kwargs.update({"channel_ids": ",".join(channel_ids)})
+            else:
+                kwargs.update({"channel_ids": channel_ids})
+        if user_ids is not None:
+            if isinstance(user_ids, (list, tuple)):
+                kwargs.update({"user_ids": ",".join(user_ids)})
+            else:
+                kwargs.update({"user_ids": user_ids})
+        return self.api_call("canvases.access.delete", params=kwargs)
+
+    def canvases_sections_lookup(
+        self,
+        *,
+        canvas_id: str,
+        criteria: Dict[str, Any],
+        **kwargs,
+    ) -> SlackResponse:
+        """Find sections matching the provided criteria
+        https://docs.slack.dev/reference/methods/canvases.sections.lookup
+        """
+        kwargs.update({"canvas_id": canvas_id, "criteria": json.dumps(criteria)})
+        return self.api_call("canvases.sections.lookup", params=kwargs)
+
+    # --------------------------
+    # Deprecated: channels.*
+    # You can use conversations.* APIs instead.
+    # https://docs.slack.dev/changelog/2020-01-deprecating-antecedents-to-the-conversations-api/
+    # --------------------------
+
+    def channels_archive(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Archives a channel."""
+        kwargs.update({"channel": channel})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("channels.archive", json=kwargs)
+
+    def channels_create(
+        self,
+        *,
+        name: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Creates a channel."""
+        kwargs.update({"name": name})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("channels.create", json=kwargs)
+
+    def channels_history(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Fetches history of messages and events from a channel."""
+        kwargs.update({"channel": channel})
+        return self.api_call("channels.history", http_verb="GET", params=kwargs)
+
+    def channels_info(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Gets information about a channel."""
+        kwargs.update({"channel": channel})
+        return self.api_call("channels.info", http_verb="GET", params=kwargs)
+
+    def channels_invite(
+        self,
+        *,
+        channel: str,
+        user: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Invites a user to a channel."""
+        kwargs.update({"channel": channel, "user": user})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("channels.invite", json=kwargs)
+
+    def channels_join(
+        self,
+        *,
+        name: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Joins a channel, creating it if needed."""
+        kwargs.update({"name": name})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("channels.join", json=kwargs)
+
+    def channels_kick(
+        self,
+        *,
+        channel: str,
+        user: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Removes a user from a channel."""
+        kwargs.update({"channel": channel, "user": user})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("channels.kick", json=kwargs)
+
+    def channels_leave(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Leaves a channel."""
+        kwargs.update({"channel": channel})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("channels.leave", json=kwargs)
+
+    def channels_list(
+        self,
+        **kwargs,
+    ) -> SlackResponse:
+        """Lists all channels in a Slack team."""
+        return self.api_call("channels.list", http_verb="GET", params=kwargs)
+
+    def channels_mark(
+        self,
+        *,
+        channel: str,
+        ts: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Sets the read cursor in a channel."""
+        kwargs.update({"channel": channel, "ts": ts})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("channels.mark", json=kwargs)
+
+    def channels_rename(
+        self,
+        *,
+        channel: str,
+        name: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Renames a channel."""
+        kwargs.update({"channel": channel, "name": name})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("channels.rename", json=kwargs)
+
+    def channels_replies(
+        self,
+        *,
+        channel: str,
+        thread_ts: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Retrieve a thread of messages posted to a channel"""
+        kwargs.update({"channel": channel, "thread_ts": thread_ts})
+        return self.api_call("channels.replies", http_verb="GET", params=kwargs)
+
+    def channels_setPurpose(
+        self,
+        *,
+        channel: str,
+        purpose: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Sets the purpose for a channel."""
+        kwargs.update({"channel": channel, "purpose": purpose})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("channels.setPurpose", json=kwargs)
+
+    def channels_setTopic(
+        self,
+        *,
+        channel: str,
+        topic: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Sets the topic for a channel."""
+        kwargs.update({"channel": channel, "topic": topic})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("channels.setTopic", json=kwargs)
+
+    def channels_unarchive(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Unarchives a channel."""
+        kwargs.update({"channel": channel})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("channels.unarchive", json=kwargs)
+
+    # --------------------------
+
+    def chat_appendStream(
+        self,
+        *,
+        channel: str,
+        ts: str,
+        markdown_text: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Appends text to an existing streaming conversation.
+        https://docs.slack.dev/reference/methods/chat.appendStream
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "ts": ts,
+                "markdown_text": markdown_text,
+            }
+        )
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("chat.appendStream", json=kwargs)
+
+    def chat_delete(
+        self,
+        *,
+        channel: str,
+        ts: str,
+        as_user: Optional[bool] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Deletes a message.
+        https://docs.slack.dev/reference/methods/chat.delete
+        """
+        kwargs.update({"channel": channel, "ts": ts, "as_user": as_user})
+        return self.api_call("chat.delete", params=kwargs)
+
+    def chat_deleteScheduledMessage(
+        self,
+        *,
+        channel: str,
+        scheduled_message_id: str,
+        as_user: Optional[bool] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Deletes a scheduled message.
+        https://docs.slack.dev/reference/methods/chat.deleteScheduledMessage
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "scheduled_message_id": scheduled_message_id,
+                "as_user": as_user,
+            }
+        )
+        return self.api_call("chat.deleteScheduledMessage", params=kwargs)
+
+    def chat_getPermalink(
+        self,
+        *,
+        channel: str,
+        message_ts: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Retrieve a permalink URL for a specific extant message
+        https://docs.slack.dev/reference/methods/chat.getPermalink
+        """
+        kwargs.update({"channel": channel, "message_ts": message_ts})
+        return self.api_call("chat.getPermalink", http_verb="GET", params=kwargs)
+
+    def chat_meMessage(
+        self,
+        *,
+        channel: str,
+        text: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Share a me message into a channel.
+        https://docs.slack.dev/reference/methods/chat.meMessage
+        """
+        kwargs.update({"channel": channel, "text": text})
+        return self.api_call("chat.meMessage", params=kwargs)
+
+    def chat_postEphemeral(
+        self,
+        *,
+        channel: str,
+        user: str,
+        text: Optional[str] = None,
+        as_user: Optional[bool] = None,
+        attachments: Optional[Union[str, Sequence[Union[Dict, Attachment]]]] = None,
+        blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None,
+        thread_ts: Optional[str] = None,
+        icon_emoji: Optional[str] = None,
+        icon_url: Optional[str] = None,
+        link_names: Optional[bool] = None,
+        username: Optional[str] = None,
+        parse: Optional[str] = None,
+        markdown_text: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Sends an ephemeral message to a user in a channel.
+        https://docs.slack.dev/reference/methods/chat.postEphemeral
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "user": user,
+                "text": text,
+                "as_user": as_user,
+                "attachments": attachments,
+                "blocks": blocks,
+                "thread_ts": thread_ts,
+                "icon_emoji": icon_emoji,
+                "icon_url": icon_url,
+                "link_names": link_names,
+                "username": username,
+                "parse": parse,
+                "markdown_text": markdown_text,
+            }
+        )
+        _parse_web_class_objects(kwargs)
+        kwargs = _remove_none_values(kwargs)
+        _warn_if_message_text_content_is_missing("chat.postEphemeral", kwargs)
+        # NOTE: intentionally using json over params for the API methods using blocks/attachments
+        return self.api_call("chat.postEphemeral", json=kwargs)
+
+    def chat_postMessage(
+        self,
+        *,
+        channel: str,
+        text: Optional[str] = None,
+        as_user: Optional[bool] = None,
+        attachments: Optional[Union[str, Sequence[Union[Dict, Attachment]]]] = None,
+        blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None,
+        thread_ts: Optional[str] = None,
+        reply_broadcast: Optional[bool] = None,
+        unfurl_links: Optional[bool] = None,
+        unfurl_media: Optional[bool] = None,
+        container_id: Optional[str] = None,
+        icon_emoji: Optional[str] = None,
+        icon_url: Optional[str] = None,
+        mrkdwn: Optional[bool] = None,
+        link_names: Optional[bool] = None,
+        username: Optional[str] = None,
+        parse: Optional[str] = None,  # none, full
+        metadata: Optional[Union[Dict, Metadata, EventAndEntityMetadata]] = None,
+        markdown_text: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Sends a message to a channel.
+        https://docs.slack.dev/reference/methods/chat.postMessage
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "text": text,
+                "as_user": as_user,
+                "attachments": attachments,
+                "blocks": blocks,
+                "thread_ts": thread_ts,
+                "reply_broadcast": reply_broadcast,
+                "unfurl_links": unfurl_links,
+                "unfurl_media": unfurl_media,
+                "container_id": container_id,
+                "icon_emoji": icon_emoji,
+                "icon_url": icon_url,
+                "mrkdwn": mrkdwn,
+                "link_names": link_names,
+                "username": username,
+                "parse": parse,
+                "metadata": metadata,
+                "markdown_text": markdown_text,
+            }
+        )
+        _parse_web_class_objects(kwargs)
+        kwargs = _remove_none_values(kwargs)
+        _warn_if_message_text_content_is_missing("chat.postMessage", kwargs)
+        # NOTE: intentionally using json over params for the API methods using blocks/attachments
+        return self.api_call("chat.postMessage", json=kwargs)
+
+    def chat_scheduleMessage(
+        self,
+        *,
+        channel: str,
+        post_at: Union[str, int],
+        text: Optional[str] = None,
+        as_user: Optional[bool] = None,
+        attachments: Optional[Union[str, Sequence[Union[Dict, Attachment]]]] = None,
+        blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None,
+        thread_ts: Optional[str] = None,
+        parse: Optional[str] = None,
+        reply_broadcast: Optional[bool] = None,
+        unfurl_links: Optional[bool] = None,
+        unfurl_media: Optional[bool] = None,
+        link_names: Optional[bool] = None,
+        metadata: Optional[Union[Dict, Metadata]] = None,
+        markdown_text: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Schedules a message.
+        https://docs.slack.dev/reference/methods/chat.scheduleMessage
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "post_at": post_at,
+                "text": text,
+                "as_user": as_user,
+                "attachments": attachments,
+                "blocks": blocks,
+                "thread_ts": thread_ts,
+                "reply_broadcast": reply_broadcast,
+                "parse": parse,
+                "unfurl_links": unfurl_links,
+                "unfurl_media": unfurl_media,
+                "link_names": link_names,
+                "metadata": metadata,
+                "markdown_text": markdown_text,
+            }
+        )
+        _parse_web_class_objects(kwargs)
+        kwargs = _remove_none_values(kwargs)
+        _warn_if_message_text_content_is_missing("chat.scheduleMessage", kwargs)
+        # NOTE: intentionally using json over params for the API methods using blocks/attachments
+        return self.api_call("chat.scheduleMessage", json=kwargs)
+
+    def chat_scheduledMessages_list(
+        self,
+        *,
+        channel: Optional[str] = None,
+        cursor: Optional[str] = None,
+        latest: Optional[str] = None,
+        limit: Optional[int] = None,
+        oldest: Optional[str] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Lists all scheduled messages.
+        https://docs.slack.dev/reference/methods/chat.scheduledMessages.list
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "cursor": cursor,
+                "latest": latest,
+                "limit": limit,
+                "oldest": oldest,
+                "team_id": team_id,
+            }
+        )
+        return self.api_call("chat.scheduledMessages.list", params=kwargs)
+
+    def chat_startStream(
+        self,
+        *,
+        channel: str,
+        thread_ts: str,
+        markdown_text: Optional[str] = None,
+        recipient_team_id: Optional[str] = None,
+        recipient_user_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Starts a new streaming conversation.
+        https://docs.slack.dev/reference/methods/chat.startStream
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "thread_ts": thread_ts,
+                "markdown_text": markdown_text,
+                "recipient_team_id": recipient_team_id,
+                "recipient_user_id": recipient_user_id,
+            }
+        )
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("chat.startStream", json=kwargs)
+
+    def chat_stopStream(
+        self,
+        *,
+        channel: str,
+        ts: str,
+        markdown_text: Optional[str] = None,
+        blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None,
+        metadata: Optional[Union[Dict, Metadata]] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Stops a streaming conversation.
+        https://docs.slack.dev/reference/methods/chat.stopStream
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "ts": ts,
+                "markdown_text": markdown_text,
+                "blocks": blocks,
+                "metadata": metadata,
+            }
+        )
+        _parse_web_class_objects(kwargs)
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("chat.stopStream", json=kwargs)
+
+    def chat_stream(
+        self,
+        *,
+        buffer_size: int = 256,
+        channel: str,
+        thread_ts: str,
+        recipient_team_id: Optional[str] = None,
+        recipient_user_id: Optional[str] = None,
+        **kwargs,
+    ) -> ChatStream:
+        """Stream markdown text into a conversation.
+
+        This method starts a new chat stream in a conversation that can be appended to. After appending an entire message,
+        the stream can be stopped with concluding arguments such as "blocks" for gathering feedback.
+
+        The following methods are used:
+
+        - chat.startStream: Starts a new streaming conversation.
+          [Reference](https://docs.slack.dev/reference/methods/chat.startStream).
+        - chat.appendStream: Appends text to an existing streaming conversation.
+          [Reference](https://docs.slack.dev/reference/methods/chat.appendStream).
+        - chat.stopStream: Stops a streaming conversation.
+          [Reference](https://docs.slack.dev/reference/methods/chat.stopStream).
+
+        Args:
+            buffer_size: The length of markdown_text to buffer in-memory before calling a stream method. Increasing this
+              value decreases the number of method calls made for the same amount of text, which is useful to avoid rate
+              limits. Default: 256.
+            channel: An encoded ID that represents a channel, private group, or DM.
+            thread_ts: Provide another message's ts value to reply to. Streamed messages should always be replies to a user
+              request.
+            recipient_team_id: The encoded ID of the team the user receiving the streaming text belongs to. Required when
+              streaming to channels.
+            recipient_user_id: The encoded ID of the user to receive the streaming text. Required when streaming to channels.
+            **kwargs: Additional arguments passed to the underlying API calls.
+
+        Returns:
+            ChatStream instance for managing the stream
+
+        Example:
+            ```python
+            streamer = client.chat_stream(
+                channel="C0123456789",
+                thread_ts="1700000001.123456",
+                recipient_team_id="T0123456789",
+                recipient_user_id="U0123456789",
+            )
+            streamer.append(markdown_text="**hello wo")
+            streamer.append(markdown_text="rld!**")
+            streamer.stop()
+            ```
+        """
+        return ChatStream(
+            self,
+            logger=self._logger,
+            channel=channel,
+            thread_ts=thread_ts,
+            recipient_team_id=recipient_team_id,
+            recipient_user_id=recipient_user_id,
+            buffer_size=buffer_size,
+            **kwargs,
+        )
+
+    def chat_unfurl(
+        self,
+        *,
+        channel: Optional[str] = None,
+        ts: Optional[str] = None,
+        source: Optional[str] = None,
+        unfurl_id: Optional[str] = None,
+        unfurls: Optional[Dict[str, Dict]] = None,  # or user_auth_*
+        metadata: Optional[Union[Dict, EventAndEntityMetadata]] = None,
+        user_auth_blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None,
+        user_auth_message: Optional[str] = None,
+        user_auth_required: Optional[bool] = None,
+        user_auth_url: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Provide custom unfurl behavior for user-posted URLs.
+        https://docs.slack.dev/reference/methods/chat.unfurl
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "ts": ts,
+                "source": source,
+                "unfurl_id": unfurl_id,
+                "unfurls": unfurls,
+                "metadata": metadata,
+                "user_auth_blocks": user_auth_blocks,
+                "user_auth_message": user_auth_message,
+                "user_auth_required": user_auth_required,
+                "user_auth_url": user_auth_url,
+            }
+        )
+        _parse_web_class_objects(kwargs)  # for user_auth_blocks
+        kwargs = _remove_none_values(kwargs)
+        # NOTE: intentionally using json over params for API methods using blocks/attachments
+        return self.api_call("chat.unfurl", json=kwargs)
+
+    def chat_update(
+        self,
+        *,
+        channel: str,
+        ts: str,
+        text: Optional[str] = None,
+        attachments: Optional[Union[str, Sequence[Union[Dict, Attachment]]]] = None,
+        blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None,
+        as_user: Optional[bool] = None,
+        file_ids: Optional[Union[str, Sequence[str]]] = None,
+        link_names: Optional[bool] = None,
+        parse: Optional[str] = None,  # none, full
+        reply_broadcast: Optional[bool] = None,
+        metadata: Optional[Union[Dict, Metadata]] = None,
+        markdown_text: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Updates a message in a channel.
+        https://docs.slack.dev/reference/methods/chat.update
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "ts": ts,
+                "text": text,
+                "attachments": attachments,
+                "blocks": blocks,
+                "as_user": as_user,
+                "link_names": link_names,
+                "parse": parse,
+                "reply_broadcast": reply_broadcast,
+                "metadata": metadata,
+                "markdown_text": markdown_text,
+            }
+        )
+        if isinstance(file_ids, (list, tuple)):
+            kwargs.update({"file_ids": ",".join(file_ids)})
+        else:
+            kwargs.update({"file_ids": file_ids})
+        _parse_web_class_objects(kwargs)
+        kwargs = _remove_none_values(kwargs)
+        _warn_if_message_text_content_is_missing("chat.update", kwargs)
+        # NOTE: intentionally using json over params for API methods using blocks/attachments
+        return self.api_call("chat.update", json=kwargs)
+
+    def conversations_acceptSharedInvite(
+        self,
+        *,
+        channel_name: str,
+        channel_id: Optional[str] = None,
+        invite_id: Optional[str] = None,
+        free_trial_accepted: Optional[bool] = None,
+        is_private: Optional[bool] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Accepts an invitation to a Slack Connect channel.
+        https://docs.slack.dev/reference/methods/conversations.acceptSharedInvite
+        """
+        if channel_id is None and invite_id is None:
+            raise e.SlackRequestError("Either channel_id or invite_id must be provided.")
+        kwargs.update(
+            {
+                "channel_name": channel_name,
+                "channel_id": channel_id,
+                "invite_id": invite_id,
+                "free_trial_accepted": free_trial_accepted,
+                "is_private": is_private,
+                "team_id": team_id,
+            }
+        )
+        return self.api_call("conversations.acceptSharedInvite", http_verb="POST", params=kwargs)
+
+    def conversations_approveSharedInvite(
+        self,
+        *,
+        invite_id: str,
+        target_team: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Approves an invitation to a Slack Connect channel.
+        https://docs.slack.dev/reference/methods/conversations.approveSharedInvite
+        """
+        kwargs.update({"invite_id": invite_id, "target_team": target_team})
+        return self.api_call("conversations.approveSharedInvite", http_verb="POST", params=kwargs)
+
+    def conversations_archive(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Archives a conversation.
+        https://docs.slack.dev/reference/methods/conversations.archive
+        """
+        kwargs.update({"channel": channel})
+        return self.api_call("conversations.archive", params=kwargs)
+
+    def conversations_close(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Closes a direct message or multi-person direct message.
+        https://docs.slack.dev/reference/methods/conversations.close
+        """
+        kwargs.update({"channel": channel})
+        return self.api_call("conversations.close", params=kwargs)
+
+    def conversations_create(
+        self,
+        *,
+        name: str,
+        is_private: Optional[bool] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Initiates a public or private channel-based conversation
+        https://docs.slack.dev/reference/methods/conversations.create
+        """
+        kwargs.update({"name": name, "is_private": is_private, "team_id": team_id})
+        return self.api_call("conversations.create", params=kwargs)
+
+    def conversations_declineSharedInvite(
+        self,
+        *,
+        invite_id: str,
+        target_team: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Declines a Slack Connect channel invite.
+        https://docs.slack.dev/reference/methods/conversations.declineSharedInvite
+        """
+        kwargs.update({"invite_id": invite_id, "target_team": target_team})
+        return self.api_call("conversations.declineSharedInvite", http_verb="GET", params=kwargs)
+
+    def conversations_externalInvitePermissions_set(
+        self, *, action: str, channel: str, target_team: str, **kwargs
+    ) -> SlackResponse:
+        """Sets a team in a shared External Limited channel to a shared Slack Connect channel or vice versa.
+        https://docs.slack.dev/reference/methods/conversations.externalInvitePermissions.set
+        """
+        kwargs.update(
+            {
+                "action": action,
+                "channel": channel,
+                "target_team": target_team,
+            }
+        )
+        return self.api_call("conversations.externalInvitePermissions.set", params=kwargs)
+
+    def conversations_history(
+        self,
+        *,
+        channel: str,
+        cursor: Optional[str] = None,
+        inclusive: Optional[bool] = None,
+        include_all_metadata: Optional[bool] = None,
+        latest: Optional[str] = None,
+        limit: Optional[int] = None,
+        oldest: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Fetches a conversation's history of messages and events.
+        https://docs.slack.dev/reference/methods/conversations.history
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "cursor": cursor,
+                "inclusive": inclusive,
+                "include_all_metadata": include_all_metadata,
+                "limit": limit,
+                "latest": latest,
+                "oldest": oldest,
+            }
+        )
+        return self.api_call("conversations.history", http_verb="GET", params=kwargs)
+
+    def conversations_info(
+        self,
+        *,
+        channel: str,
+        include_locale: Optional[bool] = None,
+        include_num_members: Optional[bool] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Retrieve information about a conversation.
+        https://docs.slack.dev/reference/methods/conversations.info
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "include_locale": include_locale,
+                "include_num_members": include_num_members,
+            }
+        )
+        return self.api_call("conversations.info", http_verb="GET", params=kwargs)
+
+    def conversations_invite(
+        self,
+        *,
+        channel: str,
+        users: Union[str, Sequence[str]],
+        force: Optional[bool] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Invites users to a channel.
+        https://docs.slack.dev/reference/methods/conversations.invite
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "force": force,
+            }
+        )
+        if isinstance(users, (list, tuple)):
+            kwargs.update({"users": ",".join(users)})
+        else:
+            kwargs.update({"users": users})
+        return self.api_call("conversations.invite", params=kwargs)
+
+    def conversations_inviteShared(
+        self,
+        *,
+        channel: str,
+        emails: Optional[Union[str, Sequence[str]]] = None,
+        user_ids: Optional[Union[str, Sequence[str]]] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Sends an invitation to a Slack Connect channel.
+        https://docs.slack.dev/reference/methods/conversations.inviteShared
+        """
+        if emails is None and user_ids is None:
+            raise e.SlackRequestError("Either emails or user ids must be provided.")
+        kwargs.update({"channel": channel})
+        if isinstance(emails, (list, tuple)):
+            kwargs.update({"emails": ",".join(emails)})
+        else:
+            kwargs.update({"emails": emails})
+        if isinstance(user_ids, (list, tuple)):
+            kwargs.update({"user_ids": ",".join(user_ids)})
+        else:
+            kwargs.update({"user_ids": user_ids})
+        return self.api_call("conversations.inviteShared", http_verb="GET", params=kwargs)
+
+    def conversations_join(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Joins an existing conversation.
+        https://docs.slack.dev/reference/methods/conversations.join
+        """
+        kwargs.update({"channel": channel})
+        return self.api_call("conversations.join", params=kwargs)
+
+    def conversations_kick(
+        self,
+        *,
+        channel: str,
+        user: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Removes a user from a conversation.
+        https://docs.slack.dev/reference/methods/conversations.kick
+        """
+        kwargs.update({"channel": channel, "user": user})
+        return self.api_call("conversations.kick", params=kwargs)
+
+    def conversations_leave(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Leaves a conversation.
+        https://docs.slack.dev/reference/methods/conversations.leave
+        """
+        kwargs.update({"channel": channel})
+        return self.api_call("conversations.leave", params=kwargs)
+
+    def conversations_list(
+        self,
+        *,
+        cursor: Optional[str] = None,
+        exclude_archived: Optional[bool] = None,
+        limit: Optional[int] = None,
+        team_id: Optional[str] = None,
+        types: Optional[Union[str, Sequence[str]]] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Lists all channels in a Slack team.
+        https://docs.slack.dev/reference/methods/conversations.list
+        """
+        kwargs.update(
+            {
+                "cursor": cursor,
+                "exclude_archived": exclude_archived,
+                "limit": limit,
+                "team_id": team_id,
+            }
+        )
+        if isinstance(types, (list, tuple)):
+            kwargs.update({"types": ",".join(types)})
+        else:
+            kwargs.update({"types": types})
+        return self.api_call("conversations.list", http_verb="GET", params=kwargs)
+
+    def conversations_listConnectInvites(
+        self,
+        *,
+        count: Optional[int] = None,
+        cursor: Optional[str] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """List shared channel invites that have been generated
+        or received but have not yet been approved by all parties.
+        https://docs.slack.dev/reference/methods/conversations.listConnectInvites
+        """
+        kwargs.update({"count": count, "cursor": cursor, "team_id": team_id})
+        return self.api_call("conversations.listConnectInvites", params=kwargs)
+
+    def conversations_mark(
+        self,
+        *,
+        channel: str,
+        ts: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Sets the read cursor in a channel.
+        https://docs.slack.dev/reference/methods/conversations.mark
+        """
+        kwargs.update({"channel": channel, "ts": ts})
+        return self.api_call("conversations.mark", params=kwargs)
+
+    def conversations_members(
+        self,
+        *,
+        channel: str,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Retrieve members of a conversation.
+        https://docs.slack.dev/reference/methods/conversations.members
+        """
+        kwargs.update({"channel": channel, "cursor": cursor, "limit": limit})
+        return self.api_call("conversations.members", http_verb="GET", params=kwargs)
+
+    def conversations_open(
+        self,
+        *,
+        channel: Optional[str] = None,
+        return_im: Optional[bool] = None,
+        users: Optional[Union[str, Sequence[str]]] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Opens or resumes a direct message or multi-person direct message.
+        https://docs.slack.dev/reference/methods/conversations.open
+        """
+        if channel is None and users is None:
+            raise e.SlackRequestError("Either channel or users must be provided.")
+        kwargs.update({"channel": channel, "return_im": return_im})
+        if isinstance(users, (list, tuple)):
+            kwargs.update({"users": ",".join(users)})
+        else:
+            kwargs.update({"users": users})
+        return self.api_call("conversations.open", params=kwargs)
+
+    def conversations_rename(
+        self,
+        *,
+        channel: str,
+        name: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Renames a conversation.
+        https://docs.slack.dev/reference/methods/conversations.rename
+        """
+        kwargs.update({"channel": channel, "name": name})
+        return self.api_call("conversations.rename", params=kwargs)
+
+    def conversations_replies(
+        self,
+        *,
+        channel: str,
+        ts: str,
+        cursor: Optional[str] = None,
+        inclusive: Optional[bool] = None,
+        include_all_metadata: Optional[bool] = None,
+        latest: Optional[str] = None,
+        limit: Optional[int] = None,
+        oldest: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Retrieve a thread of messages posted to a conversation
+        https://docs.slack.dev/reference/methods/conversations.replies
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "ts": ts,
+                "cursor": cursor,
+                "inclusive": inclusive,
+                "include_all_metadata": include_all_metadata,
+                "limit": limit,
+                "latest": latest,
+                "oldest": oldest,
+            }
+        )
+        return self.api_call("conversations.replies", http_verb="GET", params=kwargs)
+
+    def conversations_requestSharedInvite_approve(
+        self,
+        *,
+        invite_id: str,
+        channel_id: Optional[str] = None,
+        is_external_limited: Optional[str] = None,
+        message: Optional[Dict[str, Any]] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Approve a request to add an external user to a channel. This also sends them a Slack Connect invite.
+        https://docs.slack.dev/reference/methods/conversations.requestSharedInvite.approve
+        """
+        kwargs.update(
+            {
+                "invite_id": invite_id,
+                "channel_id": channel_id,
+                "is_external_limited": is_external_limited,
+            }
+        )
+        if message is not None:
+            kwargs.update({"message": json.dumps(message)})
+        return self.api_call("conversations.requestSharedInvite.approve", params=kwargs)
+
+    def conversations_requestSharedInvite_deny(
+        self,
+        *,
+        invite_id: str,
+        message: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Deny a request to invite an external user to a channel.
+        https://docs.slack.dev/reference/methods/conversations.requestSharedInvite.deny
+        """
+        kwargs.update({"invite_id": invite_id, "message": message})
+        return self.api_call("conversations.requestSharedInvite.deny", params=kwargs)
+
+    def conversations_requestSharedInvite_list(
+        self,
+        *,
+        cursor: Optional[str] = None,
+        include_approved: Optional[bool] = None,
+        include_denied: Optional[bool] = None,
+        include_expired: Optional[bool] = None,
+        invite_ids: Optional[Union[str, Sequence[str]]] = None,
+        limit: Optional[int] = None,
+        user_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Lists requests to add external users to channels with ability to filter.
+        https://docs.slack.dev/reference/methods/conversations.requestSharedInvite.list
+        """
+        kwargs.update(
+            {
+                "cursor": cursor,
+                "include_approved": include_approved,
+                "include_denied": include_denied,
+                "include_expired": include_expired,
+                "limit": limit,
+                "user_id": user_id,
+            }
+        )
+        if invite_ids is not None:
+            if isinstance(invite_ids, (list, tuple)):
+                kwargs.update({"invite_ids": ",".join(invite_ids)})
+            else:
+                kwargs.update({"invite_ids": invite_ids})
+        return self.api_call("conversations.requestSharedInvite.list", params=kwargs)
+
+    def conversations_setPurpose(
+        self,
+        *,
+        channel: str,
+        purpose: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Sets the purpose for a conversation.
+        https://docs.slack.dev/reference/methods/conversations.setPurpose
+        """
+        kwargs.update({"channel": channel, "purpose": purpose})
+        return self.api_call("conversations.setPurpose", params=kwargs)
+
+    def conversations_setTopic(
+        self,
+        *,
+        channel: str,
+        topic: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Sets the topic for a conversation.
+        https://docs.slack.dev/reference/methods/conversations.setTopic
+        """
+        kwargs.update({"channel": channel, "topic": topic})
+        return self.api_call("conversations.setTopic", params=kwargs)
+
+    def conversations_unarchive(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Reverses conversation archival.
+        https://docs.slack.dev/reference/methods/conversations.unarchive
+        """
+        kwargs.update({"channel": channel})
+        return self.api_call("conversations.unarchive", params=kwargs)
+
+    def conversations_canvases_create(
+        self,
+        *,
+        channel_id: str,
+        document_content: Dict[str, str],
+        **kwargs,
+    ) -> SlackResponse:
+        """Create a Channel Canvas for a channel
+        https://docs.slack.dev/reference/methods/conversations.canvases.create
+        """
+        kwargs.update({"channel_id": channel_id, "document_content": document_content})
+        return self.api_call("conversations.canvases.create", json=kwargs)
+
+    def dialog_open(
+        self,
+        *,
+        dialog: Dict[str, Any],
+        trigger_id: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Open a dialog with a user.
+        https://docs.slack.dev/reference/methods/dialog.open
+        """
+        kwargs.update({"dialog": dialog, "trigger_id": trigger_id})
+        kwargs = _remove_none_values(kwargs)
+        # NOTE: As the dialog can be a dict, this API call works only with json format.
+        return self.api_call("dialog.open", json=kwargs)
+
+    def dnd_endDnd(
+        self,
+        **kwargs,
+    ) -> SlackResponse:
+        """Ends the current user's Do Not Disturb session immediately.
+        https://docs.slack.dev/reference/methods/dnd.endDnd
+        """
+        return self.api_call("dnd.endDnd", params=kwargs)
+
+    def dnd_endSnooze(
+        self,
+        **kwargs,
+    ) -> SlackResponse:
+        """Ends the current user's snooze mode immediately.
+        https://docs.slack.dev/reference/methods/dnd.endSnooze
+        """
+        return self.api_call("dnd.endSnooze", params=kwargs)
+
+    def dnd_info(
+        self,
+        *,
+        team_id: Optional[str] = None,
+        user: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Retrieves a user's current Do Not Disturb status.
+        https://docs.slack.dev/reference/methods/dnd.info
+        """
+        kwargs.update({"team_id": team_id, "user": user})
+        return self.api_call("dnd.info", http_verb="GET", params=kwargs)
+
+    def dnd_setSnooze(
+        self,
+        *,
+        num_minutes: Union[int, str],
+        **kwargs,
+    ) -> SlackResponse:
+        """Turns on Do Not Disturb mode for the current user, or changes its duration.
+        https://docs.slack.dev/reference/methods/dnd.setSnooze
+        """
+        kwargs.update({"num_minutes": num_minutes})
+        return self.api_call("dnd.setSnooze", http_verb="GET", params=kwargs)
+
+    def dnd_teamInfo(
+        self,
+        users: Union[str, Sequence[str]],
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Retrieves the Do Not Disturb status for users on a team.
+        https://docs.slack.dev/reference/methods/dnd.teamInfo
+        """
+        if isinstance(users, (list, tuple)):
+            kwargs.update({"users": ",".join(users)})
+        else:
+            kwargs.update({"users": users})
+        kwargs.update({"team_id": team_id})
+        return self.api_call("dnd.teamInfo", http_verb="GET", params=kwargs)
+
+    def emoji_list(
+        self,
+        include_categories: Optional[bool] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Lists custom emoji for a team.
+        https://docs.slack.dev/reference/methods/emoji.list
+        """
+        kwargs.update({"include_categories": include_categories})
+        return self.api_call("emoji.list", http_verb="GET", params=kwargs)
+
+    def entity_presentDetails(
+        self,
+        trigger_id: str,
+        metadata: Optional[Union[Dict, EntityMetadata]] = None,
+        user_auth_required: Optional[bool] = None,
+        user_auth_url: Optional[str] = None,
+        error: Optional[Dict[str, Any]] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Provides entity details for the flexpane.
+        https://docs.slack.dev/reference/methods/entity.presentDetails/
+        """
+        kwargs.update({"trigger_id": trigger_id})
+        if metadata is not None:
+            kwargs.update({"metadata": metadata})
+        if user_auth_required is not None:
+            kwargs.update({"user_auth_required": user_auth_required})
+        if user_auth_url is not None:
+            kwargs.update({"user_auth_url": user_auth_url})
+        if error is not None:
+            kwargs.update({"error": error})
+        _parse_web_class_objects(kwargs)
+        return self.api_call("entity.presentDetails", json=kwargs)
+
+    def files_comments_delete(
+        self,
+        *,
+        file: str,
+        id: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Deletes an existing comment on a file.
+        https://docs.slack.dev/reference/methods/files.comments.delete
+        """
+        kwargs.update({"file": file, "id": id})
+        return self.api_call("files.comments.delete", params=kwargs)
+
+    def files_delete(
+        self,
+        *,
+        file: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Deletes a file.
+        https://docs.slack.dev/reference/methods/files.delete
+        """
+        kwargs.update({"file": file})
+        return self.api_call("files.delete", params=kwargs)
+
+    def files_info(
+        self,
+        *,
+        file: str,
+        count: Optional[int] = None,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        page: Optional[int] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Gets information about a team file.
+        https://docs.slack.dev/reference/methods/files.info
+        """
+        kwargs.update(
+            {
+                "file": file,
+                "count": count,
+                "cursor": cursor,
+                "limit": limit,
+                "page": page,
+            }
+        )
+        return self.api_call("files.info", http_verb="GET", params=kwargs)
+
+    def files_list(
+        self,
+        *,
+        channel: Optional[str] = None,
+        count: Optional[int] = None,
+        page: Optional[int] = None,
+        show_files_hidden_by_limit: Optional[bool] = None,
+        team_id: Optional[str] = None,
+        ts_from: Optional[str] = None,
+        ts_to: Optional[str] = None,
+        types: Optional[Union[str, Sequence[str]]] = None,
+        user: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Lists & filters team files.
+        https://docs.slack.dev/reference/methods/files.list
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "count": count,
+                "page": page,
+                "show_files_hidden_by_limit": show_files_hidden_by_limit,
+                "team_id": team_id,
+                "ts_from": ts_from,
+                "ts_to": ts_to,
+                "user": user,
+            }
+        )
+        if isinstance(types, (list, tuple)):
+            kwargs.update({"types": ",".join(types)})
+        else:
+            kwargs.update({"types": types})
+        return self.api_call("files.list", http_verb="GET", params=kwargs)
+
+    def files_remote_info(
+        self,
+        *,
+        external_id: Optional[str] = None,
+        file: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Retrieve information about a remote file added to Slack.
+        https://docs.slack.dev/reference/methods/files.remote.info
+        """
+        kwargs.update({"external_id": external_id, "file": file})
+        return self.api_call("files.remote.info", http_verb="GET", params=kwargs)
+
+    def files_remote_list(
+        self,
+        *,
+        channel: Optional[str] = None,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        ts_from: Optional[str] = None,
+        ts_to: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Retrieve information about a remote file added to Slack.
+        https://docs.slack.dev/reference/methods/files.remote.list
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "cursor": cursor,
+                "limit": limit,
+                "ts_from": ts_from,
+                "ts_to": ts_to,
+            }
+        )
+        return self.api_call("files.remote.list", http_verb="GET", params=kwargs)
+
+    def files_remote_add(
+        self,
+        *,
+        external_id: str,
+        external_url: str,
+        title: str,
+        filetype: Optional[str] = None,
+        indexable_file_contents: Optional[Union[str, bytes, IOBase]] = None,
+        preview_image: Optional[Union[str, bytes, IOBase]] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Adds a file from a remote service.
+        https://docs.slack.dev/reference/methods/files.remote.add
+        """
+        kwargs.update(
+            {
+                "external_id": external_id,
+                "external_url": external_url,
+                "title": title,
+                "filetype": filetype,
+            }
+        )
+        files = None
+        # preview_image (file): Preview of the document via multipart/form-data.
+        if preview_image is not None or indexable_file_contents is not None:
+            files = {
+                "preview_image": preview_image,
+                "indexable_file_contents": indexable_file_contents,
+            }
+
+        return self.api_call(
+            # Intentionally using "POST" method over "GET" here
+            "files.remote.add",
+            http_verb="POST",
+            data=kwargs,
+            files=files,
+        )
+
+    def files_remote_update(
+        self,
+        *,
+        external_id: Optional[str] = None,
+        external_url: Optional[str] = None,
+        file: Optional[str] = None,
+        title: Optional[str] = None,
+        filetype: Optional[str] = None,
+        indexable_file_contents: Optional[str] = None,
+        preview_image: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Updates an existing remote file.
+        https://docs.slack.dev/reference/methods/files.remote.update
+        """
+        kwargs.update(
+            {
+                "external_id": external_id,
+                "external_url": external_url,
+                "file": file,
+                "title": title,
+                "filetype": filetype,
+            }
+        )
+        files = None
+        # preview_image (file): Preview of the document via multipart/form-data.
+        if preview_image is not None or indexable_file_contents is not None:
+            files = {
+                "preview_image": preview_image,
+                "indexable_file_contents": indexable_file_contents,
+            }
+
+        return self.api_call(
+            # Intentionally using "POST" method over "GET" here
+            "files.remote.update",
+            http_verb="POST",
+            data=kwargs,
+            files=files,
+        )
+
+    def files_remote_remove(
+        self,
+        *,
+        external_id: Optional[str] = None,
+        file: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Remove a remote file.
+        https://docs.slack.dev/reference/methods/files.remote.remove
+        """
+        kwargs.update({"external_id": external_id, "file": file})
+        return self.api_call("files.remote.remove", http_verb="POST", params=kwargs)
+
+    def files_remote_share(
+        self,
+        *,
+        channels: Union[str, Sequence[str]],
+        external_id: Optional[str] = None,
+        file: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Share a remote file into a channel.
+        https://docs.slack.dev/reference/methods/files.remote.share
+        """
+        if external_id is None and file is None:
+            raise e.SlackRequestError("Either external_id or file must be provided.")
+        if isinstance(channels, (list, tuple)):
+            kwargs.update({"channels": ",".join(channels)})
+        else:
+            kwargs.update({"channels": channels})
+        kwargs.update({"external_id": external_id, "file": file})
+        return self.api_call("files.remote.share", http_verb="GET", params=kwargs)
+
+    def files_revokePublicURL(
+        self,
+        *,
+        file: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Revokes public/external sharing access for a file
+        https://docs.slack.dev/reference/methods/files.revokePublicURL
+        """
+        kwargs.update({"file": file})
+        return self.api_call("files.revokePublicURL", params=kwargs)
+
+    def files_sharedPublicURL(
+        self,
+        *,
+        file: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Enables a file for public/external sharing.
+        https://docs.slack.dev/reference/methods/files.sharedPublicURL
+        """
+        kwargs.update({"file": file})
+        return self.api_call("files.sharedPublicURL", params=kwargs)
+
+    def files_upload(
+        self,
+        *,
+        file: Optional[Union[str, bytes, IOBase]] = None,
+        content: Optional[Union[str, bytes]] = None,
+        filename: Optional[str] = None,
+        filetype: Optional[str] = None,
+        initial_comment: Optional[str] = None,
+        thread_ts: Optional[str] = None,
+        title: Optional[str] = None,
+        channels: Optional[Union[str, Sequence[str]]] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Uploads or creates a file.
+        https://docs.slack.dev/reference/methods/files.upload
+        """
+        _print_files_upload_v2_suggestion()
+
+        if file is None and content is None:
+            raise e.SlackRequestError("The file or content argument must be specified.")
+        if file is not None and content is not None:
+            raise e.SlackRequestError("You cannot specify both the file and the content argument.")
+
+        if isinstance(channels, (list, tuple)):
+            kwargs.update({"channels": ",".join(channels)})
+        else:
+            kwargs.update({"channels": channels})
+        kwargs.update(
+            {
+                "filename": filename,
+                "filetype": filetype,
+                "initial_comment": initial_comment,
+                "thread_ts": thread_ts,
+                "title": title,
+            }
+        )
+        if file:
+            if kwargs.get("filename") is None and isinstance(file, str):
+                # use the local filename if filename is missing
+                if kwargs.get("filename") is None:
+                    kwargs["filename"] = file.split(os.path.sep)[-1]
+            return self.api_call("files.upload", files={"file": file}, data=kwargs)
+        else:
+            kwargs["content"] = content
+            return self.api_call("files.upload", data=kwargs)
+
+    def files_upload_v2(
+        self,
+        *,
+        # for sending a single file
+        filename: Optional[str] = None,  # you can skip this only when sending along with content parameter
+        file: Optional[Union[str, bytes, IOBase, os.PathLike]] = None,
+        content: Optional[Union[str, bytes]] = None,
+        title: Optional[str] = None,
+        alt_txt: Optional[str] = None,
+        snippet_type: Optional[str] = None,
+        # To upload multiple files at a time
+        file_uploads: Optional[List[Dict[str, Any]]] = None,
+        channel: Optional[str] = None,
+        channels: Optional[List[str]] = None,
+        initial_comment: Optional[str] = None,
+        thread_ts: Optional[str] = None,
+        request_file_info: bool = True,  # since v3.23, this flag is no longer necessary
+        **kwargs,
+    ) -> SlackResponse:
+        """This wrapper method provides an easy way to upload files using the following endpoints:
+
+        - step1: https://docs.slack.dev/reference/methods/files.getUploadURLExternal
+
+        - step2: "https://files.slack.com/upload/v1/..." URLs returned from files.getUploadURLExternal API
+
+        - step3: https://docs.slack.dev/reference/methods/files.completeUploadExternal
+            and https://docs.slack.dev/reference/methods/files.info
+
+        """
+        if file is None and content is None and file_uploads is None:
+            raise e.SlackRequestError("Any of file, content, and file_uploads must be specified.")
+        if file is not None and content is not None:
+            raise e.SlackRequestError("You cannot specify both the file and the content argument.")
+
+        # deprecated arguments:
+        filetype = kwargs.get("filetype")
+
+        if filetype is not None:
+            warnings.warn("The filetype parameter is no longer supported. Please remove it from the arguments.")
+
+        # step1: files.getUploadURLExternal per file
+        files: List[Dict[str, Any]] = []
+        if file_uploads is not None:
+            for f in file_uploads:
+                files.append(_to_v2_file_upload_item(f))
+        else:
+            f = _to_v2_file_upload_item(
+                {
+                    "filename": filename,
+                    "file": file,
+                    "content": content,
+                    "title": title,
+                    "alt_txt": alt_txt,
+                    "snippet_type": snippet_type,
+                }
+            )
+            files.append(f)
+
+        for f in files:
+            url_response = self.files_getUploadURLExternal(
+                filename=f.get("filename"),  # type: ignore[arg-type]
+                length=f.get("length"),  # type: ignore[arg-type]
+                alt_txt=f.get("alt_txt"),
+                snippet_type=f.get("snippet_type"),
+                token=kwargs.get("token"),
+            )
+            _validate_for_legacy_client(url_response)
+            f["file_id"] = url_response.get("file_id")  # type: ignore[union-attr, unused-ignore]
+            f["upload_url"] = url_response.get("upload_url")  # type: ignore[union-attr, unused-ignore]
+
+        # step2: "https://files.slack.com/upload/v1/..." per file
+        for f in files:
+            upload_result = self._upload_file(
+                url=f["upload_url"],
+                data=f["data"],
+                logger=self._logger,
+                timeout=self.timeout,
+                proxy=self.proxy,
+                ssl=self.ssl,
+            )
+            if upload_result.status != 200:
+                status = upload_result.status
+                body = upload_result.body
+                message = (
+                    "Failed to upload a file "
+                    f"(status: {status}, body: {body}, filename: {f.get('filename')}, title: {f.get('title')})"
+                )
+                raise e.SlackRequestError(message)
+
+        # step3: files.completeUploadExternal with all the sets of (file_id + title)
+        completion = self.files_completeUploadExternal(
+            files=[{"id": f["file_id"], "title": f["title"]} for f in files],
+            channel_id=channel,
+            channels=channels,
+            initial_comment=initial_comment,
+            thread_ts=thread_ts,
+            **kwargs,
+        )
+        if len(completion.get("files")) == 1:  # type: ignore[arg-type, union-attr, unused-ignore]
+            completion.data["file"] = completion.get("files")[0]  # type: ignore[index, union-attr, unused-ignore]
+        return completion
+
+    def files_getUploadURLExternal(
+        self,
+        *,
+        filename: str,
+        length: int,
+        alt_txt: Optional[str] = None,
+        snippet_type: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Gets a URL for an edge external upload.
+        https://docs.slack.dev/reference/methods/files.getUploadURLExternal
+        """
+        kwargs.update(
+            {
+                "filename": filename,
+                "length": length,
+                "alt_txt": alt_txt,
+                "snippet_type": snippet_type,
+            }
+        )
+        return self.api_call("files.getUploadURLExternal", params=kwargs)
+
+    def files_completeUploadExternal(
+        self,
+        *,
+        files: List[Dict[str, str]],
+        channel_id: Optional[str] = None,
+        channels: Optional[List[str]] = None,
+        initial_comment: Optional[str] = None,
+        thread_ts: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Finishes an upload started with files.getUploadURLExternal.
+        https://docs.slack.dev/reference/methods/files.completeUploadExternal
+        """
+        _files = [{k: v for k, v in f.items() if v is not None} for f in files]
+        kwargs.update(
+            {
+                "files": json.dumps(_files),
+                "channel_id": channel_id,
+                "initial_comment": initial_comment,
+                "thread_ts": thread_ts,
+            }
+        )
+        if channels:
+            kwargs["channels"] = ",".join(channels)
+        return self.api_call("files.completeUploadExternal", params=kwargs)
+
+    def functions_completeSuccess(
+        self,
+        *,
+        function_execution_id: str,
+        outputs: Dict[str, Any],
+        **kwargs,
+    ) -> SlackResponse:
+        """Signal the successful completion of a function
+        https://docs.slack.dev/reference/methods/functions.completeSuccess
+        """
+        kwargs.update({"function_execution_id": function_execution_id, "outputs": json.dumps(outputs)})
+        return self.api_call("functions.completeSuccess", params=kwargs)
+
+    def functions_completeError(
+        self,
+        *,
+        function_execution_id: str,
+        error: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Signal the failure to execute a function
+        https://docs.slack.dev/reference/methods/functions.completeError
+        """
+        kwargs.update({"function_execution_id": function_execution_id, "error": error})
+        return self.api_call("functions.completeError", params=kwargs)
+
+    # --------------------------
+    # Deprecated: groups.*
+    # You can use conversations.* APIs instead.
+    # https://docs.slack.dev/changelog/2020-01-deprecating-antecedents-to-the-conversations-api/
+    # --------------------------
+
+    def groups_archive(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Archives a private channel."""
+        kwargs.update({"channel": channel})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("groups.archive", json=kwargs)
+
+    def groups_create(
+        self,
+        *,
+        name: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Creates a private channel."""
+        kwargs.update({"name": name})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("groups.create", json=kwargs)
+
+    def groups_createChild(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Clones and archives a private channel."""
+        kwargs.update({"channel": channel})
+        return self.api_call("groups.createChild", http_verb="GET", params=kwargs)
+
+    def groups_history(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Fetches history of messages and events from a private channel."""
+        kwargs.update({"channel": channel})
+        return self.api_call("groups.history", http_verb="GET", params=kwargs)
+
+    def groups_info(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Gets information about a private channel."""
+        kwargs.update({"channel": channel})
+        return self.api_call("groups.info", http_verb="GET", params=kwargs)
+
+    def groups_invite(
+        self,
+        *,
+        channel: str,
+        user: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Invites a user to a private channel."""
+        kwargs.update({"channel": channel, "user": user})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("groups.invite", json=kwargs)
+
+    def groups_kick(
+        self,
+        *,
+        channel: str,
+        user: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Removes a user from a private channel."""
+        kwargs.update({"channel": channel, "user": user})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("groups.kick", json=kwargs)
+
+    def groups_leave(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Leaves a private channel."""
+        kwargs.update({"channel": channel})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("groups.leave", json=kwargs)
+
+    def groups_list(
+        self,
+        **kwargs,
+    ) -> SlackResponse:
+        """Lists private channels that the calling user has access to."""
+        return self.api_call("groups.list", http_verb="GET", params=kwargs)
+
+    def groups_mark(
+        self,
+        *,
+        channel: str,
+        ts: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Sets the read cursor in a private channel."""
+        kwargs.update({"channel": channel, "ts": ts})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("groups.mark", json=kwargs)
+
+    def groups_open(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Opens a private channel."""
+        kwargs.update({"channel": channel})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("groups.open", json=kwargs)
+
+    def groups_rename(
+        self,
+        *,
+        channel: str,
+        name: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Renames a private channel."""
+        kwargs.update({"channel": channel, "name": name})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("groups.rename", json=kwargs)
+
+    def groups_replies(
+        self,
+        *,
+        channel: str,
+        thread_ts: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Retrieve a thread of messages posted to a private channel"""
+        kwargs.update({"channel": channel, "thread_ts": thread_ts})
+        return self.api_call("groups.replies", http_verb="GET", params=kwargs)
+
+    def groups_setPurpose(
+        self,
+        *,
+        channel: str,
+        purpose: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Sets the purpose for a private channel."""
+        kwargs.update({"channel": channel, "purpose": purpose})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("groups.setPurpose", json=kwargs)
+
+    def groups_setTopic(
+        self,
+        *,
+        channel: str,
+        topic: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Sets the topic for a private channel."""
+        kwargs.update({"channel": channel, "topic": topic})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("groups.setTopic", json=kwargs)
+
+    def groups_unarchive(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Unarchives a private channel."""
+        kwargs.update({"channel": channel})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("groups.unarchive", json=kwargs)
+
+    # --------------------------
+    # Deprecated: im.*
+    # You can use conversations.* APIs instead.
+    # https://docs.slack.dev/changelog/2020-01-deprecating-antecedents-to-the-conversations-api/
+    # --------------------------
+
+    def im_close(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Close a direct message channel."""
+        kwargs.update({"channel": channel})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("im.close", json=kwargs)
+
+    def im_history(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Fetches history of messages and events from direct message channel."""
+        kwargs.update({"channel": channel})
+        return self.api_call("im.history", http_verb="GET", params=kwargs)
+
+    def im_list(
+        self,
+        **kwargs,
+    ) -> SlackResponse:
+        """Lists direct message channels for the calling user."""
+        return self.api_call("im.list", http_verb="GET", params=kwargs)
+
+    def im_mark(
+        self,
+        *,
+        channel: str,
+        ts: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Sets the read cursor in a direct message channel."""
+        kwargs.update({"channel": channel, "ts": ts})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("im.mark", json=kwargs)
+
+    def im_open(
+        self,
+        *,
+        user: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Opens a direct message channel."""
+        kwargs.update({"user": user})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("im.open", json=kwargs)
+
+    def im_replies(
+        self,
+        *,
+        channel: str,
+        thread_ts: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Retrieve a thread of messages posted to a direct message conversation"""
+        kwargs.update({"channel": channel, "thread_ts": thread_ts})
+        return self.api_call("im.replies", http_verb="GET", params=kwargs)
+
+    # --------------------------
+
+    def migration_exchange(
+        self,
+        *,
+        users: Union[str, Sequence[str]],
+        team_id: Optional[str] = None,
+        to_old: Optional[bool] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """For Enterprise Grid workspaces, map local user IDs to global user IDs
+        https://docs.slack.dev/reference/methods/migration.exchange
+        """
+        if isinstance(users, (list, tuple)):
+            kwargs.update({"users": ",".join(users)})
+        else:
+            kwargs.update({"users": users})
+        kwargs.update({"team_id": team_id, "to_old": to_old})
+        return self.api_call("migration.exchange", http_verb="GET", params=kwargs)
+
+    # --------------------------
+    # Deprecated: mpim.*
+    # You can use conversations.* APIs instead.
+    # https://docs.slack.dev/changelog/2020-01-deprecating-antecedents-to-the-conversations-api/
+    # --------------------------
+
+    def mpim_close(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Closes a multiparty direct message channel."""
+        kwargs.update({"channel": channel})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("mpim.close", json=kwargs)
+
+    def mpim_history(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Fetches history of messages and events from a multiparty direct message."""
+        kwargs.update({"channel": channel})
+        return self.api_call("mpim.history", http_verb="GET", params=kwargs)
+
+    def mpim_list(
+        self,
+        **kwargs,
+    ) -> SlackResponse:
+        """Lists multiparty direct message channels for the calling user."""
+        return self.api_call("mpim.list", http_verb="GET", params=kwargs)
+
+    def mpim_mark(
+        self,
+        *,
+        channel: str,
+        ts: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Sets the read cursor in a multiparty direct message channel."""
+        kwargs.update({"channel": channel, "ts": ts})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("mpim.mark", json=kwargs)
+
+    def mpim_open(
+        self,
+        *,
+        users: Union[str, Sequence[str]],
+        **kwargs,
+    ) -> SlackResponse:
+        """This method opens a multiparty direct message."""
+        if isinstance(users, (list, tuple)):
+            kwargs.update({"users": ",".join(users)})
+        else:
+            kwargs.update({"users": users})
+        return self.api_call("mpim.open", params=kwargs)
+
+    def mpim_replies(
+        self,
+        *,
+        channel: str,
+        thread_ts: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Retrieve a thread of messages posted to a direct message conversation from a
+        multiparty direct message.
+        """
+        kwargs.update({"channel": channel, "thread_ts": thread_ts})
+        return self.api_call("mpim.replies", http_verb="GET", params=kwargs)
+
+    # --------------------------
+
+    def oauth_v2_access(
+        self,
+        *,
+        client_id: str,
+        client_secret: str,
+        # This field is required when processing the OAuth redirect URL requests
+        # while it's absent for token rotation
+        code: Optional[str] = None,
+        redirect_uri: Optional[str] = None,
+        # This field is required for token rotation
+        grant_type: Optional[str] = None,
+        # This field is required for token rotation
+        refresh_token: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Exchanges a temporary OAuth verifier code for an access token.
+        https://docs.slack.dev/reference/methods/oauth.v2.access
+        """
+        if redirect_uri is not None:
+            kwargs.update({"redirect_uri": redirect_uri})
+        if code is not None:
+            kwargs.update({"code": code})
+        if grant_type is not None:
+            kwargs.update({"grant_type": grant_type})
+        if refresh_token is not None:
+            kwargs.update({"refresh_token": refresh_token})
+        return self.api_call(
+            "oauth.v2.access",
+            data=kwargs,
+            auth={"client_id": client_id, "client_secret": client_secret},
+        )
+
+    def oauth_access(
+        self,
+        *,
+        client_id: str,
+        client_secret: str,
+        code: str,
+        redirect_uri: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Exchanges a temporary OAuth verifier code for an access token.
+        https://docs.slack.dev/reference/methods/oauth.access
+        """
+        if redirect_uri is not None:
+            kwargs.update({"redirect_uri": redirect_uri})
+        kwargs.update({"code": code})
+        return self.api_call(
+            "oauth.access",
+            data=kwargs,
+            auth={"client_id": client_id, "client_secret": client_secret},
+        )
+
+    def oauth_v2_exchange(
+        self,
+        *,
+        token: str,
+        client_id: str,
+        client_secret: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Exchanges a legacy access token for a new expiring access token and refresh token
+        https://docs.slack.dev/reference/methods/oauth.v2.exchange
+        """
+        kwargs.update({"client_id": client_id, "client_secret": client_secret, "token": token})
+        return self.api_call("oauth.v2.exchange", params=kwargs)
+
+    def openid_connect_token(
+        self,
+        client_id: str,
+        client_secret: str,
+        code: Optional[str] = None,
+        redirect_uri: Optional[str] = None,
+        grant_type: Optional[str] = None,
+        refresh_token: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Exchanges a temporary OAuth verifier code for an access token for Sign in with Slack.
+        https://docs.slack.dev/reference/methods/openid.connect.token
+        """
+        if redirect_uri is not None:
+            kwargs.update({"redirect_uri": redirect_uri})
+        if code is not None:
+            kwargs.update({"code": code})
+        if grant_type is not None:
+            kwargs.update({"grant_type": grant_type})
+        if refresh_token is not None:
+            kwargs.update({"refresh_token": refresh_token})
+        return self.api_call(
+            "openid.connect.token",
+            data=kwargs,
+            auth={"client_id": client_id, "client_secret": client_secret},
+        )
+
+    def openid_connect_userInfo(
+        self,
+        **kwargs,
+    ) -> SlackResponse:
+        """Get the identity of a user who has authorized Sign in with Slack.
+        https://docs.slack.dev/reference/methods/openid.connect.userInfo
+        """
+        return self.api_call("openid.connect.userInfo", params=kwargs)
+
+    def pins_add(
+        self,
+        *,
+        channel: str,
+        timestamp: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Pins an item to a channel.
+        https://docs.slack.dev/reference/methods/pins.add
+        """
+        kwargs.update({"channel": channel, "timestamp": timestamp})
+        return self.api_call("pins.add", params=kwargs)
+
+    def pins_list(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Lists items pinned to a channel.
+        https://docs.slack.dev/reference/methods/pins.list
+        """
+        kwargs.update({"channel": channel})
+        return self.api_call("pins.list", http_verb="GET", params=kwargs)
+
+    def pins_remove(
+        self,
+        *,
+        channel: str,
+        timestamp: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Un-pins an item from a channel.
+        https://docs.slack.dev/reference/methods/pins.remove
+        """
+        kwargs.update({"channel": channel, "timestamp": timestamp})
+        return self.api_call("pins.remove", params=kwargs)
+
+    def reactions_add(
+        self,
+        *,
+        channel: str,
+        name: str,
+        timestamp: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Adds a reaction to an item.
+        https://docs.slack.dev/reference/methods/reactions.add
+        """
+        kwargs.update({"channel": channel, "name": name, "timestamp": timestamp})
+        return self.api_call("reactions.add", params=kwargs)
+
+    def reactions_get(
+        self,
+        *,
+        channel: Optional[str] = None,
+        file: Optional[str] = None,
+        file_comment: Optional[str] = None,
+        full: Optional[bool] = None,
+        timestamp: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Gets reactions for an item.
+        https://docs.slack.dev/reference/methods/reactions.get
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "file": file,
+                "file_comment": file_comment,
+                "full": full,
+                "timestamp": timestamp,
+            }
+        )
+        return self.api_call("reactions.get", http_verb="GET", params=kwargs)
+
+    def reactions_list(
+        self,
+        *,
+        count: Optional[int] = None,
+        cursor: Optional[str] = None,
+        full: Optional[bool] = None,
+        limit: Optional[int] = None,
+        page: Optional[int] = None,
+        team_id: Optional[str] = None,
+        user: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Lists reactions made by a user.
+        https://docs.slack.dev/reference/methods/reactions.list
+        """
+        kwargs.update(
+            {
+                "count": count,
+                "cursor": cursor,
+                "full": full,
+                "limit": limit,
+                "page": page,
+                "team_id": team_id,
+                "user": user,
+            }
+        )
+        return self.api_call("reactions.list", http_verb="GET", params=kwargs)
+
+    def reactions_remove(
+        self,
+        *,
+        name: str,
+        channel: Optional[str] = None,
+        file: Optional[str] = None,
+        file_comment: Optional[str] = None,
+        timestamp: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Removes a reaction from an item.
+        https://docs.slack.dev/reference/methods/reactions.remove
+        """
+        kwargs.update(
+            {
+                "name": name,
+                "channel": channel,
+                "file": file,
+                "file_comment": file_comment,
+                "timestamp": timestamp,
+            }
+        )
+        return self.api_call("reactions.remove", params=kwargs)
+
+    def reminders_add(
+        self,
+        *,
+        text: str,
+        time: str,
+        team_id: Optional[str] = None,
+        user: Optional[str] = None,
+        recurrence: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Creates a reminder.
+        https://docs.slack.dev/reference/methods/reminders.add
+        """
+        kwargs.update(
+            {
+                "text": text,
+                "time": time,
+                "team_id": team_id,
+                "user": user,
+                "recurrence": recurrence,
+            }
+        )
+        return self.api_call("reminders.add", params=kwargs)
+
+    def reminders_complete(
+        self,
+        *,
+        reminder: str,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Marks a reminder as complete.
+        https://docs.slack.dev/reference/methods/reminders.complete
+        """
+        kwargs.update({"reminder": reminder, "team_id": team_id})
+        return self.api_call("reminders.complete", params=kwargs)
+
+    def reminders_delete(
+        self,
+        *,
+        reminder: str,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Deletes a reminder.
+        https://docs.slack.dev/reference/methods/reminders.delete
+        """
+        kwargs.update({"reminder": reminder, "team_id": team_id})
+        return self.api_call("reminders.delete", params=kwargs)
+
+    def reminders_info(
+        self,
+        *,
+        reminder: str,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Gets information about a reminder.
+        https://docs.slack.dev/reference/methods/reminders.info
+        """
+        kwargs.update({"reminder": reminder, "team_id": team_id})
+        return self.api_call("reminders.info", http_verb="GET", params=kwargs)
+
+    def reminders_list(
+        self,
+        *,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Lists all reminders created by or for a given user.
+        https://docs.slack.dev/reference/methods/reminders.list
+        """
+        kwargs.update({"team_id": team_id})
+        return self.api_call("reminders.list", http_verb="GET", params=kwargs)
+
+    def rtm_connect(
+        self,
+        *,
+        batch_presence_aware: Optional[bool] = None,
+        presence_sub: Optional[bool] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Starts a Real Time Messaging session.
+        https://docs.slack.dev/reference/methods/rtm.connect
+        """
+        kwargs.update({"batch_presence_aware": batch_presence_aware, "presence_sub": presence_sub})
+        return self.api_call("rtm.connect", http_verb="GET", params=kwargs)
+
+    def rtm_start(
+        self,
+        *,
+        batch_presence_aware: Optional[bool] = None,
+        include_locale: Optional[bool] = None,
+        mpim_aware: Optional[bool] = None,
+        no_latest: Optional[bool] = None,
+        no_unreads: Optional[bool] = None,
+        presence_sub: Optional[bool] = None,
+        simple_latest: Optional[bool] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Starts a Real Time Messaging session.
+        https://docs.slack.dev/reference/methods/rtm.start
+        """
+        kwargs.update(
+            {
+                "batch_presence_aware": batch_presence_aware,
+                "include_locale": include_locale,
+                "mpim_aware": mpim_aware,
+                "no_latest": no_latest,
+                "no_unreads": no_unreads,
+                "presence_sub": presence_sub,
+                "simple_latest": simple_latest,
+            }
+        )
+        return self.api_call("rtm.start", http_verb="GET", params=kwargs)
+
+    def search_all(
+        self,
+        *,
+        query: str,
+        count: Optional[int] = None,
+        highlight: Optional[bool] = None,
+        page: Optional[int] = None,
+        sort: Optional[str] = None,
+        sort_dir: Optional[str] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Searches for messages and files matching a query.
+        https://docs.slack.dev/reference/methods/search.all
+        """
+        kwargs.update(
+            {
+                "query": query,
+                "count": count,
+                "highlight": highlight,
+                "page": page,
+                "sort": sort,
+                "sort_dir": sort_dir,
+                "team_id": team_id,
+            }
+        )
+        return self.api_call("search.all", http_verb="GET", params=kwargs)
+
+    def search_files(
+        self,
+        *,
+        query: str,
+        count: Optional[int] = None,
+        highlight: Optional[bool] = None,
+        page: Optional[int] = None,
+        sort: Optional[str] = None,
+        sort_dir: Optional[str] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Searches for files matching a query.
+        https://docs.slack.dev/reference/methods/search.files
+        """
+        kwargs.update(
+            {
+                "query": query,
+                "count": count,
+                "highlight": highlight,
+                "page": page,
+                "sort": sort,
+                "sort_dir": sort_dir,
+                "team_id": team_id,
+            }
+        )
+        return self.api_call("search.files", http_verb="GET", params=kwargs)
+
+    def search_messages(
+        self,
+        *,
+        query: str,
+        count: Optional[int] = None,
+        cursor: Optional[str] = None,
+        highlight: Optional[bool] = None,
+        page: Optional[int] = None,
+        sort: Optional[str] = None,
+        sort_dir: Optional[str] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Searches for messages matching a query.
+        https://docs.slack.dev/reference/methods/search.messages
+        """
+        kwargs.update(
+            {
+                "query": query,
+                "count": count,
+                "cursor": cursor,
+                "highlight": highlight,
+                "page": page,
+                "sort": sort,
+                "sort_dir": sort_dir,
+                "team_id": team_id,
+            }
+        )
+        return self.api_call("search.messages", http_verb="GET", params=kwargs)
+
+    def slackLists_access_delete(
+        self,
+        *,
+        list_id: str,
+        channel_ids: Optional[List[str]] = None,
+        user_ids: Optional[List[str]] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Revoke access to a List for specified entities.
+        https://docs.slack.dev/reference/methods/slackLists.access.delete
+        """
+        kwargs.update({"list_id": list_id, "channel_ids": channel_ids, "user_ids": user_ids})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("slackLists.access.delete", json=kwargs)
+
+    def slackLists_access_set(
+        self,
+        *,
+        list_id: str,
+        access_level: str,
+        channel_ids: Optional[List[str]] = None,
+        user_ids: Optional[List[str]] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Set the access level to a List for specified entities.
+        https://docs.slack.dev/reference/methods/slackLists.access.set
+        """
+        kwargs.update({"list_id": list_id, "access_level": access_level, "channel_ids": channel_ids, "user_ids": user_ids})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("slackLists.access.set", json=kwargs)
+
+    def slackLists_create(
+        self,
+        *,
+        name: str,
+        description_blocks: Optional[Union[str, Sequence[Union[Dict, RichTextBlock]]]] = None,
+        schema: Optional[List[Dict[str, Any]]] = None,
+        copy_from_list_id: Optional[str] = None,
+        include_copied_list_records: Optional[bool] = None,
+        todo_mode: Optional[bool] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Creates a List.
+        https://docs.slack.dev/reference/methods/slackLists.create
+        """
+        kwargs.update(
+            {
+                "name": name,
+                "description_blocks": description_blocks,
+                "schema": schema,
+                "copy_from_list_id": copy_from_list_id,
+                "include_copied_list_records": include_copied_list_records,
+                "todo_mode": todo_mode,
+            }
+        )
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("slackLists.create", json=kwargs)
+
+    def slackLists_download_get(
+        self,
+        *,
+        list_id: str,
+        job_id: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Retrieve List download URL from an export job to download List contents.
+        https://docs.slack.dev/reference/methods/slackLists.download.get
+        """
+        kwargs.update(
+            {
+                "list_id": list_id,
+                "job_id": job_id,
+            }
+        )
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("slackLists.download.get", json=kwargs)
+
+    def slackLists_download_start(
+        self,
+        *,
+        list_id: str,
+        include_archived: Optional[bool] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Initiate a job to export List contents.
+        https://docs.slack.dev/reference/methods/slackLists.download.start
+        """
+        kwargs.update(
+            {
+                "list_id": list_id,
+                "include_archived": include_archived,
+            }
+        )
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("slackLists.download.start", json=kwargs)
+
+    def slackLists_items_create(
+        self,
+        *,
+        list_id: str,
+        duplicated_item_id: Optional[str] = None,
+        parent_item_id: Optional[str] = None,
+        initial_fields: Optional[List[Dict[str, Any]]] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Add a new item to an existing List.
+        https://docs.slack.dev/reference/methods/slackLists.items.create
+        """
+        kwargs.update(
+            {
+                "list_id": list_id,
+                "duplicated_item_id": duplicated_item_id,
+                "parent_item_id": parent_item_id,
+                "initial_fields": initial_fields,
+            }
+        )
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("slackLists.items.create", json=kwargs)
+
+    def slackLists_items_delete(
+        self,
+        *,
+        list_id: str,
+        id: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Deletes an item from an existing List.
+        https://docs.slack.dev/reference/methods/slackLists.items.delete
+        """
+        kwargs.update(
+            {
+                "list_id": list_id,
+                "id": id,
+            }
+        )
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("slackLists.items.delete", json=kwargs)
+
+    def slackLists_items_deleteMultiple(
+        self,
+        *,
+        list_id: str,
+        ids: List[str],
+        **kwargs,
+    ) -> SlackResponse:
+        """Deletes multiple items from an existing List.
+        https://docs.slack.dev/reference/methods/slackLists.items.deleteMultiple
+        """
+        kwargs.update(
+            {
+                "list_id": list_id,
+                "ids": ids,
+            }
+        )
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("slackLists.items.deleteMultiple", json=kwargs)
+
+    def slackLists_items_info(
+        self,
+        *,
+        list_id: str,
+        id: str,
+        include_is_subscribed: Optional[bool] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Get a row from a List.
+        https://docs.slack.dev/reference/methods/slackLists.items.info
+        """
+        kwargs.update(
+            {
+                "list_id": list_id,
+                "id": id,
+                "include_is_subscribed": include_is_subscribed,
+            }
+        )
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("slackLists.items.info", json=kwargs)
+
+    def slackLists_items_list(
+        self,
+        *,
+        list_id: str,
+        limit: Optional[int] = None,
+        cursor: Optional[str] = None,
+        archived: Optional[bool] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Get records from a List.
+        https://docs.slack.dev/reference/methods/slackLists.items.list
+        """
+        kwargs.update(
+            {
+                "list_id": list_id,
+                "limit": limit,
+                "cursor": cursor,
+                "archived": archived,
+            }
+        )
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("slackLists.items.list", json=kwargs)
+
+    def slackLists_items_update(
+        self,
+        *,
+        list_id: str,
+        cells: List[Dict[str, Any]],
+        **kwargs,
+    ) -> SlackResponse:
+        """Updates cells in a List.
+        https://docs.slack.dev/reference/methods/slackLists.items.update
+        """
+        kwargs.update(
+            {
+                "list_id": list_id,
+                "cells": cells,
+            }
+        )
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("slackLists.items.update", json=kwargs)
+
+    def slackLists_update(
+        self,
+        *,
+        id: str,
+        name: Optional[str] = None,
+        description_blocks: Optional[Union[str, Sequence[Union[Dict, RichTextBlock]]]] = None,
+        todo_mode: Optional[bool] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Update a List.
+        https://docs.slack.dev/reference/methods/slackLists.update
+        """
+        kwargs.update(
+            {
+                "id": id,
+                "name": name,
+                "description_blocks": description_blocks,
+                "todo_mode": todo_mode,
+            }
+        )
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("slackLists.update", json=kwargs)
+
+    def stars_add(
+        self,
+        *,
+        channel: Optional[str] = None,
+        file: Optional[str] = None,
+        file_comment: Optional[str] = None,
+        timestamp: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Adds a star to an item.
+        https://docs.slack.dev/reference/methods/stars.add
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "file": file,
+                "file_comment": file_comment,
+                "timestamp": timestamp,
+            }
+        )
+        return self.api_call("stars.add", params=kwargs)
+
+    def stars_list(
+        self,
+        *,
+        count: Optional[int] = None,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        page: Optional[int] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Lists stars for a user.
+        https://docs.slack.dev/reference/methods/stars.list
+        """
+        kwargs.update(
+            {
+                "count": count,
+                "cursor": cursor,
+                "limit": limit,
+                "page": page,
+                "team_id": team_id,
+            }
+        )
+        return self.api_call("stars.list", http_verb="GET", params=kwargs)
+
+    def stars_remove(
+        self,
+        *,
+        channel: Optional[str] = None,
+        file: Optional[str] = None,
+        file_comment: Optional[str] = None,
+        timestamp: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Removes a star from an item.
+        https://docs.slack.dev/reference/methods/stars.remove
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "file": file,
+                "file_comment": file_comment,
+                "timestamp": timestamp,
+            }
+        )
+        return self.api_call("stars.remove", params=kwargs)
+
+    def team_accessLogs(
+        self,
+        *,
+        before: Optional[Union[int, str]] = None,
+        count: Optional[Union[int, str]] = None,
+        page: Optional[Union[int, str]] = None,
+        team_id: Optional[str] = None,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Gets the access logs for the current team.
+        https://docs.slack.dev/reference/methods/team.accessLogs
+        """
+        kwargs.update(
+            {
+                "before": before,
+                "count": count,
+                "page": page,
+                "team_id": team_id,
+                "cursor": cursor,
+                "limit": limit,
+            }
+        )
+        return self.api_call("team.accessLogs", http_verb="GET", params=kwargs)
+
+    def team_billableInfo(
+        self,
+        *,
+        team_id: Optional[str] = None,
+        user: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Gets billable users information for the current team.
+        https://docs.slack.dev/reference/methods/team.billableInfo
+        """
+        kwargs.update({"team_id": team_id, "user": user})
+        return self.api_call("team.billableInfo", http_verb="GET", params=kwargs)
+
+    def team_billing_info(
+        self,
+        **kwargs,
+    ) -> SlackResponse:
+        """Reads a workspace's billing plan information.
+        https://docs.slack.dev/reference/methods/team.billing.info
+        """
+        return self.api_call("team.billing.info", params=kwargs)
+
+    def team_externalTeams_disconnect(
+        self,
+        *,
+        target_team: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Disconnects an external organization.
+        https://docs.slack.dev/reference/methods/team.externalTeams.disconnect
+        """
+        kwargs.update(
+            {
+                "target_team": target_team,
+            }
+        )
+        return self.api_call("team.externalTeams.disconnect", params=kwargs)
+
+    def team_externalTeams_list(
+        self,
+        *,
+        connection_status_filter: Optional[str] = None,
+        slack_connect_pref_filter: Optional[Sequence[str]] = None,
+        sort_direction: Optional[str] = None,
+        sort_field: Optional[str] = None,
+        workspace_filter: Optional[Sequence[str]] = None,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Returns a list of all the external teams connected and details about the connection.
+        https://docs.slack.dev/reference/methods/team.externalTeams.list
+        """
+        kwargs.update(
+            {
+                "connection_status_filter": connection_status_filter,
+                "sort_direction": sort_direction,
+                "sort_field": sort_field,
+                "cursor": cursor,
+                "limit": limit,
+            }
+        )
+        if slack_connect_pref_filter is not None:
+            if isinstance(slack_connect_pref_filter, (list, tuple)):
+                kwargs.update({"slack_connect_pref_filter": ",".join(slack_connect_pref_filter)})
+            else:
+                kwargs.update({"slack_connect_pref_filter": slack_connect_pref_filter})
+        if workspace_filter is not None:
+            if isinstance(workspace_filter, (list, tuple)):
+                kwargs.update({"workspace_filter": ",".join(workspace_filter)})
+            else:
+                kwargs.update({"workspace_filter": workspace_filter})
+        return self.api_call("team.externalTeams.list", http_verb="GET", params=kwargs)
+
+    def team_info(
+        self,
+        *,
+        team: Optional[str] = None,
+        domain: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Gets information about the current team.
+        https://docs.slack.dev/reference/methods/team.info
+        """
+        kwargs.update({"team": team, "domain": domain})
+        return self.api_call("team.info", http_verb="GET", params=kwargs)
+
+    def team_integrationLogs(
+        self,
+        *,
+        app_id: Optional[str] = None,
+        change_type: Optional[str] = None,
+        count: Optional[Union[int, str]] = None,
+        page: Optional[Union[int, str]] = None,
+        service_id: Optional[str] = None,
+        team_id: Optional[str] = None,
+        user: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Gets the integration logs for the current team.
+        https://docs.slack.dev/reference/methods/team.integrationLogs
+        """
+        kwargs.update(
+            {
+                "app_id": app_id,
+                "change_type": change_type,
+                "count": count,
+                "page": page,
+                "service_id": service_id,
+                "team_id": team_id,
+                "user": user,
+            }
+        )
+        return self.api_call("team.integrationLogs", http_verb="GET", params=kwargs)
+
+    def team_profile_get(
+        self,
+        *,
+        visibility: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Retrieve a team's profile.
+        https://docs.slack.dev/reference/methods/team.profile.get
+        """
+        kwargs.update({"visibility": visibility})
+        return self.api_call("team.profile.get", http_verb="GET", params=kwargs)
+
+    def team_preferences_list(
+        self,
+        **kwargs,
+    ) -> SlackResponse:
+        """Retrieve a list of a workspace's team preferences.
+        https://docs.slack.dev/reference/methods/team.preferences.list
+        """
+        return self.api_call("team.preferences.list", params=kwargs)
+
+    def usergroups_create(
+        self,
+        *,
+        name: str,
+        channels: Optional[Union[str, Sequence[str]]] = None,
+        description: Optional[str] = None,
+        handle: Optional[str] = None,
+        include_count: Optional[bool] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Create a User Group
+        https://docs.slack.dev/reference/methods/usergroups.create
+        """
+        kwargs.update(
+            {
+                "name": name,
+                "description": description,
+                "handle": handle,
+                "include_count": include_count,
+                "team_id": team_id,
+            }
+        )
+        if isinstance(channels, (list, tuple)):
+            kwargs.update({"channels": ",".join(channels)})
+        else:
+            kwargs.update({"channels": channels})
+        return self.api_call("usergroups.create", params=kwargs)
+
+    def usergroups_disable(
+        self,
+        *,
+        usergroup: str,
+        include_count: Optional[bool] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Disable an existing User Group
+        https://docs.slack.dev/reference/methods/usergroups.disable
+        """
+        kwargs.update({"usergroup": usergroup, "include_count": include_count, "team_id": team_id})
+        return self.api_call("usergroups.disable", params=kwargs)
+
+    def usergroups_enable(
+        self,
+        *,
+        usergroup: str,
+        include_count: Optional[bool] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Enable a User Group
+        https://docs.slack.dev/reference/methods/usergroups.enable
+        """
+        kwargs.update({"usergroup": usergroup, "include_count": include_count, "team_id": team_id})
+        return self.api_call("usergroups.enable", params=kwargs)
+
+    def usergroups_list(
+        self,
+        *,
+        include_count: Optional[bool] = None,
+        include_disabled: Optional[bool] = None,
+        include_users: Optional[bool] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """List all User Groups for a team
+        https://docs.slack.dev/reference/methods/usergroups.list
+        """
+        kwargs.update(
+            {
+                "include_count": include_count,
+                "include_disabled": include_disabled,
+                "include_users": include_users,
+                "team_id": team_id,
+            }
+        )
+        return self.api_call("usergroups.list", http_verb="GET", params=kwargs)
+
+    def usergroups_update(
+        self,
+        *,
+        usergroup: str,
+        channels: Optional[Union[str, Sequence[str]]] = None,
+        description: Optional[str] = None,
+        handle: Optional[str] = None,
+        include_count: Optional[bool] = None,
+        name: Optional[str] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Update an existing User Group
+        https://docs.slack.dev/reference/methods/usergroups.update
+        """
+        kwargs.update(
+            {
+                "usergroup": usergroup,
+                "description": description,
+                "handle": handle,
+                "include_count": include_count,
+                "name": name,
+                "team_id": team_id,
+            }
+        )
+        if isinstance(channels, (list, tuple)):
+            kwargs.update({"channels": ",".join(channels)})
+        else:
+            kwargs.update({"channels": channels})
+        return self.api_call("usergroups.update", params=kwargs)
+
+    def usergroups_users_list(
+        self,
+        *,
+        usergroup: str,
+        include_disabled: Optional[bool] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """List all users in a User Group
+        https://docs.slack.dev/reference/methods/usergroups.users.list
+        """
+        kwargs.update(
+            {
+                "usergroup": usergroup,
+                "include_disabled": include_disabled,
+                "team_id": team_id,
+            }
+        )
+        return self.api_call("usergroups.users.list", http_verb="GET", params=kwargs)
+
+    def usergroups_users_update(
+        self,
+        *,
+        usergroup: str,
+        users: Union[str, Sequence[str]],
+        include_count: Optional[bool] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Update the list of users for a User Group
+        https://docs.slack.dev/reference/methods/usergroups.users.update
+        """
+        kwargs.update(
+            {
+                "usergroup": usergroup,
+                "include_count": include_count,
+                "team_id": team_id,
+            }
+        )
+        if isinstance(users, (list, tuple)):
+            kwargs.update({"users": ",".join(users)})
+        else:
+            kwargs.update({"users": users})
+        return self.api_call("usergroups.users.update", params=kwargs)
+
+    def users_conversations(
+        self,
+        *,
+        cursor: Optional[str] = None,
+        exclude_archived: Optional[bool] = None,
+        limit: Optional[int] = None,
+        team_id: Optional[str] = None,
+        types: Optional[Union[str, Sequence[str]]] = None,
+        user: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """List conversations the calling user may access.
+        https://docs.slack.dev/reference/methods/users.conversations
+        """
+        kwargs.update(
+            {
+                "cursor": cursor,
+                "exclude_archived": exclude_archived,
+                "limit": limit,
+                "team_id": team_id,
+                "user": user,
+            }
+        )
+        if isinstance(types, (list, tuple)):
+            kwargs.update({"types": ",".join(types)})
+        else:
+            kwargs.update({"types": types})
+        return self.api_call("users.conversations", http_verb="GET", params=kwargs)
+
+    def users_deletePhoto(
+        self,
+        **kwargs,
+    ) -> SlackResponse:
+        """Delete the user profile photo
+        https://docs.slack.dev/reference/methods/users.deletePhoto
+        """
+        return self.api_call("users.deletePhoto", http_verb="GET", params=kwargs)
+
+    def users_getPresence(
+        self,
+        *,
+        user: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Gets user presence information.
+        https://docs.slack.dev/reference/methods/users.getPresence
+        """
+        kwargs.update({"user": user})
+        return self.api_call("users.getPresence", http_verb="GET", params=kwargs)
+
+    def users_identity(
+        self,
+        **kwargs,
+    ) -> SlackResponse:
+        """Get a user's identity.
+        https://docs.slack.dev/reference/methods/users.identity
+        """
+        return self.api_call("users.identity", http_verb="GET", params=kwargs)
+
+    def users_info(
+        self,
+        *,
+        user: str,
+        include_locale: Optional[bool] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Gets information about a user.
+        https://docs.slack.dev/reference/methods/users.info
+        """
+        kwargs.update({"user": user, "include_locale": include_locale})
+        return self.api_call("users.info", http_verb="GET", params=kwargs)
+
+    def users_list(
+        self,
+        *,
+        cursor: Optional[str] = None,
+        include_locale: Optional[bool] = None,
+        limit: Optional[int] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Lists all users in a Slack team.
+        https://docs.slack.dev/reference/methods/users.list
+        """
+        kwargs.update(
+            {
+                "cursor": cursor,
+                "include_locale": include_locale,
+                "limit": limit,
+                "team_id": team_id,
+            }
+        )
+        return self.api_call("users.list", http_verb="GET", params=kwargs)
+
+    def users_lookupByEmail(
+        self,
+        *,
+        email: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Find a user with an email address.
+        https://docs.slack.dev/reference/methods/users.lookupByEmail
+        """
+        kwargs.update({"email": email})
+        return self.api_call("users.lookupByEmail", http_verb="GET", params=kwargs)
+
+    def users_setPhoto(
+        self,
+        *,
+        image: Union[str, IOBase],
+        crop_w: Optional[Union[int, str]] = None,
+        crop_x: Optional[Union[int, str]] = None,
+        crop_y: Optional[Union[int, str]] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Set the user profile photo
+        https://docs.slack.dev/reference/methods/users.setPhoto
+        """
+        kwargs.update({"crop_w": crop_w, "crop_x": crop_x, "crop_y": crop_y})
+        return self.api_call("users.setPhoto", files={"image": image}, data=kwargs)
+
+    def users_setPresence(
+        self,
+        *,
+        presence: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Manually sets user presence.
+        https://docs.slack.dev/reference/methods/users.setPresence
+        """
+        kwargs.update({"presence": presence})
+        return self.api_call("users.setPresence", params=kwargs)
+
+    def users_discoverableContacts_lookup(
+        self,
+        email: str,
+        **kwargs,
+    ) -> SlackResponse:
+        """Lookup an email address to see if someone is on Slack
+        https://docs.slack.dev/reference/methods/users.discoverableContacts.lookup
+        """
+        kwargs.update({"email": email})
+        return self.api_call("users.discoverableContacts.lookup", params=kwargs)
+
+    def users_profile_get(
+        self,
+        *,
+        user: Optional[str] = None,
+        include_labels: Optional[bool] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Retrieves a user's profile information.
+        https://docs.slack.dev/reference/methods/users.profile.get
+        """
+        kwargs.update({"user": user, "include_labels": include_labels})
+        return self.api_call("users.profile.get", http_verb="GET", params=kwargs)
+
+    def users_profile_set(
+        self,
+        *,
+        name: Optional[str] = None,
+        value: Optional[str] = None,
+        user: Optional[str] = None,
+        profile: Optional[Dict] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Set the profile information for a user.
+        https://docs.slack.dev/reference/methods/users.profile.set
+        """
+        kwargs.update(
+            {
+                "name": name,
+                "profile": profile,
+                "user": user,
+                "value": value,
+            }
+        )
+        kwargs = _remove_none_values(kwargs)
+        # NOTE: Intentionally using json for the "profile" parameter
+        return self.api_call("users.profile.set", json=kwargs)
+
+    def views_open(
+        self,
+        *,
+        trigger_id: Optional[str] = None,
+        interactivity_pointer: Optional[str] = None,
+        view: Union[dict, View],
+        **kwargs,
+    ) -> SlackResponse:
+        """Open a view for a user.
+        https://docs.slack.dev/reference/methods/views.open
+        See https://docs.slack.dev/surfaces/modals/ for details.
+        """
+        kwargs.update({"trigger_id": trigger_id, "interactivity_pointer": interactivity_pointer})
+        if isinstance(view, View):
+            kwargs.update({"view": view.to_dict()})
+        else:
+            kwargs.update({"view": view})
+        kwargs = _remove_none_values(kwargs)
+        # NOTE: Intentionally using json for the "view" parameter
+        return self.api_call("views.open", json=kwargs)
+
+    def views_push(
+        self,
+        *,
+        trigger_id: Optional[str] = None,
+        interactivity_pointer: Optional[str] = None,
+        view: Union[dict, View],
+        **kwargs,
+    ) -> SlackResponse:
+        """Push a view onto the stack of a root view.
+        Push a new view onto the existing view stack by passing a view
+        payload and a valid trigger_id generated from an interaction
+        within the existing modal.
+        Read the modals documentation (https://docs.slack.dev/surfaces/modals/)
+        to learn more about the lifecycle and intricacies of views.
+        https://docs.slack.dev/reference/methods/views.push
+        """
+        kwargs.update({"trigger_id": trigger_id, "interactivity_pointer": interactivity_pointer})
+        if isinstance(view, View):
+            kwargs.update({"view": view.to_dict()})
+        else:
+            kwargs.update({"view": view})
+        kwargs = _remove_none_values(kwargs)
+        # NOTE: Intentionally using json for the "view" parameter
+        return self.api_call("views.push", json=kwargs)
+
+    def views_update(
+        self,
+        *,
+        view: Union[dict, View],
+        external_id: Optional[str] = None,
+        view_id: Optional[str] = None,
+        hash: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Update an existing view.
+        Update a view by passing a new view definition along with the
+        view_id returned in views.open or the external_id.
+        See the modals documentation (https://docs.slack.dev/surfaces/modals/#updating_views)
+        to learn more about updating views and avoiding race conditions with the hash argument.
+        https://docs.slack.dev/reference/methods/views.update
+        """
+        if isinstance(view, View):
+            kwargs.update({"view": view.to_dict()})
+        else:
+            kwargs.update({"view": view})
+        if external_id:
+            kwargs.update({"external_id": external_id})
+        elif view_id:
+            kwargs.update({"view_id": view_id})
+        else:
+            raise e.SlackRequestError("Either view_id or external_id is required.")
+        kwargs.update({"hash": hash})
+        kwargs = _remove_none_values(kwargs)
+        # NOTE: Intentionally using json for the "view" parameter
+        return self.api_call("views.update", json=kwargs)
+
+    def views_publish(
+        self,
+        *,
+        user_id: str,
+        view: Union[dict, View],
+        hash: Optional[str] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Publish a static view for a User.
+        Create or update the view that comprises an
+        app's Home tab (https://docs.slack.dev/surfaces/app-home/)
+        https://docs.slack.dev/reference/methods/views.publish
+        """
+        kwargs.update({"user_id": user_id, "hash": hash})
+        if isinstance(view, View):
+            kwargs.update({"view": view.to_dict()})
+        else:
+            kwargs.update({"view": view})
+        kwargs = _remove_none_values(kwargs)
+        # NOTE: Intentionally using json for the "view" parameter
+        return self.api_call("views.publish", json=kwargs)
+
+    def workflows_featured_add(
+        self,
+        *,
+        channel_id: str,
+        trigger_ids: Union[str, Sequence[str]],
+        **kwargs,
+    ) -> SlackResponse:
+        """Add featured workflows to a channel.
+        https://docs.slack.dev/reference/methods/workflows.featured.add
+        """
+        kwargs.update({"channel_id": channel_id})
+        if isinstance(trigger_ids, (list, tuple)):
+            kwargs.update({"trigger_ids": ",".join(trigger_ids)})
+        else:
+            kwargs.update({"trigger_ids": trigger_ids})
+        return self.api_call("workflows.featured.add", params=kwargs)
+
+    def workflows_featured_list(
+        self,
+        *,
+        channel_ids: Union[str, Sequence[str]],
+        **kwargs,
+    ) -> SlackResponse:
+        """List the featured workflows for specified channels.
+        https://docs.slack.dev/reference/methods/workflows.featured.list
+        """
+        if isinstance(channel_ids, (list, tuple)):
+            kwargs.update({"channel_ids": ",".join(channel_ids)})
+        else:
+            kwargs.update({"channel_ids": channel_ids})
+        return self.api_call("workflows.featured.list", params=kwargs)
+
+    def workflows_featured_remove(
+        self,
+        *,
+        channel_id: str,
+        trigger_ids: Union[str, Sequence[str]],
+        **kwargs,
+    ) -> SlackResponse:
+        """Remove featured workflows from a channel.
+        https://docs.slack.dev/reference/methods/workflows.featured.remove
+        """
+        kwargs.update({"channel_id": channel_id})
+        if isinstance(trigger_ids, (list, tuple)):
+            kwargs.update({"trigger_ids": ",".join(trigger_ids)})
+        else:
+            kwargs.update({"trigger_ids": trigger_ids})
+        return self.api_call("workflows.featured.remove", params=kwargs)
+
+    def workflows_featured_set(
+        self,
+        *,
+        channel_id: str,
+        trigger_ids: Union[str, Sequence[str]],
+        **kwargs,
+    ) -> SlackResponse:
+        """Set featured workflows for a channel.
+        https://docs.slack.dev/reference/methods/workflows.featured.set
+        """
+        kwargs.update({"channel_id": channel_id})
+        if isinstance(trigger_ids, (list, tuple)):
+            kwargs.update({"trigger_ids": ",".join(trigger_ids)})
+        else:
+            kwargs.update({"trigger_ids": trigger_ids})
+        return self.api_call("workflows.featured.set", params=kwargs)
+
+    def workflows_stepCompleted(
+        self,
+        *,
+        workflow_step_execute_id: str,
+        outputs: Optional[dict] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Indicate a successful outcome of a workflow step's execution.
+        https://docs.slack.dev/reference/methods/workflows.stepCompleted
+        """
+        kwargs.update({"workflow_step_execute_id": workflow_step_execute_id})
+        if outputs is not None:
+            kwargs.update({"outputs": outputs})
+        kwargs = _remove_none_values(kwargs)
+        # NOTE: Intentionally using json for the "outputs" parameter
+        return self.api_call("workflows.stepCompleted", json=kwargs)
+
+    def workflows_stepFailed(
+        self,
+        *,
+        workflow_step_execute_id: str,
+        error: Dict[str, str],
+        **kwargs,
+    ) -> SlackResponse:
+        """Indicate an unsuccessful outcome of a workflow step's execution.
+        https://docs.slack.dev/reference/methods/workflows.stepFailed
+        """
+        kwargs.update(
+            {
+                "workflow_step_execute_id": workflow_step_execute_id,
+                "error": error,
+            }
+        )
+        kwargs = _remove_none_values(kwargs)
+        # NOTE: Intentionally using json for the "error" parameter
+        return self.api_call("workflows.stepFailed", json=kwargs)
+
+    def workflows_updateStep(
+        self,
+        *,
+        workflow_step_edit_id: str,
+        inputs: Optional[Dict[str, Any]] = None,
+        outputs: Optional[List[Dict[str, str]]] = None,
+        **kwargs,
+    ) -> SlackResponse:
+        """Update the configuration for a workflow extension step.
+        https://docs.slack.dev/reference/methods/workflows.updateStep
+        """
+        kwargs.update({"workflow_step_edit_id": workflow_step_edit_id})
+        if inputs is not None:
+            kwargs.update({"inputs": inputs})
+        if outputs is not None:
+            kwargs.update({"outputs": outputs})
+        kwargs = _remove_none_values(kwargs)
+        # NOTE: Intentionally using json for the "inputs" / "outputs" parameters
+        return self.api_call("workflows.updateStep", json=kwargs)
+
+

A WebClient allows apps to communicate with the Slack Platform's Web API.

+

https://docs.slack.dev/reference/methods

+

The Slack Web API is an interface for querying information from +and enacting change in a Slack workspace.

+

This client handles constructing and sending HTTP requests to Slack +as well as parsing any responses received into a SlackResponse.

+

Attributes

+
+
token : str
+
A string specifying an xoxp-* or xoxb-* token.
+
base_url : str
+
A string representing the Slack API base URL. +Default is 'https://slack.com/api/'
+
timeout : int
+
The maximum number of seconds the client will wait +to connect and receive a response from Slack. +Default is 30 seconds.
+
ssl : SSLContext
+
An ssl.SSLContext instance, helpful for specifying +your own custom certificate chain.
+
proxy : str
+
String representing a fully-qualified URL to a proxy through +which to route all requests to the Slack API. Even if this parameter +is not specified, if any of the following environment variables are +present, they will be loaded into this parameter: HTTPS_PROXY, +https_proxy, HTTP_PROXY or http_proxy.
+
headers : dict
+
Additional request headers to attach to all requests.
+
+

Methods

+

api_call: Constructs a request and executes the API call to Slack.

+

Example of recommended usage:

+
    import os
+    from slack_sdk import WebClient
+
+    client = WebClient(token=os.environ['SLACK_API_TOKEN'])
+    response = client.chat_postMessage(
+        channel='#random',
+        text="Hello world!")
+    assert response["ok"]
+    assert response["message"]["text"] == "Hello world!"
+
+

Example manually creating an API request:

+
    import os
+    from slack_sdk import WebClient
+
+    client = WebClient(token=os.environ['SLACK_API_TOKEN'])
+    response = client.api_call(
+        api_method='chat.postMessage',
+        json={'channel': '#random','text': "Hello world!"}
+    )
+    assert response["ok"]
+    assert response["message"]["text"] == "Hello world!"
+
+

Note

+

Any attributes or methods prefixed with _underscores are +intended to be "private" internal use only. They may be changed or +removed at anytime.

+

Ancestors

+ +

Methods

+
+
+def admin_analytics_getFile(self,
*,
type: str,
date: str | None = None,
metadata_only: bool | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_analytics_getFile(
+    self,
+    *,
+    type: str,
+    date: Optional[str] = None,
+    metadata_only: Optional[bool] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Retrieve analytics data for a given date, presented as a compressed JSON file
+    https://docs.slack.dev/reference/methods/admin.analytics.getFile
+    """
+    kwargs.update({"type": type})
+    if date is not None:
+        kwargs.update({"date": date})
+    if metadata_only is not None:
+        kwargs.update({"metadata_only": metadata_only})
+    return self.api_call("admin.analytics.getFile", params=kwargs)
+
+

Retrieve analytics data for a given date, presented as a compressed JSON file +https://docs.slack.dev/reference/methods/admin.analytics.getFile

+
+
+def admin_apps_activities_list(self,
*,
app_id: str | None = None,
component_id: str | None = None,
component_type: str | None = None,
log_event_type: str | None = None,
max_date_created: int | None = None,
min_date_created: int | None = None,
min_log_level: str | None = None,
sort_direction: str | None = None,
source: str | None = None,
team_id: str | None = None,
trace_id: str | None = None,
cursor: str | None = None,
limit: int | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_apps_activities_list(
+    self,
+    *,
+    app_id: Optional[str] = None,
+    component_id: Optional[str] = None,
+    component_type: Optional[str] = None,
+    log_event_type: Optional[str] = None,
+    max_date_created: Optional[int] = None,
+    min_date_created: Optional[int] = None,
+    min_log_level: Optional[str] = None,
+    sort_direction: Optional[str] = None,
+    source: Optional[str] = None,
+    team_id: Optional[str] = None,
+    trace_id: Optional[str] = None,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Get logs for a specified team/org
+    https://docs.slack.dev/reference/methods/admin.apps.activities.list
+    """
+    kwargs.update(
+        {
+            "app_id": app_id,
+            "component_id": component_id,
+            "component_type": component_type,
+            "log_event_type": log_event_type,
+            "max_date_created": max_date_created,
+            "min_date_created": min_date_created,
+            "min_log_level": min_log_level,
+            "sort_direction": sort_direction,
+            "source": source,
+            "team_id": team_id,
+            "trace_id": trace_id,
+            "cursor": cursor,
+            "limit": limit,
+        }
+    )
+    return self.api_call("admin.apps.activities.list", params=kwargs)
+
+ +
+
+def admin_apps_approve(self,
*,
app_id: str | None = None,
request_id: str | None = None,
enterprise_id: str | None = None,
team_id: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_apps_approve(
+    self,
+    *,
+    app_id: Optional[str] = None,
+    request_id: Optional[str] = None,
+    enterprise_id: Optional[str] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Approve an app for installation on a workspace.
+    Either app_id or request_id is required.
+    These IDs can be obtained either directly via the app_requested event,
+    or by the admin.apps.requests.list method.
+    https://docs.slack.dev/reference/methods/admin.apps.approve
+    """
+    if app_id:
+        kwargs.update({"app_id": app_id})
+    elif request_id:
+        kwargs.update({"request_id": request_id})
+    else:
+        raise e.SlackRequestError("The app_id or request_id argument must be specified.")
+
+    kwargs.update(
+        {
+            "enterprise_id": enterprise_id,
+            "team_id": team_id,
+        }
+    )
+    return self.api_call("admin.apps.approve", params=kwargs)
+
+

Approve an app for installation on a workspace. +Either app_id or request_id is required. +These IDs can be obtained either directly via the app_requested event, +or by the admin.apps.requests.list method. +https://docs.slack.dev/reference/methods/admin.apps.approve

+
+
+def admin_apps_approved_list(self,
*,
cursor: str | None = None,
limit: int | None = None,
enterprise_id: str | None = None,
team_id: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_apps_approved_list(
+    self,
+    *,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    enterprise_id: Optional[str] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """List approved apps for an org or workspace.
+    https://docs.slack.dev/reference/methods/admin.apps.approved.list
+    """
+    kwargs.update(
+        {
+            "cursor": cursor,
+            "limit": limit,
+            "enterprise_id": enterprise_id,
+            "team_id": team_id,
+        }
+    )
+    return self.api_call("admin.apps.approved.list", http_verb="GET", params=kwargs)
+
+

List approved apps for an org or workspace. +https://docs.slack.dev/reference/methods/admin.apps.approved.list

+
+
+def admin_apps_clearResolution(self,
*,
app_id: str,
enterprise_id: str | None = None,
team_id: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_apps_clearResolution(
+    self,
+    *,
+    app_id: str,
+    enterprise_id: Optional[str] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Clear an app resolution
+    https://docs.slack.dev/reference/methods/admin.apps.clearResolution
+    """
+    kwargs.update(
+        {
+            "app_id": app_id,
+            "enterprise_id": enterprise_id,
+            "team_id": team_id,
+        }
+    )
+    return self.api_call("admin.apps.clearResolution", http_verb="POST", params=kwargs)
+
+ +
+
+def admin_apps_config_lookup(self, *, app_ids: str | Sequence[str], **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_apps_config_lookup(
+    self,
+    *,
+    app_ids: Union[str, Sequence[str]],
+    **kwargs,
+) -> SlackResponse:
+    """Look up the app config for connectors by their IDs
+    https://docs.slack.dev/reference/methods/admin.apps.config.lookup
+    """
+    if isinstance(app_ids, (list, tuple)):
+        kwargs.update({"app_ids": ",".join(app_ids)})
+    else:
+        kwargs.update({"app_ids": app_ids})
+    return self.api_call("admin.apps.config.lookup", params=kwargs)
+
+

Look up the app config for connectors by their IDs +https://docs.slack.dev/reference/methods/admin.apps.config.lookup

+
+
+def admin_apps_config_set(self,
*,
app_id: str,
domain_restrictions: Dict[str, Any] | None = None,
workflow_auth_strategy: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_apps_config_set(
+    self,
+    *,
+    app_id: str,
+    domain_restrictions: Optional[Dict[str, Any]] = None,
+    workflow_auth_strategy: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Set the app config for a connector
+    https://docs.slack.dev/reference/methods/admin.apps.config.set
+    """
+    kwargs.update(
+        {
+            "app_id": app_id,
+            "workflow_auth_strategy": workflow_auth_strategy,
+        }
+    )
+    if domain_restrictions is not None:
+        kwargs.update({"domain_restrictions": json.dumps(domain_restrictions)})
+    return self.api_call("admin.apps.config.set", params=kwargs)
+
+ +
+
+def admin_apps_requests_cancel(self,
*,
request_id: str,
enterprise_id: str | None = None,
team_id: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_apps_requests_cancel(
+    self,
+    *,
+    request_id: str,
+    enterprise_id: Optional[str] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """List app requests for a team/workspace.
+    https://docs.slack.dev/reference/methods/admin.apps.requests.cancel
+    """
+    kwargs.update(
+        {
+            "request_id": request_id,
+            "enterprise_id": enterprise_id,
+            "team_id": team_id,
+        }
+    )
+    return self.api_call("admin.apps.requests.cancel", http_verb="POST", params=kwargs)
+
+ +
+
+def admin_apps_requests_list(self,
*,
cursor: str | None = None,
limit: int | None = None,
team_id: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_apps_requests_list(
+    self,
+    *,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """List app requests for a team/workspace.
+    https://docs.slack.dev/reference/methods/admin.apps.requests.list
+    """
+    kwargs.update(
+        {
+            "cursor": cursor,
+            "limit": limit,
+            "team_id": team_id,
+        }
+    )
+    return self.api_call("admin.apps.requests.list", http_verb="GET", params=kwargs)
+
+ +
+
+def admin_apps_restrict(self,
*,
app_id: str | None = None,
request_id: str | None = None,
enterprise_id: str | None = None,
team_id: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_apps_restrict(
+    self,
+    *,
+    app_id: Optional[str] = None,
+    request_id: Optional[str] = None,
+    enterprise_id: Optional[str] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Restrict an app for installation on a workspace.
+    Exactly one of the team_id or enterprise_id arguments is required, not both.
+    Either app_id or request_id is required. These IDs can be obtained either directly
+    via the app_requested event, or by the admin.apps.requests.list method.
+    https://docs.slack.dev/reference/methods/admin.apps.restrict
+    """
+    if app_id:
+        kwargs.update({"app_id": app_id})
+    elif request_id:
+        kwargs.update({"request_id": request_id})
+    else:
+        raise e.SlackRequestError("The app_id or request_id argument must be specified.")
+
+    kwargs.update(
+        {
+            "enterprise_id": enterprise_id,
+            "team_id": team_id,
+        }
+    )
+    return self.api_call("admin.apps.restrict", params=kwargs)
+
+

Restrict an app for installation on a workspace. +Exactly one of the team_id or enterprise_id arguments is required, not both. +Either app_id or request_id is required. These IDs can be obtained either directly +via the app_requested event, or by the admin.apps.requests.list method. +https://docs.slack.dev/reference/methods/admin.apps.restrict

+
+
+def admin_apps_restricted_list(self,
*,
cursor: str | None = None,
limit: int | None = None,
enterprise_id: str | None = None,
team_id: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_apps_restricted_list(
+    self,
+    *,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    enterprise_id: Optional[str] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """List restricted apps for an org or workspace.
+    https://docs.slack.dev/reference/methods/admin.apps.restricted.list
+    """
+    kwargs.update(
+        {
+            "cursor": cursor,
+            "limit": limit,
+            "enterprise_id": enterprise_id,
+            "team_id": team_id,
+        }
+    )
+    return self.api_call("admin.apps.restricted.list", http_verb="GET", params=kwargs)
+
+

List restricted apps for an org or workspace. +https://docs.slack.dev/reference/methods/admin.apps.restricted.list

+
+
+def admin_apps_uninstall(self,
*,
app_id: str,
enterprise_id: str | None = None,
team_ids: str | Sequence[str] | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_apps_uninstall(
+    self,
+    *,
+    app_id: str,
+    enterprise_id: Optional[str] = None,
+    team_ids: Optional[Union[str, Sequence[str]]] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Uninstall an app from one or many workspaces, or an entire enterprise organization.
+    With an org-level token, enterprise_id or team_ids is required.
+    https://docs.slack.dev/reference/methods/admin.apps.uninstall
+    """
+    kwargs.update({"app_id": app_id})
+    if enterprise_id is not None:
+        kwargs.update({"enterprise_id": enterprise_id})
+    if team_ids is not None:
+        if isinstance(team_ids, (list, tuple)):
+            kwargs.update({"team_ids": ",".join(team_ids)})
+        else:
+            kwargs.update({"team_ids": team_ids})
+    return self.api_call("admin.apps.uninstall", http_verb="POST", params=kwargs)
+
+

Uninstall an app from one or many workspaces, or an entire enterprise organization. +With an org-level token, enterprise_id or team_ids is required. +https://docs.slack.dev/reference/methods/admin.apps.uninstall

+
+
+def admin_auth_policy_assignEntities(self,
*,
entity_ids: str | Sequence[str],
policy_name: str,
entity_type: str,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_auth_policy_assignEntities(
+    self,
+    *,
+    entity_ids: Union[str, Sequence[str]],
+    policy_name: str,
+    entity_type: str,
+    **kwargs,
+) -> SlackResponse:
+    """Assign entities to a particular authentication policy.
+    https://docs.slack.dev/reference/methods/admin.auth.policy.assignEntities
+    """
+    if isinstance(entity_ids, (list, tuple)):
+        kwargs.update({"entity_ids": ",".join(entity_ids)})
+    else:
+        kwargs.update({"entity_ids": entity_ids})
+    kwargs.update({"policy_name": policy_name})
+    kwargs.update({"entity_type": entity_type})
+    return self.api_call("admin.auth.policy.assignEntities", http_verb="POST", params=kwargs)
+
+

Assign entities to a particular authentication policy. +https://docs.slack.dev/reference/methods/admin.auth.policy.assignEntities

+
+
+def admin_auth_policy_getEntities(self,
*,
policy_name: str,
cursor: str | None = None,
entity_type: str | None = None,
limit: int | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_auth_policy_getEntities(
+    self,
+    *,
+    policy_name: str,
+    cursor: Optional[str] = None,
+    entity_type: Optional[str] = None,
+    limit: Optional[int] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Fetch all the entities assigned to a particular authentication policy by name.
+    https://docs.slack.dev/reference/methods/admin.auth.policy.getEntities
+    """
+    kwargs.update({"policy_name": policy_name})
+    if cursor is not None:
+        kwargs.update({"cursor": cursor})
+    if entity_type is not None:
+        kwargs.update({"entity_type": entity_type})
+    if limit is not None:
+        kwargs.update({"limit": limit})
+    return self.api_call("admin.auth.policy.getEntities", http_verb="POST", params=kwargs)
+
+

Fetch all the entities assigned to a particular authentication policy by name. +https://docs.slack.dev/reference/methods/admin.auth.policy.getEntities

+
+
+def admin_auth_policy_removeEntities(self,
*,
entity_ids: str | Sequence[str],
policy_name: str,
entity_type: str,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_auth_policy_removeEntities(
+    self,
+    *,
+    entity_ids: Union[str, Sequence[str]],
+    policy_name: str,
+    entity_type: str,
+    **kwargs,
+) -> SlackResponse:
+    """Remove specified entities from a specified authentication policy.
+    https://docs.slack.dev/reference/methods/admin.auth.policy.removeEntities
+    """
+    if isinstance(entity_ids, (list, tuple)):
+        kwargs.update({"entity_ids": ",".join(entity_ids)})
+    else:
+        kwargs.update({"entity_ids": entity_ids})
+    kwargs.update({"policy_name": policy_name})
+    kwargs.update({"entity_type": entity_type})
+    return self.api_call("admin.auth.policy.removeEntities", http_verb="POST", params=kwargs)
+
+

Remove specified entities from a specified authentication policy. +https://docs.slack.dev/reference/methods/admin.auth.policy.removeEntities

+
+
+def admin_barriers_create(self,
*,
barriered_from_usergroup_ids: str | Sequence[str],
primary_usergroup_id: str,
restricted_subjects: str | Sequence[str],
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_barriers_create(
+    self,
+    *,
+    barriered_from_usergroup_ids: Union[str, Sequence[str]],
+    primary_usergroup_id: str,
+    restricted_subjects: Union[str, Sequence[str]],
+    **kwargs,
+) -> SlackResponse:
+    """Create an Information Barrier
+    https://docs.slack.dev/reference/methods/admin.barriers.create
+    """
+    kwargs.update({"primary_usergroup_id": primary_usergroup_id})
+    if isinstance(barriered_from_usergroup_ids, (list, tuple)):
+        kwargs.update({"barriered_from_usergroup_ids": ",".join(barriered_from_usergroup_ids)})
+    else:
+        kwargs.update({"barriered_from_usergroup_ids": barriered_from_usergroup_ids})
+    if isinstance(restricted_subjects, (list, tuple)):
+        kwargs.update({"restricted_subjects": ",".join(restricted_subjects)})
+    else:
+        kwargs.update({"restricted_subjects": restricted_subjects})
+    return self.api_call("admin.barriers.create", http_verb="POST", params=kwargs)
+
+ +
+
+def admin_barriers_delete(self, *, barrier_id: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_barriers_delete(
+    self,
+    *,
+    barrier_id: str,
+    **kwargs,
+) -> SlackResponse:
+    """Delete an existing Information Barrier
+    https://docs.slack.dev/reference/methods/admin.barriers.delete
+    """
+    kwargs.update({"barrier_id": barrier_id})
+    return self.api_call("admin.barriers.delete", http_verb="POST", params=kwargs)
+
+ +
+
+def admin_barriers_list(self, *, cursor: str | None = None, limit: int | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_barriers_list(
+    self,
+    *,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Get all Information Barriers for your organization
+    https://docs.slack.dev/reference/methods/admin.barriers.list"""
+    kwargs.update(
+        {
+            "cursor": cursor,
+            "limit": limit,
+        }
+    )
+    return self.api_call("admin.barriers.list", http_verb="GET", params=kwargs)
+
+

Get all Information Barriers for your organization +https://docs.slack.dev/reference/methods/admin.barriers.list

+
+
+def admin_barriers_update(self,
*,
barrier_id: str,
barriered_from_usergroup_ids: str | Sequence[str],
primary_usergroup_id: str,
restricted_subjects: str | Sequence[str],
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_barriers_update(
+    self,
+    *,
+    barrier_id: str,
+    barriered_from_usergroup_ids: Union[str, Sequence[str]],
+    primary_usergroup_id: str,
+    restricted_subjects: Union[str, Sequence[str]],
+    **kwargs,
+) -> SlackResponse:
+    """Update an existing Information Barrier
+    https://docs.slack.dev/reference/methods/admin.barriers.update
+    """
+    kwargs.update({"barrier_id": barrier_id, "primary_usergroup_id": primary_usergroup_id})
+    if isinstance(barriered_from_usergroup_ids, (list, tuple)):
+        kwargs.update({"barriered_from_usergroup_ids": ",".join(barriered_from_usergroup_ids)})
+    else:
+        kwargs.update({"barriered_from_usergroup_ids": barriered_from_usergroup_ids})
+    if isinstance(restricted_subjects, (list, tuple)):
+        kwargs.update({"restricted_subjects": ",".join(restricted_subjects)})
+    else:
+        kwargs.update({"restricted_subjects": restricted_subjects})
+    return self.api_call("admin.barriers.update", http_verb="POST", params=kwargs)
+
+ +
+
+def admin_conversations_archive(self, *, channel_id: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_conversations_archive(
+    self,
+    *,
+    channel_id: str,
+    **kwargs,
+) -> SlackResponse:
+    """Archive a public or private channel.
+    https://docs.slack.dev/reference/methods/admin.conversations.archive
+    """
+    kwargs.update({"channel_id": channel_id})
+    return self.api_call("admin.conversations.archive", params=kwargs)
+
+ +
+
+def admin_conversations_bulkArchive(self, *, channel_ids: str | Sequence[str], **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_conversations_bulkArchive(
+    self,
+    *,
+    channel_ids: Union[Sequence[str], str],
+    **kwargs,
+) -> SlackResponse:
+    """Archive public or private channels in bulk.
+    https://docs.slack.dev/reference/methods/admin.conversations.bulkArchive
+    """
+    kwargs.update({"channel_ids": ",".join(channel_ids) if isinstance(channel_ids, (list, tuple)) else channel_ids})
+    return self.api_call("admin.conversations.bulkArchive", params=kwargs)
+
+ +
+
+def admin_conversations_bulkDelete(self, *, channel_ids: str | Sequence[str], **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_conversations_bulkDelete(
+    self,
+    *,
+    channel_ids: Union[Sequence[str], str],
+    **kwargs,
+) -> SlackResponse:
+    """Delete public or private channels in bulk.
+    https://slack.com/api/admin.conversations.bulkDelete
+    """
+    kwargs.update({"channel_ids": ",".join(channel_ids) if isinstance(channel_ids, (list, tuple)) else channel_ids})
+    return self.api_call("admin.conversations.bulkDelete", params=kwargs)
+
+

Delete public or private channels in bulk. +https://slack.com/api/admin.conversations.bulkDelete

+
+
+def admin_conversations_bulkMove(self, *, channel_ids: str | Sequence[str], target_team_id: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_conversations_bulkMove(
+    self,
+    *,
+    channel_ids: Union[Sequence[str], str],
+    target_team_id: str,
+    **kwargs,
+) -> SlackResponse:
+    """Move public or private channels in bulk.
+    https://docs.slack.dev/reference/methods/admin.conversations.bulkMove
+    """
+    kwargs.update(
+        {
+            "target_team_id": target_team_id,
+            "channel_ids": ",".join(channel_ids) if isinstance(channel_ids, (list, tuple)) else channel_ids,
+        }
+    )
+    return self.api_call("admin.conversations.bulkMove", params=kwargs)
+
+ +
+
+def admin_conversations_convertToPrivate(self, *, channel_id: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_conversations_convertToPrivate(
+    self,
+    *,
+    channel_id: str,
+    **kwargs,
+) -> SlackResponse:
+    """Convert a public channel to a private channel.
+    https://docs.slack.dev/reference/methods/admin.conversations.convertToPrivate
+    """
+    kwargs.update({"channel_id": channel_id})
+    return self.api_call("admin.conversations.convertToPrivate", params=kwargs)
+
+ +
+
+def admin_conversations_convertToPublic(self, *, channel_id: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_conversations_convertToPublic(
+    self,
+    *,
+    channel_id: str,
+    **kwargs,
+) -> SlackResponse:
+    """Convert a privte channel to a public channel.
+    https://docs.slack.dev/reference/methods/admin.conversations.convertToPublic
+    """
+    kwargs.update({"channel_id": channel_id})
+    return self.api_call("admin.conversations.convertToPublic", params=kwargs)
+
+ +
+
+def admin_conversations_create(self,
*,
is_private: bool,
name: str,
description: str | None = None,
org_wide: bool | None = None,
team_id: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_conversations_create(
+    self,
+    *,
+    is_private: bool,
+    name: str,
+    description: Optional[str] = None,
+    org_wide: Optional[bool] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Create a public or private channel-based conversation.
+    https://docs.slack.dev/reference/methods/admin.conversations.create
+    """
+    kwargs.update(
+        {
+            "is_private": is_private,
+            "name": name,
+            "description": description,
+            "org_wide": org_wide,
+            "team_id": team_id,
+        }
+    )
+    return self.api_call("admin.conversations.create", params=kwargs)
+
+

Create a public or private channel-based conversation. +https://docs.slack.dev/reference/methods/admin.conversations.create

+
+
+def admin_conversations_createForObjects(self,
*,
object_id: str,
salesforce_org_id: str,
invite_object_team: bool | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_conversations_createForObjects(
+    self,
+    *,
+    object_id: str,
+    salesforce_org_id: str,
+    invite_object_team: Optional[bool] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Create a Salesforce channel for the corresponding object provided.
+    https://docs.slack.dev/reference/methods/admin.conversations.createForObjects
+    """
+    kwargs.update(
+        {"object_id": object_id, "salesforce_org_id": salesforce_org_id, "invite_object_team": invite_object_team}
+    )
+    return self.api_call("admin.conversations.createForObjects", params=kwargs)
+
+

Create a Salesforce channel for the corresponding object provided. +https://docs.slack.dev/reference/methods/admin.conversations.createForObjects

+
+
+def admin_conversations_delete(self, *, channel_id: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_conversations_delete(
+    self,
+    *,
+    channel_id: str,
+    **kwargs,
+) -> SlackResponse:
+    """Delete a public or private channel.
+    https://docs.slack.dev/reference/methods/admin.conversations.delete
+    """
+    kwargs.update({"channel_id": channel_id})
+    return self.api_call("admin.conversations.delete", params=kwargs)
+
+ +
+
+def admin_conversations_disconnectShared(self,
*,
channel_id: str,
leaving_team_ids: str | Sequence[str] | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_conversations_disconnectShared(
+    self,
+    *,
+    channel_id: str,
+    leaving_team_ids: Optional[Union[str, Sequence[str]]] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Disconnect a connected channel from one or more workspaces.
+    https://docs.slack.dev/reference/methods/admin.conversations.disconnectShared
+    """
+    kwargs.update({"channel_id": channel_id})
+    if isinstance(leaving_team_ids, (list, tuple)):
+        kwargs.update({"leaving_team_ids": ",".join(leaving_team_ids)})
+    else:
+        kwargs.update({"leaving_team_ids": leaving_team_ids})
+    return self.api_call("admin.conversations.disconnectShared", params=kwargs)
+
+

Disconnect a connected channel from one or more workspaces. +https://docs.slack.dev/reference/methods/admin.conversations.disconnectShared

+
+
+def admin_conversations_ekm_listOriginalConnectedChannelInfo(self,
*,
channel_ids: str | Sequence[str] | None = None,
cursor: str | None = None,
limit: int | None = None,
team_ids: str | Sequence[str] | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_conversations_ekm_listOriginalConnectedChannelInfo(
+    self,
+    *,
+    channel_ids: Optional[Union[str, Sequence[str]]] = None,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    team_ids: Optional[Union[str, Sequence[str]]] = None,
+    **kwargs,
+) -> SlackResponse:
+    """List all disconnected channels—i.e.,
+    channels that were once connected to other workspaces and then disconnected—and
+    the corresponding original channel IDs for key revocation with EKM.
+    https://docs.slack.dev/reference/methods/admin.conversations.ekm.listOriginalConnectedChannelInfo
+    """
+    kwargs.update(
+        {
+            "cursor": cursor,
+            "limit": limit,
+        }
+    )
+    if isinstance(channel_ids, (list, tuple)):
+        kwargs.update({"channel_ids": ",".join(channel_ids)})
+    else:
+        kwargs.update({"channel_ids": channel_ids})
+    if isinstance(team_ids, (list, tuple)):
+        kwargs.update({"team_ids": ",".join(team_ids)})
+    else:
+        kwargs.update({"team_ids": team_ids})
+    return self.api_call("admin.conversations.ekm.listOriginalConnectedChannelInfo", params=kwargs)
+
+

List all disconnected channels—i.e., +channels that were once connected to other workspaces and then disconnected—and +the corresponding original channel IDs for key revocation with EKM. +https://docs.slack.dev/reference/methods/admin.conversations.ekm.listOriginalConnectedChannelInfo

+
+
+def admin_conversations_getConversationPrefs(self, *, channel_id: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_conversations_getConversationPrefs(
+    self,
+    *,
+    channel_id: str,
+    **kwargs,
+) -> SlackResponse:
+    """Get conversation preferences for a public or private channel.
+    https://docs.slack.dev/reference/methods/admin.conversations.getConversationPrefs
+    """
+    kwargs.update({"channel_id": channel_id})
+    return self.api_call("admin.conversations.getConversationPrefs", params=kwargs)
+
+

Get conversation preferences for a public or private channel. +https://docs.slack.dev/reference/methods/admin.conversations.getConversationPrefs

+
+
+def admin_conversations_getCustomRetention(self, *, channel_id: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_conversations_getCustomRetention(
+    self,
+    *,
+    channel_id: str,
+    **kwargs,
+) -> SlackResponse:
+    """Get a channel's retention policy
+    https://docs.slack.dev/reference/methods/admin.conversations.getCustomRetention
+    """
+    kwargs.update({"channel_id": channel_id})
+    return self.api_call("admin.conversations.getCustomRetention", params=kwargs)
+
+ +
+
+def admin_conversations_getTeams(self,
*,
channel_id: str,
cursor: str | None = None,
limit: int | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_conversations_getTeams(
+    self,
+    *,
+    channel_id: str,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Set the workspaces in an Enterprise grid org that connect to a channel.
+    https://docs.slack.dev/reference/methods/admin.conversations.getTeams
+    """
+    kwargs.update(
+        {
+            "channel_id": channel_id,
+            "cursor": cursor,
+            "limit": limit,
+        }
+    )
+    return self.api_call("admin.conversations.getTeams", params=kwargs)
+
+

Set the workspaces in an Enterprise grid org that connect to a channel. +https://docs.slack.dev/reference/methods/admin.conversations.getTeams

+
+
+def admin_conversations_invite(self, *, channel_id: str, user_ids: str | Sequence[str], **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_conversations_invite(
+    self,
+    *,
+    channel_id: str,
+    user_ids: Union[str, Sequence[str]],
+    **kwargs,
+) -> SlackResponse:
+    """Invite a user to a public or private channel.
+    https://docs.slack.dev/reference/methods/admin.conversations.invite
+    """
+    kwargs.update({"channel_id": channel_id})
+    if isinstance(user_ids, (list, tuple)):
+        kwargs.update({"user_ids": ",".join(user_ids)})
+    else:
+        kwargs.update({"user_ids": user_ids})
+    # NOTE: the endpoint is unable to handle Content-Type: application/json as of Sep 3, 2020.
+    return self.api_call("admin.conversations.invite", params=kwargs)
+
+

Invite a user to a public or private channel. +https://docs.slack.dev/reference/methods/admin.conversations.invite

+
+
+def admin_conversations_linkObjects(self, *, channel: str, record_id: str, salesforce_org_id: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_conversations_linkObjects(
+    self,
+    *,
+    channel: str,
+    record_id: str,
+    salesforce_org_id: str,
+    **kwargs,
+) -> SlackResponse:
+    """Link a Salesforce record to a channel.
+    https://docs.slack.dev/reference/methods/admin.conversations.linkObjects
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "record_id": record_id,
+            "salesforce_org_id": salesforce_org_id,
+        }
+    )
+    return self.api_call("admin.conversations.linkObjects", params=kwargs)
+
+ +
+
+def admin_conversations_lookup(self,
*,
last_message_activity_before: int,
team_ids: str | Sequence[str],
cursor: str | None = None,
limit: int | None = None,
max_member_count: int | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_conversations_lookup(
+    self,
+    *,
+    last_message_activity_before: int,
+    team_ids: Union[str, Sequence[str]],
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    max_member_count: Optional[int] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Returns channels on the given team using the filters.
+    https://docs.slack.dev/reference/methods/admin.conversations.lookup
+    """
+    kwargs.update(
+        {
+            "last_message_activity_before": last_message_activity_before,
+            "cursor": cursor,
+            "limit": limit,
+            "max_member_count": max_member_count,
+        }
+    )
+    if isinstance(team_ids, (list, tuple)):
+        kwargs.update({"team_ids": ",".join(team_ids)})
+    else:
+        kwargs.update({"team_ids": team_ids})
+    return self.api_call("admin.conversations.lookup", params=kwargs)
+
+

Returns channels on the given team using the filters. +https://docs.slack.dev/reference/methods/admin.conversations.lookup

+
+
+def admin_conversations_removeCustomRetention(self, *, channel_id: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_conversations_removeCustomRetention(
+    self,
+    *,
+    channel_id: str,
+    **kwargs,
+) -> SlackResponse:
+    """Remove a channel's retention policy
+    https://docs.slack.dev/reference/methods/admin.conversations.removeCustomRetention
+    """
+    kwargs.update({"channel_id": channel_id})
+    return self.api_call("admin.conversations.removeCustomRetention", params=kwargs)
+
+ +
+
+def admin_conversations_rename(self, *, channel_id: str, name: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_conversations_rename(
+    self,
+    *,
+    channel_id: str,
+    name: str,
+    **kwargs,
+) -> SlackResponse:
+    """Rename a public or private channel.
+    https://docs.slack.dev/reference/methods/admin.conversations.rename
+    """
+    kwargs.update({"channel_id": channel_id, "name": name})
+    return self.api_call("admin.conversations.rename", params=kwargs)
+
+ +
+
+def admin_conversations_restrictAccess_addGroup(self, *, channel_id: str, group_id: str, team_id: str | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_conversations_restrictAccess_addGroup(
+    self,
+    *,
+    channel_id: str,
+    group_id: str,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Add an allowlist of IDP groups for accessing a channel.
+    https://docs.slack.dev/reference/methods/admin.conversations.restrictAccess.addGroup
+    """
+    kwargs.update(
+        {
+            "channel_id": channel_id,
+            "group_id": group_id,
+            "team_id": team_id,
+        }
+    )
+    return self.api_call(
+        "admin.conversations.restrictAccess.addGroup",
+        http_verb="GET",
+        params=kwargs,
+    )
+
+

Add an allowlist of IDP groups for accessing a channel. +https://docs.slack.dev/reference/methods/admin.conversations.restrictAccess.addGroup

+
+
+def admin_conversations_restrictAccess_listGroups(self, *, channel_id: str, team_id: str | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_conversations_restrictAccess_listGroups(
+    self,
+    *,
+    channel_id: str,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """List all IDP Groups linked to a channel.
+    https://docs.slack.dev/reference/methods/admin.conversations.restrictAccess.listGroups
+    """
+    kwargs.update(
+        {
+            "channel_id": channel_id,
+            "team_id": team_id,
+        }
+    )
+    return self.api_call(
+        "admin.conversations.restrictAccess.listGroups",
+        http_verb="GET",
+        params=kwargs,
+    )
+
+ +
+
+def admin_conversations_restrictAccess_removeGroup(self, *, channel_id: str, group_id: str, team_id: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_conversations_restrictAccess_removeGroup(
+    self,
+    *,
+    channel_id: str,
+    group_id: str,
+    team_id: str,
+    **kwargs,
+) -> SlackResponse:
+    """Remove a linked IDP group linked from a private channel.
+    https://docs.slack.dev/reference/methods/admin.conversations.restrictAccess.removeGroup
+    """
+    kwargs.update(
+        {
+            "channel_id": channel_id,
+            "group_id": group_id,
+            "team_id": team_id,
+        }
+    )
+    return self.api_call(
+        "admin.conversations.restrictAccess.removeGroup",
+        http_verb="GET",
+        params=kwargs,
+    )
+
+

Remove a linked IDP group linked from a private channel. +https://docs.slack.dev/reference/methods/admin.conversations.restrictAccess.removeGroup

+
+ +
+
+ +Expand source code + +
def admin_conversations_search(
+    self,
+    *,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    query: Optional[str] = None,
+    search_channel_types: Optional[Union[str, Sequence[str]]] = None,
+    sort: Optional[str] = None,
+    sort_dir: Optional[str] = None,
+    team_ids: Optional[Union[str, Sequence[str]]] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Search for public or private channels in an Enterprise organization.
+    https://docs.slack.dev/reference/methods/admin.conversations.search
+    """
+    kwargs.update(
+        {
+            "cursor": cursor,
+            "limit": limit,
+            "query": query,
+            "sort": sort,
+            "sort_dir": sort_dir,
+        }
+    )
+
+    if isinstance(search_channel_types, (list, tuple)):
+        kwargs.update({"search_channel_types": ",".join(search_channel_types)})
+    else:
+        kwargs.update({"search_channel_types": search_channel_types})
+
+    if isinstance(team_ids, (list, tuple)):
+        kwargs.update({"team_ids": ",".join(team_ids)})
+    else:
+        kwargs.update({"team_ids": team_ids})
+
+    return self.api_call("admin.conversations.search", params=kwargs)
+
+

Search for public or private channels in an Enterprise organization. +https://docs.slack.dev/reference/methods/admin.conversations.search

+
+
+def admin_conversations_setConversationPrefs(self, *, channel_id: str, prefs: str | Dict[str, str], **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_conversations_setConversationPrefs(
+    self,
+    *,
+    channel_id: str,
+    prefs: Union[str, Dict[str, str]],
+    **kwargs,
+) -> SlackResponse:
+    """Set the posting permissions for a public or private channel.
+    https://docs.slack.dev/reference/methods/admin.conversations.setConversationPrefs
+    """
+    kwargs.update({"channel_id": channel_id})
+    if isinstance(prefs, dict):
+        kwargs.update({"prefs": json.dumps(prefs)})
+    else:
+        kwargs.update({"prefs": prefs})
+    return self.api_call("admin.conversations.setConversationPrefs", params=kwargs)
+
+

Set the posting permissions for a public or private channel. +https://docs.slack.dev/reference/methods/admin.conversations.setConversationPrefs

+
+
+def admin_conversations_setCustomRetention(self, *, channel_id: str, duration_days: int, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_conversations_setCustomRetention(
+    self,
+    *,
+    channel_id: str,
+    duration_days: int,
+    **kwargs,
+) -> SlackResponse:
+    """Set a channel's retention policy
+    https://docs.slack.dev/reference/methods/admin.conversations.setCustomRetention
+    """
+    kwargs.update({"channel_id": channel_id, "duration_days": duration_days})
+    return self.api_call("admin.conversations.setCustomRetention", params=kwargs)
+
+ +
+
+def admin_conversations_setTeams(self,
*,
channel_id: str,
org_channel: bool | None = None,
target_team_ids: str | Sequence[str] | None = None,
team_id: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_conversations_setTeams(
+    self,
+    *,
+    channel_id: str,
+    org_channel: Optional[bool] = None,
+    target_team_ids: Optional[Union[str, Sequence[str]]] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Set the workspaces in an Enterprise grid org that connect to a public or private channel.
+    https://docs.slack.dev/reference/methods/admin.conversations.setTeams
+    """
+    kwargs.update(
+        {
+            "channel_id": channel_id,
+            "org_channel": org_channel,
+            "team_id": team_id,
+        }
+    )
+    if isinstance(target_team_ids, (list, tuple)):
+        kwargs.update({"target_team_ids": ",".join(target_team_ids)})
+    else:
+        kwargs.update({"target_team_ids": target_team_ids})
+    return self.api_call("admin.conversations.setTeams", params=kwargs)
+
+

Set the workspaces in an Enterprise grid org that connect to a public or private channel. +https://docs.slack.dev/reference/methods/admin.conversations.setTeams

+
+
+def admin_conversations_unarchive(self, *, channel_id: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_conversations_unarchive(
+    self,
+    *,
+    channel_id: str,
+    **kwargs,
+) -> SlackResponse:
+    """Unarchive a public or private channel.
+    https://docs.slack.dev/reference/methods/admin.conversations.archive
+    """
+    kwargs.update({"channel_id": channel_id})
+    return self.api_call("admin.conversations.unarchive", params=kwargs)
+
+ +
+
+def admin_conversations_unlinkObjects(self, *, channel: str, new_name: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_conversations_unlinkObjects(
+    self,
+    *,
+    channel: str,
+    new_name: str,
+    **kwargs,
+) -> SlackResponse:
+    """Unlink a Salesforce record from a channel.
+    https://docs.slack.dev/reference/methods/admin.conversations.unlinkObjects
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "new_name": new_name,
+        }
+    )
+    return self.api_call("admin.conversations.unlinkObjects", params=kwargs)
+
+ +
+
+def admin_emoji_add(self, *, name: str, url: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_emoji_add(
+    self,
+    *,
+    name: str,
+    url: str,
+    **kwargs,
+) -> SlackResponse:
+    """Add an emoji.
+    https://docs.slack.dev/reference/methods/admin.emoji.add
+    """
+    kwargs.update({"name": name, "url": url})
+    return self.api_call("admin.emoji.add", http_verb="GET", params=kwargs)
+
+ +
+
+def admin_emoji_addAlias(self, *, alias_for: str, name: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_emoji_addAlias(
+    self,
+    *,
+    alias_for: str,
+    name: str,
+    **kwargs,
+) -> SlackResponse:
+    """Add an emoji alias.
+    https://docs.slack.dev/reference/methods/admin.emoji.addAlias
+    """
+    kwargs.update({"alias_for": alias_for, "name": name})
+    return self.api_call("admin.emoji.addAlias", http_verb="GET", params=kwargs)
+
+ +
+
+def admin_emoji_list(self, *, cursor: str | None = None, limit: int | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_emoji_list(
+    self,
+    *,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    **kwargs,
+) -> SlackResponse:
+    """List emoji for an Enterprise Grid organization.
+    https://docs.slack.dev/reference/methods/admin.emoji.list
+    """
+    kwargs.update({"cursor": cursor, "limit": limit})
+    return self.api_call("admin.emoji.list", http_verb="GET", params=kwargs)
+
+

List emoji for an Enterprise Grid organization. +https://docs.slack.dev/reference/methods/admin.emoji.list

+
+
+def admin_emoji_remove(self, *, name: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_emoji_remove(
+    self,
+    *,
+    name: str,
+    **kwargs,
+) -> SlackResponse:
+    """Remove an emoji across an Enterprise Grid organization.
+    https://docs.slack.dev/reference/methods/admin.emoji.remove
+    """
+    kwargs.update({"name": name})
+    return self.api_call("admin.emoji.remove", http_verb="GET", params=kwargs)
+
+

Remove an emoji across an Enterprise Grid organization. +https://docs.slack.dev/reference/methods/admin.emoji.remove

+
+
+def admin_emoji_rename(self, *, name: str, new_name: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_emoji_rename(
+    self,
+    *,
+    name: str,
+    new_name: str,
+    **kwargs,
+) -> SlackResponse:
+    """Rename an emoji.
+    https://docs.slack.dev/reference/methods/admin.emoji.rename
+    """
+    kwargs.update({"name": name, "new_name": new_name})
+    return self.api_call("admin.emoji.rename", http_verb="GET", params=kwargs)
+
+ +
+
+def admin_functions_list(self,
*,
app_ids: str | Sequence[str],
team_id: str | None = None,
cursor: str | None = None,
limit: int | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_functions_list(
+    self,
+    *,
+    app_ids: Union[str, Sequence[str]],
+    team_id: Optional[str] = None,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Look up functions by a set of apps
+    https://docs.slack.dev/reference/methods/admin.functions.list
+    """
+    if isinstance(app_ids, (list, tuple)):
+        kwargs.update({"app_ids": ",".join(app_ids)})
+    else:
+        kwargs.update({"app_ids": app_ids})
+    kwargs.update(
+        {
+            "team_id": team_id,
+            "cursor": cursor,
+            "limit": limit,
+        }
+    )
+    return self.api_call("admin.functions.list", params=kwargs)
+
+ +
+
+def admin_functions_permissions_lookup(self, *, function_ids: str | Sequence[str], **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_functions_permissions_lookup(
+    self,
+    *,
+    function_ids: Union[str, Sequence[str]],
+    **kwargs,
+) -> SlackResponse:
+    """Lookup the visibility of multiple Slack functions
+    and include the users if it is limited to particular named entities.
+    https://docs.slack.dev/reference/methods/admin.functions.permissions.lookup
+    """
+    if isinstance(function_ids, (list, tuple)):
+        kwargs.update({"function_ids": ",".join(function_ids)})
+    else:
+        kwargs.update({"function_ids": function_ids})
+    return self.api_call("admin.functions.permissions.lookup", params=kwargs)
+
+

Lookup the visibility of multiple Slack functions +and include the users if it is limited to particular named entities. +https://docs.slack.dev/reference/methods/admin.functions.permissions.lookup

+
+
+def admin_functions_permissions_set(self,
*,
function_id: str,
visibility: str,
user_ids: str | Sequence[str] | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_functions_permissions_set(
+    self,
+    *,
+    function_id: str,
+    visibility: str,
+    user_ids: Optional[Union[str, Sequence[str]]] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Set the visibility of a Slack function
+    and define the users or workspaces if it is set to named_entities
+    https://docs.slack.dev/reference/methods/admin.functions.permissions.set
+    """
+    kwargs.update(
+        {
+            "function_id": function_id,
+            "visibility": visibility,
+        }
+    )
+    if user_ids is not None:
+        if isinstance(user_ids, (list, tuple)):
+            kwargs.update({"user_ids": ",".join(user_ids)})
+        else:
+            kwargs.update({"user_ids": user_ids})
+    return self.api_call("admin.functions.permissions.set", params=kwargs)
+
+

Set the visibility of a Slack function +and define the users or workspaces if it is set to named_entities +https://docs.slack.dev/reference/methods/admin.functions.permissions.set

+
+
+def admin_inviteRequests_approve(self, *, invite_request_id: str, team_id: str | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_inviteRequests_approve(
+    self,
+    *,
+    invite_request_id: str,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Approve a workspace invite request.
+    https://docs.slack.dev/reference/methods/admin.inviteRequests.approve
+    """
+    kwargs.update({"invite_request_id": invite_request_id, "team_id": team_id})
+    return self.api_call("admin.inviteRequests.approve", params=kwargs)
+
+ +
+
+def admin_inviteRequests_approved_list(self,
*,
cursor: str | None = None,
limit: int | None = None,
team_id: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_inviteRequests_approved_list(
+    self,
+    *,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """List all approved workspace invite requests.
+    https://docs.slack.dev/reference/methods/admin.inviteRequests.approved.list
+    """
+    kwargs.update(
+        {
+            "cursor": cursor,
+            "limit": limit,
+            "team_id": team_id,
+        }
+    )
+    return self.api_call("admin.inviteRequests.approved.list", params=kwargs)
+
+ +
+
+def admin_inviteRequests_denied_list(self,
*,
cursor: str | None = None,
limit: int | None = None,
team_id: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_inviteRequests_denied_list(
+    self,
+    *,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """List all denied workspace invite requests.
+    https://docs.slack.dev/reference/methods/admin.inviteRequests.denied.list
+    """
+    kwargs.update(
+        {
+            "cursor": cursor,
+            "limit": limit,
+            "team_id": team_id,
+        }
+    )
+    return self.api_call("admin.inviteRequests.denied.list", params=kwargs)
+
+ +
+
+def admin_inviteRequests_deny(self, *, invite_request_id: str, team_id: str | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_inviteRequests_deny(
+    self,
+    *,
+    invite_request_id: str,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Deny a workspace invite request.
+    https://docs.slack.dev/reference/methods/admin.inviteRequests.deny
+    """
+    kwargs.update({"invite_request_id": invite_request_id, "team_id": team_id})
+    return self.api_call("admin.inviteRequests.deny", params=kwargs)
+
+ +
+
+def admin_inviteRequests_list(self, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_inviteRequests_list(
+    self,
+    **kwargs,
+) -> SlackResponse:
+    """List all pending workspace invite requests."""
+    return self.api_call("admin.inviteRequests.list", params=kwargs)
+
+

List all pending workspace invite requests.

+
+
+def admin_roles_addAssignments(self,
*,
role_id: str,
entity_ids: str | Sequence[str],
user_ids: str | Sequence[str],
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_roles_addAssignments(
+    self,
+    *,
+    role_id: str,
+    entity_ids: Union[str, Sequence[str]],
+    user_ids: Union[str, Sequence[str]],
+    **kwargs,
+) -> SlackResponse:
+    """Adds members to the specified role with the specified scopes
+    https://docs.slack.dev/reference/methods/admin.roles.addAssignments
+    """
+    kwargs.update({"role_id": role_id})
+    if isinstance(entity_ids, (list, tuple)):
+        kwargs.update({"entity_ids": ",".join(entity_ids)})
+    else:
+        kwargs.update({"entity_ids": entity_ids})
+    if isinstance(user_ids, (list, tuple)):
+        kwargs.update({"user_ids": ",".join(user_ids)})
+    else:
+        kwargs.update({"user_ids": user_ids})
+    return self.api_call("admin.roles.addAssignments", params=kwargs)
+
+

Adds members to the specified role with the specified scopes +https://docs.slack.dev/reference/methods/admin.roles.addAssignments

+
+
+def admin_roles_listAssignments(self,
*,
role_ids: str | Sequence[str] | None = None,
entity_ids: str | Sequence[str] | None = None,
cursor: str | None = None,
limit: str | int | None = None,
sort_dir: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_roles_listAssignments(
+    self,
+    *,
+    role_ids: Optional[Union[str, Sequence[str]]] = None,
+    entity_ids: Optional[Union[str, Sequence[str]]] = None,
+    cursor: Optional[str] = None,
+    limit: Optional[Union[str, int]] = None,
+    sort_dir: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Lists assignments for all roles across entities.
+        Options to scope results by any combination of roles or entities
+    https://docs.slack.dev/reference/methods/admin.roles.listAssignments
+    """
+    kwargs.update({"cursor": cursor, "limit": limit, "sort_dir": sort_dir})
+    if isinstance(entity_ids, (list, tuple)):
+        kwargs.update({"entity_ids": ",".join(entity_ids)})
+    else:
+        kwargs.update({"entity_ids": entity_ids})
+    if isinstance(role_ids, (list, tuple)):
+        kwargs.update({"role_ids": ",".join(role_ids)})
+    else:
+        kwargs.update({"role_ids": role_ids})
+    return self.api_call("admin.roles.listAssignments", params=kwargs)
+
+

Lists assignments for all roles across entities. +Options to scope results by any combination of roles or entities +https://docs.slack.dev/reference/methods/admin.roles.listAssignments

+
+
+def admin_roles_removeAssignments(self,
*,
role_id: str,
entity_ids: str | Sequence[str],
user_ids: str | Sequence[str],
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_roles_removeAssignments(
+    self,
+    *,
+    role_id: str,
+    entity_ids: Union[str, Sequence[str]],
+    user_ids: Union[str, Sequence[str]],
+    **kwargs,
+) -> SlackResponse:
+    """Removes a set of users from a role for the given scopes and entities
+    https://docs.slack.dev/reference/methods/admin.roles.removeAssignments
+    """
+    kwargs.update({"role_id": role_id})
+    if isinstance(entity_ids, (list, tuple)):
+        kwargs.update({"entity_ids": ",".join(entity_ids)})
+    else:
+        kwargs.update({"entity_ids": entity_ids})
+    if isinstance(user_ids, (list, tuple)):
+        kwargs.update({"user_ids": ",".join(user_ids)})
+    else:
+        kwargs.update({"user_ids": user_ids})
+    return self.api_call("admin.roles.removeAssignments", params=kwargs)
+
+

Removes a set of users from a role for the given scopes and entities +https://docs.slack.dev/reference/methods/admin.roles.removeAssignments

+
+
+def admin_teams_admins_list(self, *, team_id: str, cursor: str | None = None, limit: int | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_teams_admins_list(
+    self,
+    *,
+    team_id: str,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    **kwargs,
+) -> SlackResponse:
+    """List all of the admins on a given workspace.
+    https://docs.slack.dev/reference/methods/admin.inviteRequests.list
+    """
+    kwargs.update(
+        {
+            "cursor": cursor,
+            "limit": limit,
+            "team_id": team_id,
+        }
+    )
+    return self.api_call("admin.teams.admins.list", http_verb="GET", params=kwargs)
+
+

List all of the admins on a given workspace. +https://docs.slack.dev/reference/methods/admin.inviteRequests.list

+
+
+def admin_teams_create(self,
*,
team_domain: str,
team_name: str,
team_description: str | None = None,
team_discoverability: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_teams_create(
+    self,
+    *,
+    team_domain: str,
+    team_name: str,
+    team_description: Optional[str] = None,
+    team_discoverability: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Create an Enterprise team.
+    https://docs.slack.dev/reference/methods/admin.teams.create
+    """
+    kwargs.update(
+        {
+            "team_domain": team_domain,
+            "team_name": team_name,
+            "team_description": team_description,
+            "team_discoverability": team_discoverability,
+        }
+    )
+    return self.api_call("admin.teams.create", params=kwargs)
+
+ +
+
+def admin_teams_list(self, *, cursor: str | None = None, limit: int | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_teams_list(
+    self,
+    *,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    **kwargs,
+) -> SlackResponse:
+    """List all teams on an Enterprise organization.
+    https://docs.slack.dev/reference/methods/admin.teams.list
+    """
+    kwargs.update({"cursor": cursor, "limit": limit})
+    return self.api_call("admin.teams.list", params=kwargs)
+
+

List all teams on an Enterprise organization. +https://docs.slack.dev/reference/methods/admin.teams.list

+
+
+def admin_teams_owners_list(self, *, team_id: str, cursor: str | None = None, limit: int | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_teams_owners_list(
+    self,
+    *,
+    team_id: str,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    **kwargs,
+) -> SlackResponse:
+    """List all of the admins on a given workspace.
+    https://docs.slack.dev/reference/methods/admin.teams.owners.list
+    """
+    kwargs.update({"team_id": team_id, "cursor": cursor, "limit": limit})
+    return self.api_call("admin.teams.owners.list", http_verb="GET", params=kwargs)
+
+

List all of the admins on a given workspace. +https://docs.slack.dev/reference/methods/admin.teams.owners.list

+
+
+def admin_teams_settings_info(self, *, team_id: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_teams_settings_info(
+    self,
+    *,
+    team_id: str,
+    **kwargs,
+) -> SlackResponse:
+    """Fetch information about settings in a workspace
+    https://docs.slack.dev/reference/methods/admin.teams.settings.info
+    """
+    kwargs.update({"team_id": team_id})
+    return self.api_call("admin.teams.settings.info", params=kwargs)
+
+

Fetch information about settings in a workspace +https://docs.slack.dev/reference/methods/admin.teams.settings.info

+
+
+def admin_teams_settings_setDefaultChannels(self, *, team_id: str, channel_ids: str | Sequence[str], **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_teams_settings_setDefaultChannels(
+    self,
+    *,
+    team_id: str,
+    channel_ids: Union[str, Sequence[str]],
+    **kwargs,
+) -> SlackResponse:
+    """Set the default channels of a workspace.
+    https://docs.slack.dev/reference/methods/admin.teams.settings.setDefaultChannels
+    """
+    kwargs.update({"team_id": team_id})
+    if isinstance(channel_ids, (list, tuple)):
+        kwargs.update({"channel_ids": ",".join(channel_ids)})
+    else:
+        kwargs.update({"channel_ids": channel_ids})
+    return self.api_call("admin.teams.settings.setDefaultChannels", http_verb="GET", params=kwargs)
+
+ +
+
+def admin_teams_settings_setDescription(self, *, team_id: str, description: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_teams_settings_setDescription(
+    self,
+    *,
+    team_id: str,
+    description: str,
+    **kwargs,
+) -> SlackResponse:
+    """Set the description of a given workspace.
+    https://docs.slack.dev/reference/methods/admin.teams.settings.setDescription
+    """
+    kwargs.update({"team_id": team_id, "description": description})
+    return self.api_call("admin.teams.settings.setDescription", params=kwargs)
+
+ +
+
+def admin_teams_settings_setDiscoverability(self, *, team_id: str, discoverability: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_teams_settings_setDiscoverability(
+    self,
+    *,
+    team_id: str,
+    discoverability: str,
+    **kwargs,
+) -> SlackResponse:
+    """Sets the icon of a workspace.
+    https://docs.slack.dev/reference/methods/admin.teams.settings.setDiscoverability
+    """
+    kwargs.update({"team_id": team_id, "discoverability": discoverability})
+    return self.api_call("admin.teams.settings.setDiscoverability", params=kwargs)
+
+ +
+
+def admin_teams_settings_setIcon(self, *, team_id: str, image_url: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_teams_settings_setIcon(
+    self,
+    *,
+    team_id: str,
+    image_url: str,
+    **kwargs,
+) -> SlackResponse:
+    """Sets the icon of a workspace.
+    https://docs.slack.dev/reference/methods/admin.teams.settings.setIcon
+    """
+    kwargs.update({"team_id": team_id, "image_url": image_url})
+    return self.api_call("admin.teams.settings.setIcon", http_verb="GET", params=kwargs)
+
+ +
+
+def admin_teams_settings_setName(self, *, team_id: str, name: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_teams_settings_setName(
+    self,
+    *,
+    team_id: str,
+    name: str,
+    **kwargs,
+) -> SlackResponse:
+    """Sets the icon of a workspace.
+    https://docs.slack.dev/reference/methods/admin.teams.settings.setName
+    """
+    kwargs.update({"team_id": team_id, "name": name})
+    return self.api_call("admin.teams.settings.setName", params=kwargs)
+
+ +
+
+def admin_usergroups_addChannels(self,
*,
channel_ids: str | Sequence[str],
usergroup_id: str,
team_id: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_usergroups_addChannels(
+    self,
+    *,
+    channel_ids: Union[str, Sequence[str]],
+    usergroup_id: str,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Add one or more default channels to an IDP group.
+    https://docs.slack.dev/reference/methods/admin.usergroups.addChannels
+    """
+    kwargs.update({"team_id": team_id, "usergroup_id": usergroup_id})
+    if isinstance(channel_ids, (list, tuple)):
+        kwargs.update({"channel_ids": ",".join(channel_ids)})
+    else:
+        kwargs.update({"channel_ids": channel_ids})
+    return self.api_call("admin.usergroups.addChannels", params=kwargs)
+
+

Add one or more default channels to an IDP group. +https://docs.slack.dev/reference/methods/admin.usergroups.addChannels

+
+
+def admin_usergroups_addTeams(self,
*,
usergroup_id: str,
team_ids: str | Sequence[str],
auto_provision: bool | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_usergroups_addTeams(
+    self,
+    *,
+    usergroup_id: str,
+    team_ids: Union[str, Sequence[str]],
+    auto_provision: Optional[bool] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Associate one or more default workspaces with an organization-wide IDP group.
+    https://docs.slack.dev/reference/methods/admin.usergroups.addTeams
+    """
+    kwargs.update({"usergroup_id": usergroup_id, "auto_provision": auto_provision})
+    if isinstance(team_ids, (list, tuple)):
+        kwargs.update({"team_ids": ",".join(team_ids)})
+    else:
+        kwargs.update({"team_ids": team_ids})
+    return self.api_call("admin.usergroups.addTeams", params=kwargs)
+
+

Associate one or more default workspaces with an organization-wide IDP group. +https://docs.slack.dev/reference/methods/admin.usergroups.addTeams

+
+
+def admin_usergroups_listChannels(self,
*,
usergroup_id: str,
include_num_members: bool | None = None,
team_id: bool | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_usergroups_listChannels(
+    self,
+    *,
+    usergroup_id: str,
+    include_num_members: Optional[bool] = None,
+    team_id: Optional[bool] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Add one or more default channels to an IDP group.
+    https://docs.slack.dev/reference/methods/admin.usergroups.listChannels
+    """
+    kwargs.update(
+        {
+            "usergroup_id": usergroup_id,
+            "include_num_members": include_num_members,
+            "team_id": team_id,
+        }
+    )
+    return self.api_call("admin.usergroups.listChannels", params=kwargs)
+
+

Add one or more default channels to an IDP group. +https://docs.slack.dev/reference/methods/admin.usergroups.listChannels

+
+
+def admin_usergroups_removeChannels(self, *, usergroup_id: str, channel_ids: str | Sequence[str], **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_usergroups_removeChannels(
+    self,
+    *,
+    usergroup_id: str,
+    channel_ids: Union[str, Sequence[str]],
+    **kwargs,
+) -> SlackResponse:
+    """Add one or more default channels to an IDP group.
+    https://docs.slack.dev/reference/methods/admin.usergroups.removeChannels
+    """
+    kwargs.update({"usergroup_id": usergroup_id})
+    if isinstance(channel_ids, (list, tuple)):
+        kwargs.update({"channel_ids": ",".join(channel_ids)})
+    else:
+        kwargs.update({"channel_ids": channel_ids})
+    return self.api_call("admin.usergroups.removeChannels", params=kwargs)
+
+

Add one or more default channels to an IDP group. +https://docs.slack.dev/reference/methods/admin.usergroups.removeChannels

+
+
+def admin_users_assign(self,
*,
team_id: str,
user_id: str,
channel_ids: str | Sequence[str] | None = None,
is_restricted: bool | None = None,
is_ultra_restricted: bool | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_users_assign(
+    self,
+    *,
+    team_id: str,
+    user_id: str,
+    channel_ids: Optional[Union[str, Sequence[str]]] = None,
+    is_restricted: Optional[bool] = None,
+    is_ultra_restricted: Optional[bool] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Add an Enterprise user to a workspace.
+    https://docs.slack.dev/reference/methods/admin.users.assign
+    """
+    kwargs.update(
+        {
+            "team_id": team_id,
+            "user_id": user_id,
+            "is_restricted": is_restricted,
+            "is_ultra_restricted": is_ultra_restricted,
+        }
+    )
+    if isinstance(channel_ids, (list, tuple)):
+        kwargs.update({"channel_ids": ",".join(channel_ids)})
+    else:
+        kwargs.update({"channel_ids": channel_ids})
+    return self.api_call("admin.users.assign", params=kwargs)
+
+

Add an Enterprise user to a workspace. +https://docs.slack.dev/reference/methods/admin.users.assign

+
+
+def admin_users_invite(self,
*,
team_id: str,
email: str,
channel_ids: str | Sequence[str],
custom_message: str | None = None,
email_password_policy_enabled: bool | None = None,
guest_expiration_ts: str | float | None = None,
is_restricted: bool | None = None,
is_ultra_restricted: bool | None = None,
real_name: str | None = None,
resend: bool | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_users_invite(
+    self,
+    *,
+    team_id: str,
+    email: str,
+    channel_ids: Union[str, Sequence[str]],
+    custom_message: Optional[str] = None,
+    email_password_policy_enabled: Optional[bool] = None,
+    guest_expiration_ts: Optional[Union[str, float]] = None,
+    is_restricted: Optional[bool] = None,
+    is_ultra_restricted: Optional[bool] = None,
+    real_name: Optional[str] = None,
+    resend: Optional[bool] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Invite a user to a workspace.
+    https://docs.slack.dev/reference/methods/admin.users.invite
+    """
+    kwargs.update(
+        {
+            "team_id": team_id,
+            "email": email,
+            "custom_message": custom_message,
+            "email_password_policy_enabled": email_password_policy_enabled,
+            "guest_expiration_ts": str(guest_expiration_ts) if guest_expiration_ts is not None else None,
+            "is_restricted": is_restricted,
+            "is_ultra_restricted": is_ultra_restricted,
+            "real_name": real_name,
+            "resend": resend,
+        }
+    )
+    if isinstance(channel_ids, (list, tuple)):
+        kwargs.update({"channel_ids": ",".join(channel_ids)})
+    else:
+        kwargs.update({"channel_ids": channel_ids})
+    return self.api_call("admin.users.invite", params=kwargs)
+
+ +
+
+def admin_users_list(self,
*,
team_id: str | None = None,
include_deactivated_user_workspaces: bool | None = None,
is_active: bool | None = None,
cursor: str | None = None,
limit: int | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_users_list(
+    self,
+    *,
+    team_id: Optional[str] = None,
+    include_deactivated_user_workspaces: Optional[bool] = None,
+    is_active: Optional[bool] = None,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    **kwargs,
+) -> SlackResponse:
+    """List users on a workspace
+    https://docs.slack.dev/reference/methods/admin.users.list
+    """
+    kwargs.update(
+        {
+            "team_id": team_id,
+            "include_deactivated_user_workspaces": include_deactivated_user_workspaces,
+            "is_active": is_active,
+            "cursor": cursor,
+            "limit": limit,
+        }
+    )
+    return self.api_call("admin.users.list", params=kwargs)
+
+ +
+
+def admin_users_remove(self, *, team_id: str, user_id: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_users_remove(
+    self,
+    *,
+    team_id: str,
+    user_id: str,
+    **kwargs,
+) -> SlackResponse:
+    """Remove a user from a workspace.
+    https://docs.slack.dev/reference/methods/admin.users.remove
+    """
+    kwargs.update({"team_id": team_id, "user_id": user_id})
+    return self.api_call("admin.users.remove", params=kwargs)
+
+ +
+
+def admin_users_session_clearSettings(self, *, user_ids: str | Sequence[str], **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_users_session_clearSettings(
+    self,
+    *,
+    user_ids: Union[str, Sequence[str]],
+    **kwargs,
+) -> SlackResponse:
+    """Clear user-specific session settings—the session duration
+    and what happens when the client closes—for a list of users.
+    https://docs.slack.dev/reference/methods/admin.users.session.clearSettings
+    """
+    if isinstance(user_ids, (list, tuple)):
+        kwargs.update({"user_ids": ",".join(user_ids)})
+    else:
+        kwargs.update({"user_ids": user_ids})
+    return self.api_call("admin.users.session.clearSettings", params=kwargs)
+
+

Clear user-specific session settings—the session duration +and what happens when the client closes—for a list of users. +https://docs.slack.dev/reference/methods/admin.users.session.clearSettings

+
+
+def admin_users_session_getSettings(self, *, user_ids: str | Sequence[str], **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_users_session_getSettings(
+    self,
+    *,
+    user_ids: Union[str, Sequence[str]],
+    **kwargs,
+) -> SlackResponse:
+    """Get user-specific session settings—the session duration
+    and what happens when the client closes—given a list of users.
+    https://docs.slack.dev/reference/methods/admin.users.session.getSettings
+    """
+    if isinstance(user_ids, (list, tuple)):
+        kwargs.update({"user_ids": ",".join(user_ids)})
+    else:
+        kwargs.update({"user_ids": user_ids})
+    return self.api_call("admin.users.session.getSettings", params=kwargs)
+
+

Get user-specific session settings—the session duration +and what happens when the client closes—given a list of users. +https://docs.slack.dev/reference/methods/admin.users.session.getSettings

+
+
+def admin_users_session_invalidate(self, *, session_id: str, team_id: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_users_session_invalidate(
+    self,
+    *,
+    session_id: str,
+    team_id: str,
+    **kwargs,
+) -> SlackResponse:
+    """Invalidate a single session for a user by session_id.
+    https://docs.slack.dev/reference/methods/admin.users.session.invalidate
+    """
+    kwargs.update({"session_id": session_id, "team_id": team_id})
+    return self.api_call("admin.users.session.invalidate", params=kwargs)
+
+

Invalidate a single session for a user by session_id. +https://docs.slack.dev/reference/methods/admin.users.session.invalidate

+
+
+def admin_users_session_list(self,
*,
cursor: str | None = None,
limit: int | None = None,
team_id: str | None = None,
user_id: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_users_session_list(
+    self,
+    *,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    team_id: Optional[str] = None,
+    user_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Lists all active user sessions for an organization
+    https://docs.slack.dev/reference/methods/admin.users.session.list
+    """
+    kwargs.update(
+        {
+            "cursor": cursor,
+            "limit": limit,
+            "team_id": team_id,
+            "user_id": user_id,
+        }
+    )
+    return self.api_call("admin.users.session.list", params=kwargs)
+
+

Lists all active user sessions for an organization +https://docs.slack.dev/reference/methods/admin.users.session.list

+
+
+def admin_users_session_reset(self,
*,
user_id: str,
mobile_only: bool | None = None,
web_only: bool | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_users_session_reset(
+    self,
+    *,
+    user_id: str,
+    mobile_only: Optional[bool] = None,
+    web_only: Optional[bool] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Wipes all valid sessions on all devices for a given user.
+    https://docs.slack.dev/reference/methods/admin.users.session.reset
+    """
+    kwargs.update(
+        {
+            "user_id": user_id,
+            "mobile_only": mobile_only,
+            "web_only": web_only,
+        }
+    )
+    return self.api_call("admin.users.session.reset", params=kwargs)
+
+

Wipes all valid sessions on all devices for a given user. +https://docs.slack.dev/reference/methods/admin.users.session.reset

+
+
+def admin_users_session_resetBulk(self,
*,
user_ids: str | Sequence[str],
mobile_only: bool | None = None,
web_only: bool | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_users_session_resetBulk(
+    self,
+    *,
+    user_ids: Union[str, Sequence[str]],
+    mobile_only: Optional[bool] = None,
+    web_only: Optional[bool] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Enqueues an asynchronous job to wipe all valid sessions on all devices for a given list of users
+    https://docs.slack.dev/reference/methods/admin.users.session.resetBulk
+    """
+    if isinstance(user_ids, (list, tuple)):
+        kwargs.update({"user_ids": ",".join(user_ids)})
+    else:
+        kwargs.update({"user_ids": user_ids})
+    kwargs.update(
+        {
+            "mobile_only": mobile_only,
+            "web_only": web_only,
+        }
+    )
+    return self.api_call("admin.users.session.resetBulk", params=kwargs)
+
+

Enqueues an asynchronous job to wipe all valid sessions on all devices for a given list of users +https://docs.slack.dev/reference/methods/admin.users.session.resetBulk

+
+
+def admin_users_session_setSettings(self,
*,
user_ids: str | Sequence[str],
desktop_app_browser_quit: bool | None = None,
duration: int | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_users_session_setSettings(
+    self,
+    *,
+    user_ids: Union[str, Sequence[str]],
+    desktop_app_browser_quit: Optional[bool] = None,
+    duration: Optional[int] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Configure the user-level session settings—the session duration
+    and what happens when the client closes—for one or more users.
+    https://docs.slack.dev/reference/methods/admin.users.session.setSettings
+    """
+    if isinstance(user_ids, (list, tuple)):
+        kwargs.update({"user_ids": ",".join(user_ids)})
+    else:
+        kwargs.update({"user_ids": user_ids})
+    kwargs.update(
+        {
+            "desktop_app_browser_quit": desktop_app_browser_quit,
+            "duration": duration,
+        }
+    )
+    return self.api_call("admin.users.session.setSettings", params=kwargs)
+
+

Configure the user-level session settings—the session duration +and what happens when the client closes—for one or more users. +https://docs.slack.dev/reference/methods/admin.users.session.setSettings

+
+
+def admin_users_setAdmin(self, *, team_id: str, user_id: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_users_setAdmin(
+    self,
+    *,
+    team_id: str,
+    user_id: str,
+    **kwargs,
+) -> SlackResponse:
+    """Set an existing guest, regular user, or owner to be an admin user.
+    https://docs.slack.dev/reference/methods/admin.users.setAdmin
+    """
+    kwargs.update({"team_id": team_id, "user_id": user_id})
+    return self.api_call("admin.users.setAdmin", params=kwargs)
+
+

Set an existing guest, regular user, or owner to be an admin user. +https://docs.slack.dev/reference/methods/admin.users.setAdmin

+
+
+def admin_users_setExpiration(self, *, expiration_ts: int, user_id: str, team_id: str | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_users_setExpiration(
+    self,
+    *,
+    expiration_ts: int,
+    user_id: str,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Set an expiration for a guest user.
+    https://docs.slack.dev/reference/methods/admin.users.setExpiration
+    """
+    kwargs.update({"expiration_ts": expiration_ts, "team_id": team_id, "user_id": user_id})
+    return self.api_call("admin.users.setExpiration", params=kwargs)
+
+ +
+
+def admin_users_setOwner(self, *, team_id: str, user_id: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_users_setOwner(
+    self,
+    *,
+    team_id: str,
+    user_id: str,
+    **kwargs,
+) -> SlackResponse:
+    """Set an existing guest, regular user, or admin user to be a workspace owner.
+    https://docs.slack.dev/reference/methods/admin.users.setOwner
+    """
+    kwargs.update({"team_id": team_id, "user_id": user_id})
+    return self.api_call("admin.users.setOwner", params=kwargs)
+
+

Set an existing guest, regular user, or admin user to be a workspace owner. +https://docs.slack.dev/reference/methods/admin.users.setOwner

+
+
+def admin_users_setRegular(self, *, team_id: str, user_id: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_users_setRegular(
+    self,
+    *,
+    team_id: str,
+    user_id: str,
+    **kwargs,
+) -> SlackResponse:
+    """Set an existing guest user, admin user, or owner to be a regular user.
+    https://docs.slack.dev/reference/methods/admin.users.setRegular
+    """
+    kwargs.update({"team_id": team_id, "user_id": user_id})
+    return self.api_call("admin.users.setRegular", params=kwargs)
+
+

Set an existing guest user, admin user, or owner to be a regular user. +https://docs.slack.dev/reference/methods/admin.users.setRegular

+
+
+def admin_users_unsupportedVersions_export(self,
*,
date_end_of_support: str | int | None = None,
date_sessions_started: str | int | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_users_unsupportedVersions_export(
+    self,
+    *,
+    date_end_of_support: Optional[Union[str, int]] = None,
+    date_sessions_started: Optional[Union[str, int]] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Ask Slackbot to send you an export listing all workspace members using unsupported software,
+    presented as a zipped CSV file.
+    https://docs.slack.dev/reference/methods/admin.users.unsupportedVersions.export
+    """
+    kwargs.update(
+        {
+            "date_end_of_support": date_end_of_support,
+            "date_sessions_started": date_sessions_started,
+        }
+    )
+    return self.api_call("admin.users.unsupportedVersions.export", params=kwargs)
+
+

Ask Slackbot to send you an export listing all workspace members using unsupported software, +presented as a zipped CSV file. +https://docs.slack.dev/reference/methods/admin.users.unsupportedVersions.export

+
+
+def admin_workflows_collaborators_add(self,
*,
collaborator_ids: str | Sequence[str],
workflow_ids: str | Sequence[str],
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_workflows_collaborators_add(
+    self,
+    *,
+    collaborator_ids: Union[str, Sequence[str]],
+    workflow_ids: Union[str, Sequence[str]],
+    **kwargs,
+) -> SlackResponse:
+    """Add collaborators to workflows within the team or enterprise
+    https://docs.slack.dev/reference/methods/admin.workflows.collaborators.add
+    """
+    if isinstance(collaborator_ids, (list, tuple)):
+        kwargs.update({"collaborator_ids": ",".join(collaborator_ids)})
+    else:
+        kwargs.update({"collaborator_ids": collaborator_ids})
+    if isinstance(workflow_ids, (list, tuple)):
+        kwargs.update({"workflow_ids": ",".join(workflow_ids)})
+    else:
+        kwargs.update({"workflow_ids": workflow_ids})
+    return self.api_call("admin.workflows.collaborators.add", params=kwargs)
+
+

Add collaborators to workflows within the team or enterprise +https://docs.slack.dev/reference/methods/admin.workflows.collaborators.add

+
+
+def admin_workflows_collaborators_remove(self,
*,
collaborator_ids: str | Sequence[str],
workflow_ids: str | Sequence[str],
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_workflows_collaborators_remove(
+    self,
+    *,
+    collaborator_ids: Union[str, Sequence[str]],
+    workflow_ids: Union[str, Sequence[str]],
+    **kwargs,
+) -> SlackResponse:
+    """Remove collaborators from workflows within the team or enterprise
+    https://docs.slack.dev/reference/methods/admin.workflows.collaborators.remove
+    """
+    if isinstance(collaborator_ids, (list, tuple)):
+        kwargs.update({"collaborator_ids": ",".join(collaborator_ids)})
+    else:
+        kwargs.update({"collaborator_ids": collaborator_ids})
+    if isinstance(workflow_ids, (list, tuple)):
+        kwargs.update({"workflow_ids": ",".join(workflow_ids)})
+    else:
+        kwargs.update({"workflow_ids": workflow_ids})
+    return self.api_call("admin.workflows.collaborators.remove", params=kwargs)
+
+

Remove collaborators from workflows within the team or enterprise +https://docs.slack.dev/reference/methods/admin.workflows.collaborators.remove

+
+
+def admin_workflows_permissions_lookup(self,
*,
workflow_ids: str | Sequence[str],
max_workflow_triggers: int | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def admin_workflows_permissions_lookup(
+    self,
+    *,
+    workflow_ids: Union[str, Sequence[str]],
+    max_workflow_triggers: Optional[int] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Look up the permissions for a set of workflows
+    https://docs.slack.dev/reference/methods/admin.workflows.permissions.lookup
+    """
+    if isinstance(workflow_ids, (list, tuple)):
+        kwargs.update({"workflow_ids": ",".join(workflow_ids)})
+    else:
+        kwargs.update({"workflow_ids": workflow_ids})
+    kwargs.update(
+        {
+            "max_workflow_triggers": max_workflow_triggers,
+        }
+    )
+    return self.api_call("admin.workflows.permissions.lookup", params=kwargs)
+
+ +
+ +
+
+ +Expand source code + +
def admin_workflows_search(
+    self,
+    *,
+    app_id: Optional[str] = None,
+    collaborator_ids: Optional[Union[str, Sequence[str]]] = None,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    no_collaborators: Optional[bool] = None,
+    num_trigger_ids: Optional[int] = None,
+    query: Optional[str] = None,
+    sort: Optional[str] = None,
+    sort_dir: Optional[str] = None,
+    source: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Search workflows within the team or enterprise
+    https://docs.slack.dev/reference/methods/admin.workflows.search
+    """
+    if collaborator_ids is not None:
+        if isinstance(collaborator_ids, (list, tuple)):
+            kwargs.update({"collaborator_ids": ",".join(collaborator_ids)})
+        else:
+            kwargs.update({"collaborator_ids": collaborator_ids})
+    kwargs.update(
+        {
+            "app_id": app_id,
+            "cursor": cursor,
+            "limit": limit,
+            "no_collaborators": no_collaborators,
+            "num_trigger_ids": num_trigger_ids,
+            "query": query,
+            "sort": sort,
+            "sort_dir": sort_dir,
+            "source": source,
+        }
+    )
+    return self.api_call("admin.workflows.search", params=kwargs)
+
+

Search workflows within the team or enterprise +https://docs.slack.dev/reference/methods/admin.workflows.search

+
+
+def admin_workflows_unpublish(self, *, workflow_ids: str | Sequence[str], **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def admin_workflows_unpublish(
+    self,
+    *,
+    workflow_ids: Union[str, Sequence[str]],
+    **kwargs,
+) -> SlackResponse:
+    """Unpublish workflows within the team or enterprise
+    https://docs.slack.dev/reference/methods/admin.workflows.unpublish
+    """
+    if isinstance(workflow_ids, (list, tuple)):
+        kwargs.update({"workflow_ids": ",".join(workflow_ids)})
+    else:
+        kwargs.update({"workflow_ids": workflow_ids})
+    return self.api_call("admin.workflows.unpublish", params=kwargs)
+
+

Unpublish workflows within the team or enterprise +https://docs.slack.dev/reference/methods/admin.workflows.unpublish

+
+
+def api_test(self, *, error: str | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def api_test(
+    self,
+    *,
+    error: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Checks API calling code.
+    https://docs.slack.dev/reference/methods/api.test
+    """
+    kwargs.update({"error": error})
+    return self.api_call("api.test", params=kwargs)
+
+ +
+
+def apps_connections_open(self, *, app_token: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def apps_connections_open(
+    self,
+    *,
+    app_token: str,
+    **kwargs,
+) -> SlackResponse:
+    """Generate a temporary Socket Mode WebSocket URL that your app can connect to
+    in order to receive events and interactive payloads
+    https://docs.slack.dev/reference/methods/apps.connections.open
+    """
+    kwargs.update({"token": app_token})
+    return self.api_call("apps.connections.open", http_verb="POST", params=kwargs)
+
+

Generate a temporary Socket Mode WebSocket URL that your app can connect to +in order to receive events and interactive payloads +https://docs.slack.dev/reference/methods/apps.connections.open

+
+
+def apps_event_authorizations_list(self,
*,
event_context: str,
cursor: str | None = None,
limit: int | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def apps_event_authorizations_list(
+    self,
+    *,
+    event_context: str,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Get a list of authorizations for the given event context.
+    Each authorization represents an app installation that the event is visible to.
+    https://docs.slack.dev/reference/methods/apps.event.authorizations.list
+    """
+    kwargs.update({"event_context": event_context, "cursor": cursor, "limit": limit})
+    return self.api_call("apps.event.authorizations.list", params=kwargs)
+
+

Get a list of authorizations for the given event context. +Each authorization represents an app installation that the event is visible to. +https://docs.slack.dev/reference/methods/apps.event.authorizations.list

+
+
+def apps_manifest_create(self, *, manifest: str | Dict[str, Any], **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def apps_manifest_create(
+    self,
+    *,
+    manifest: Union[str, Dict[str, Any]],
+    **kwargs,
+) -> SlackResponse:
+    """Create an app from an app manifest
+    https://docs.slack.dev/reference/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 +
+
+
+ +Expand source code + +
def apps_manifest_delete(
+    self,
+    *,
+    app_id: str,
+    **kwargs,
+) -> SlackResponse:
+    """Permanently deletes an app created through app manifests
+    https://docs.slack.dev/reference/methods/apps.manifest.delete
+    """
+    kwargs.update({"app_id": app_id})
+    return self.api_call("apps.manifest.delete", params=kwargs)
+
+

Permanently deletes an app created through app manifests +https://docs.slack.dev/reference/methods/apps.manifest.delete

+
+
+def apps_manifest_export(self, *, app_id: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def apps_manifest_export(
+    self,
+    *,
+    app_id: str,
+    **kwargs,
+) -> SlackResponse:
+    """Export an app manifest from an existing app
+    https://docs.slack.dev/reference/methods/apps.manifest.export
+    """
+    kwargs.update({"app_id": app_id})
+    return self.api_call("apps.manifest.export", params=kwargs)
+
+

Export an app manifest from an existing app +https://docs.slack.dev/reference/methods/apps.manifest.export

+
+
+def apps_manifest_update(self, *, app_id: str, manifest: str | Dict[str, Any], **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def apps_manifest_update(
+    self,
+    *,
+    app_id: str,
+    manifest: Union[str, Dict[str, Any]],
+    **kwargs,
+) -> SlackResponse:
+    """Update an app from an app manifest
+    https://docs.slack.dev/reference/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: str | Dict[str, Any], app_id: str | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def apps_manifest_validate(
+    self,
+    *,
+    manifest: Union[str, Dict[str, Any]],
+    app_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Validate an app manifest
+    https://docs.slack.dev/reference/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 apps_uninstall(self, *, client_id: str, client_secret: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def apps_uninstall(
+    self,
+    *,
+    client_id: str,
+    client_secret: str,
+    **kwargs,
+) -> SlackResponse:
+    """Uninstalls your app from a workspace.
+    https://docs.slack.dev/reference/methods/apps.uninstall
+    """
+    kwargs.update({"client_id": client_id, "client_secret": client_secret})
+    return self.api_call("apps.uninstall", params=kwargs)
+
+

Uninstalls your app from a workspace. +https://docs.slack.dev/reference/methods/apps.uninstall

+
+
+def assistant_threads_setStatus(self,
*,
channel_id: str,
thread_ts: str,
status: str,
loading_messages: List[str] | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def assistant_threads_setStatus(
+    self,
+    *,
+    channel_id: str,
+    thread_ts: str,
+    status: str,
+    loading_messages: Optional[List[str]] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Set the status for an AI assistant thread.
+    https://docs.slack.dev/reference/methods/assistant.threads.setStatus
+    """
+    kwargs.update(
+        {"channel_id": channel_id, "thread_ts": thread_ts, "status": status, "loading_messages": loading_messages}
+    )
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("assistant.threads.setStatus", json=kwargs)
+
+ +
+
+def assistant_threads_setSuggestedPrompts(self,
*,
channel_id: str,
thread_ts: str,
title: str | None = None,
prompts: List[Dict[str, str]],
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def assistant_threads_setSuggestedPrompts(
+    self,
+    *,
+    channel_id: str,
+    thread_ts: str,
+    title: Optional[str] = None,
+    prompts: List[Dict[str, str]],
+    **kwargs,
+) -> SlackResponse:
+    """Set suggested prompts for the given assistant thread.
+    https://docs.slack.dev/reference/methods/assistant.threads.setSuggestedPrompts
+    """
+    kwargs.update({"channel_id": channel_id, "thread_ts": thread_ts, "prompts": prompts})
+    if title is not None:
+        kwargs.update({"title": title})
+    return self.api_call("assistant.threads.setSuggestedPrompts", json=kwargs)
+
+

Set suggested prompts for the given assistant thread. +https://docs.slack.dev/reference/methods/assistant.threads.setSuggestedPrompts

+
+
+def assistant_threads_setTitle(self, *, channel_id: str, thread_ts: str, title: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def assistant_threads_setTitle(
+    self,
+    *,
+    channel_id: str,
+    thread_ts: str,
+    title: str,
+    **kwargs,
+) -> SlackResponse:
+    """Set the title for the given assistant thread.
+    https://docs.slack.dev/reference/methods/assistant.threads.setTitle
+    """
+    kwargs.update({"channel_id": channel_id, "thread_ts": thread_ts, "title": title})
+    return self.api_call("assistant.threads.setTitle", params=kwargs)
+
+

Set the title for the given assistant thread. +https://docs.slack.dev/reference/methods/assistant.threads.setTitle

+
+
+def auth_revoke(self, *, test: bool | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def auth_revoke(
+    self,
+    *,
+    test: Optional[bool] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Revokes a token.
+    https://docs.slack.dev/reference/methods/auth.revoke
+    """
+    kwargs.update({"test": test})
+    return self.api_call("auth.revoke", http_verb="GET", params=kwargs)
+
+ +
+
+def auth_teams_list(self,
cursor: str | None = None,
limit: int | None = None,
include_icon: bool | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def auth_teams_list(
+    self,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    include_icon: Optional[bool] = None,
+    **kwargs,
+) -> SlackResponse:
+    """List the workspaces a token can access.
+    https://docs.slack.dev/reference/methods/auth.teams.list
+    """
+    kwargs.update({"cursor": cursor, "limit": limit, "include_icon": include_icon})
+    return self.api_call("auth.teams.list", params=kwargs)
+
+

List the workspaces a token can access. +https://docs.slack.dev/reference/methods/auth.teams.list

+
+
+def auth_test(self, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def auth_test(
+    self,
+    **kwargs,
+) -> SlackResponse:
+    """Checks authentication & identity.
+    https://docs.slack.dev/reference/methods/auth.test
+    """
+    return self.api_call("auth.test", params=kwargs)
+
+

Checks authentication & identity. +https://docs.slack.dev/reference/methods/auth.test

+
+
+def bookmarks_add(self,
*,
channel_id: str,
title: str,
type: str,
emoji: str | None = None,
entity_id: str | None = None,
link: str | None = None,
parent_id: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def bookmarks_add(
+    self,
+    *,
+    channel_id: str,
+    title: str,
+    type: str,
+    emoji: Optional[str] = None,
+    entity_id: Optional[str] = None,
+    link: Optional[str] = None,  # include when type is 'link'
+    parent_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Add bookmark to a channel.
+    https://docs.slack.dev/reference/methods/bookmarks.add
+    """
+    kwargs.update(
+        {
+            "channel_id": channel_id,
+            "title": title,
+            "type": type,
+            "emoji": emoji,
+            "entity_id": entity_id,
+            "link": link,
+            "parent_id": parent_id,
+        }
+    )
+    return self.api_call("bookmarks.add", http_verb="POST", params=kwargs)
+
+ +
+
+def bookmarks_edit(self,
*,
bookmark_id: str,
channel_id: str,
emoji: str | None = None,
link: str | None = None,
title: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def bookmarks_edit(
+    self,
+    *,
+    bookmark_id: str,
+    channel_id: str,
+    emoji: Optional[str] = None,
+    link: Optional[str] = None,
+    title: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Edit bookmark.
+    https://docs.slack.dev/reference/methods/bookmarks.edit
+    """
+    kwargs.update(
+        {
+            "bookmark_id": bookmark_id,
+            "channel_id": channel_id,
+            "emoji": emoji,
+            "link": link,
+            "title": title,
+        }
+    )
+    return self.api_call("bookmarks.edit", http_verb="POST", params=kwargs)
+
+ +
+
+def bookmarks_list(self, *, channel_id: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def bookmarks_list(
+    self,
+    *,
+    channel_id: str,
+    **kwargs,
+) -> SlackResponse:
+    """List bookmark for the channel.
+    https://docs.slack.dev/reference/methods/bookmarks.list
+    """
+    kwargs.update({"channel_id": channel_id})
+    return self.api_call("bookmarks.list", http_verb="POST", params=kwargs)
+
+ +
+
+def bookmarks_remove(self, *, bookmark_id: str, channel_id: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def bookmarks_remove(
+    self,
+    *,
+    bookmark_id: str,
+    channel_id: str,
+    **kwargs,
+) -> SlackResponse:
+    """Remove bookmark from the channel.
+    https://docs.slack.dev/reference/methods/bookmarks.remove
+    """
+    kwargs.update({"bookmark_id": bookmark_id, "channel_id": channel_id})
+    return self.api_call("bookmarks.remove", http_verb="POST", params=kwargs)
+
+ +
+
+def bots_info(self, *, bot: str | None = None, team_id: str | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def bots_info(
+    self,
+    *,
+    bot: Optional[str] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Gets information about a bot user.
+    https://docs.slack.dev/reference/methods/bots.info
+    """
+    kwargs.update({"bot": bot, "team_id": team_id})
+    return self.api_call("bots.info", http_verb="GET", params=kwargs)
+
+

Gets information about a bot user. +https://docs.slack.dev/reference/methods/bots.info

+
+
+def calls_add(self,
*,
external_unique_id: str,
join_url: str,
created_by: str | None = None,
date_start: int | None = None,
desktop_app_join_url: str | None = None,
external_display_id: str | None = None,
title: str | None = None,
users: str | Sequence[Dict[str, str]] | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def calls_add(
+    self,
+    *,
+    external_unique_id: str,
+    join_url: str,
+    created_by: Optional[str] = None,
+    date_start: Optional[int] = None,
+    desktop_app_join_url: Optional[str] = None,
+    external_display_id: Optional[str] = None,
+    title: Optional[str] = None,
+    users: Optional[Union[str, Sequence[Dict[str, str]]]] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Registers a new Call.
+    https://docs.slack.dev/reference/methods/calls.add
+    """
+    kwargs.update(
+        {
+            "external_unique_id": external_unique_id,
+            "join_url": join_url,
+            "created_by": created_by,
+            "date_start": date_start,
+            "desktop_app_join_url": desktop_app_join_url,
+            "external_display_id": external_display_id,
+            "title": title,
+        }
+    )
+    _update_call_participants(
+        kwargs,
+        users if users is not None else kwargs.get("users"),  # type: ignore[arg-type]
+    )
+    return self.api_call("calls.add", http_verb="POST", params=kwargs)
+
+ +
+
+def calls_end(self, *, id: str, duration: int | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def calls_end(
+    self,
+    *,
+    id: str,
+    duration: Optional[int] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Ends a Call.
+    https://docs.slack.dev/reference/methods/calls.end
+    """
+    kwargs.update({"id": id, "duration": duration})
+    return self.api_call("calls.end", http_verb="POST", params=kwargs)
+
+ +
+
+def calls_info(self, *, id: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def calls_info(
+    self,
+    *,
+    id: str,
+    **kwargs,
+) -> SlackResponse:
+    """Returns information about a Call.
+    https://docs.slack.dev/reference/methods/calls.info
+    """
+    kwargs.update({"id": id})
+    return self.api_call("calls.info", http_verb="POST", params=kwargs)
+
+

Returns information about a Call. +https://docs.slack.dev/reference/methods/calls.info

+
+
+def calls_participants_add(self, *, id: str, users: str | Sequence[Dict[str, str]], **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def calls_participants_add(
+    self,
+    *,
+    id: str,
+    users: Union[str, Sequence[Dict[str, str]]],
+    **kwargs,
+) -> SlackResponse:
+    """Registers new participants added to a Call.
+    https://docs.slack.dev/reference/methods/calls.participants.add
+    """
+    kwargs.update({"id": id})
+    _update_call_participants(kwargs, users)
+    return self.api_call("calls.participants.add", http_verb="POST", params=kwargs)
+
+

Registers new participants added to a Call. +https://docs.slack.dev/reference/methods/calls.participants.add

+
+
+def calls_participants_remove(self, *, id: str, users: str | Sequence[Dict[str, str]], **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def calls_participants_remove(
+    self,
+    *,
+    id: str,
+    users: Union[str, Sequence[Dict[str, str]]],
+    **kwargs,
+) -> SlackResponse:
+    """Registers participants removed from a Call.
+    https://docs.slack.dev/reference/methods/calls.participants.remove
+    """
+    kwargs.update({"id": id})
+    _update_call_participants(kwargs, users)
+    return self.api_call("calls.participants.remove", http_verb="POST", params=kwargs)
+
+

Registers participants removed from a Call. +https://docs.slack.dev/reference/methods/calls.participants.remove

+
+
+def calls_update(self,
*,
id: str,
desktop_app_join_url: str | None = None,
join_url: str | None = None,
title: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def calls_update(
+    self,
+    *,
+    id: str,
+    desktop_app_join_url: Optional[str] = None,
+    join_url: Optional[str] = None,
+    title: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Updates information about a Call.
+    https://docs.slack.dev/reference/methods/calls.update
+    """
+    kwargs.update(
+        {
+            "id": id,
+            "desktop_app_join_url": desktop_app_join_url,
+            "join_url": join_url,
+            "title": title,
+        }
+    )
+    return self.api_call("calls.update", http_verb="POST", params=kwargs)
+
+ +
+
+def canvases_access_delete(self,
*,
canvas_id: str,
channel_ids: str | Sequence[str] | None = None,
user_ids: str | Sequence[str] | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def canvases_access_delete(
+    self,
+    *,
+    canvas_id: str,
+    channel_ids: Optional[Union[Sequence[str], str]] = None,
+    user_ids: Optional[Union[Sequence[str], str]] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Create a Channel Canvas for a channel
+    https://docs.slack.dev/reference/methods/canvases.access.delete
+    """
+    kwargs.update({"canvas_id": canvas_id})
+    if channel_ids is not None:
+        if isinstance(channel_ids, (list, tuple)):
+            kwargs.update({"channel_ids": ",".join(channel_ids)})
+        else:
+            kwargs.update({"channel_ids": channel_ids})
+    if user_ids is not None:
+        if isinstance(user_ids, (list, tuple)):
+            kwargs.update({"user_ids": ",".join(user_ids)})
+        else:
+            kwargs.update({"user_ids": user_ids})
+    return self.api_call("canvases.access.delete", params=kwargs)
+
+ +
+
+def canvases_access_set(self,
*,
canvas_id: str,
access_level: str,
channel_ids: str | Sequence[str] | None = None,
user_ids: str | Sequence[str] | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def canvases_access_set(
+    self,
+    *,
+    canvas_id: str,
+    access_level: str,
+    channel_ids: Optional[Union[Sequence[str], str]] = None,
+    user_ids: Optional[Union[Sequence[str], str]] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Sets the access level to a canvas for specified entities
+    https://docs.slack.dev/reference/methods/canvases.access.set
+    """
+    kwargs.update({"canvas_id": canvas_id, "access_level": access_level})
+    if channel_ids is not None:
+        if isinstance(channel_ids, (list, tuple)):
+            kwargs.update({"channel_ids": ",".join(channel_ids)})
+        else:
+            kwargs.update({"channel_ids": channel_ids})
+    if user_ids is not None:
+        if isinstance(user_ids, (list, tuple)):
+            kwargs.update({"user_ids": ",".join(user_ids)})
+        else:
+            kwargs.update({"user_ids": user_ids})
+
+    return self.api_call("canvases.access.set", params=kwargs)
+
+

Sets the access level to a canvas for specified entities +https://docs.slack.dev/reference/methods/canvases.access.set

+
+
+def canvases_create(self, *, title: str | None = None, document_content: Dict[str, str], **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def canvases_create(
+    self,
+    *,
+    title: Optional[str] = None,
+    document_content: Dict[str, str],
+    **kwargs,
+) -> SlackResponse:
+    """Create Canvas for a user
+    https://docs.slack.dev/reference/methods/canvases.create
+    """
+    kwargs.update({"title": title, "document_content": document_content})
+    return self.api_call("canvases.create", json=kwargs)
+
+ +
+
+def canvases_delete(self, *, canvas_id: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def canvases_delete(
+    self,
+    *,
+    canvas_id: str,
+    **kwargs,
+) -> SlackResponse:
+    """Deletes a canvas
+    https://docs.slack.dev/reference/methods/canvases.delete
+    """
+    kwargs.update({"canvas_id": canvas_id})
+    return self.api_call("canvases.delete", params=kwargs)
+
+ +
+
+def canvases_edit(self, *, canvas_id: str, changes: Sequence[Dict[str, Any]], **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def canvases_edit(
+    self,
+    *,
+    canvas_id: str,
+    changes: Sequence[Dict[str, Any]],
+    **kwargs,
+) -> SlackResponse:
+    """Update an existing canvas
+    https://docs.slack.dev/reference/methods/canvases.edit
+    """
+    kwargs.update({"canvas_id": canvas_id, "changes": changes})
+    return self.api_call("canvases.edit", json=kwargs)
+
+ +
+
+def canvases_sections_lookup(self, *, canvas_id: str, criteria: Dict[str, Any], **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def canvases_sections_lookup(
+    self,
+    *,
+    canvas_id: str,
+    criteria: Dict[str, Any],
+    **kwargs,
+) -> SlackResponse:
+    """Find sections matching the provided criteria
+    https://docs.slack.dev/reference/methods/canvases.sections.lookup
+    """
+    kwargs.update({"canvas_id": canvas_id, "criteria": json.dumps(criteria)})
+    return self.api_call("canvases.sections.lookup", params=kwargs)
+
+

Find sections matching the provided criteria +https://docs.slack.dev/reference/methods/canvases.sections.lookup

+
+
+def channels_archive(self, *, channel: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def channels_archive(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> SlackResponse:
+    """Archives a channel."""
+    kwargs.update({"channel": channel})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("channels.archive", json=kwargs)
+
+

Archives a channel.

+
+
+def channels_create(self, *, name: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def channels_create(
+    self,
+    *,
+    name: str,
+    **kwargs,
+) -> SlackResponse:
+    """Creates a channel."""
+    kwargs.update({"name": name})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("channels.create", json=kwargs)
+
+

Creates a channel.

+
+
+def channels_history(self, *, channel: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def channels_history(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> SlackResponse:
+    """Fetches history of messages and events from a channel."""
+    kwargs.update({"channel": channel})
+    return self.api_call("channels.history", http_verb="GET", params=kwargs)
+
+

Fetches history of messages and events from a channel.

+
+
+def channels_info(self, *, channel: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def channels_info(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> SlackResponse:
+    """Gets information about a channel."""
+    kwargs.update({"channel": channel})
+    return self.api_call("channels.info", http_verb="GET", params=kwargs)
+
+

Gets information about a channel.

+
+
+def channels_invite(self, *, channel: str, user: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def channels_invite(
+    self,
+    *,
+    channel: str,
+    user: str,
+    **kwargs,
+) -> SlackResponse:
+    """Invites a user to a channel."""
+    kwargs.update({"channel": channel, "user": user})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("channels.invite", json=kwargs)
+
+

Invites a user to a channel.

+
+
+def channels_join(self, *, name: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def channels_join(
+    self,
+    *,
+    name: str,
+    **kwargs,
+) -> SlackResponse:
+    """Joins a channel, creating it if needed."""
+    kwargs.update({"name": name})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("channels.join", json=kwargs)
+
+

Joins a channel, creating it if needed.

+
+
+def channels_kick(self, *, channel: str, user: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def channels_kick(
+    self,
+    *,
+    channel: str,
+    user: str,
+    **kwargs,
+) -> SlackResponse:
+    """Removes a user from a channel."""
+    kwargs.update({"channel": channel, "user": user})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("channels.kick", json=kwargs)
+
+

Removes a user from a channel.

+
+
+def channels_leave(self, *, channel: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def channels_leave(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> SlackResponse:
+    """Leaves a channel."""
+    kwargs.update({"channel": channel})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("channels.leave", json=kwargs)
+
+

Leaves a channel.

+
+
+def channels_list(self, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def channels_list(
+    self,
+    **kwargs,
+) -> SlackResponse:
+    """Lists all channels in a Slack team."""
+    return self.api_call("channels.list", http_verb="GET", params=kwargs)
+
+

Lists all channels in a Slack team.

+
+
+def channels_mark(self, *, channel: str, ts: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def channels_mark(
+    self,
+    *,
+    channel: str,
+    ts: str,
+    **kwargs,
+) -> SlackResponse:
+    """Sets the read cursor in a channel."""
+    kwargs.update({"channel": channel, "ts": ts})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("channels.mark", json=kwargs)
+
+

Sets the read cursor in a channel.

+
+
+def channels_rename(self, *, channel: str, name: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def channels_rename(
+    self,
+    *,
+    channel: str,
+    name: str,
+    **kwargs,
+) -> SlackResponse:
+    """Renames a channel."""
+    kwargs.update({"channel": channel, "name": name})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("channels.rename", json=kwargs)
+
+

Renames a channel.

+
+
+def channels_replies(self, *, channel: str, thread_ts: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def channels_replies(
+    self,
+    *,
+    channel: str,
+    thread_ts: str,
+    **kwargs,
+) -> SlackResponse:
+    """Retrieve a thread of messages posted to a channel"""
+    kwargs.update({"channel": channel, "thread_ts": thread_ts})
+    return self.api_call("channels.replies", http_verb="GET", params=kwargs)
+
+

Retrieve a thread of messages posted to a channel

+
+
+def channels_setPurpose(self, *, channel: str, purpose: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def channels_setPurpose(
+    self,
+    *,
+    channel: str,
+    purpose: str,
+    **kwargs,
+) -> SlackResponse:
+    """Sets the purpose for a channel."""
+    kwargs.update({"channel": channel, "purpose": purpose})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("channels.setPurpose", json=kwargs)
+
+

Sets the purpose for a channel.

+
+
+def channels_setTopic(self, *, channel: str, topic: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def channels_setTopic(
+    self,
+    *,
+    channel: str,
+    topic: str,
+    **kwargs,
+) -> SlackResponse:
+    """Sets the topic for a channel."""
+    kwargs.update({"channel": channel, "topic": topic})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("channels.setTopic", json=kwargs)
+
+

Sets the topic for a channel.

+
+
+def channels_unarchive(self, *, channel: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def channels_unarchive(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> SlackResponse:
+    """Unarchives a channel."""
+    kwargs.update({"channel": channel})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("channels.unarchive", json=kwargs)
+
+

Unarchives a channel.

+
+
+def chat_appendStream(self, *, channel: str, ts: str, markdown_text: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def chat_appendStream(
+    self,
+    *,
+    channel: str,
+    ts: str,
+    markdown_text: str,
+    **kwargs,
+) -> SlackResponse:
+    """Appends text to an existing streaming conversation.
+    https://docs.slack.dev/reference/methods/chat.appendStream
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "ts": ts,
+            "markdown_text": markdown_text,
+        }
+    )
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("chat.appendStream", json=kwargs)
+
+

Appends text to an existing streaming conversation. +https://docs.slack.dev/reference/methods/chat.appendStream

+
+
+def chat_delete(self, *, channel: str, ts: str, as_user: bool | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def chat_delete(
+    self,
+    *,
+    channel: str,
+    ts: str,
+    as_user: Optional[bool] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Deletes a message.
+    https://docs.slack.dev/reference/methods/chat.delete
+    """
+    kwargs.update({"channel": channel, "ts": ts, "as_user": as_user})
+    return self.api_call("chat.delete", params=kwargs)
+
+ +
+
+def chat_deleteScheduledMessage(self,
*,
channel: str,
scheduled_message_id: str,
as_user: bool | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def chat_deleteScheduledMessage(
+    self,
+    *,
+    channel: str,
+    scheduled_message_id: str,
+    as_user: Optional[bool] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Deletes a scheduled message.
+    https://docs.slack.dev/reference/methods/chat.deleteScheduledMessage
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "scheduled_message_id": scheduled_message_id,
+            "as_user": as_user,
+        }
+    )
+    return self.api_call("chat.deleteScheduledMessage", params=kwargs)
+
+ +
+ +
+
+ +Expand source code + +
def chat_getPermalink(
+    self,
+    *,
+    channel: str,
+    message_ts: str,
+    **kwargs,
+) -> SlackResponse:
+    """Retrieve a permalink URL for a specific extant message
+    https://docs.slack.dev/reference/methods/chat.getPermalink
+    """
+    kwargs.update({"channel": channel, "message_ts": message_ts})
+    return self.api_call("chat.getPermalink", http_verb="GET", params=kwargs)
+
+

Retrieve a permalink URL for a specific extant message +https://docs.slack.dev/reference/methods/chat.getPermalink

+
+
+def chat_meMessage(self, *, channel: str, text: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def chat_meMessage(
+    self,
+    *,
+    channel: str,
+    text: str,
+    **kwargs,
+) -> SlackResponse:
+    """Share a me message into a channel.
+    https://docs.slack.dev/reference/methods/chat.meMessage
+    """
+    kwargs.update({"channel": channel, "text": text})
+    return self.api_call("chat.meMessage", params=kwargs)
+
+ +
+
+def chat_postEphemeral(self,
*,
channel: str,
user: str,
text: str | None = None,
as_user: bool | None = None,
attachments: str | Sequence[Dict | Attachment] | None = None,
blocks: str | Sequence[Dict | Block] | None = None,
thread_ts: str | None = None,
icon_emoji: str | None = None,
icon_url: str | None = None,
link_names: bool | None = None,
username: str | None = None,
parse: str | None = None,
markdown_text: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def chat_postEphemeral(
+    self,
+    *,
+    channel: str,
+    user: str,
+    text: Optional[str] = None,
+    as_user: Optional[bool] = None,
+    attachments: Optional[Union[str, Sequence[Union[Dict, Attachment]]]] = None,
+    blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None,
+    thread_ts: Optional[str] = None,
+    icon_emoji: Optional[str] = None,
+    icon_url: Optional[str] = None,
+    link_names: Optional[bool] = None,
+    username: Optional[str] = None,
+    parse: Optional[str] = None,
+    markdown_text: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Sends an ephemeral message to a user in a channel.
+    https://docs.slack.dev/reference/methods/chat.postEphemeral
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "user": user,
+            "text": text,
+            "as_user": as_user,
+            "attachments": attachments,
+            "blocks": blocks,
+            "thread_ts": thread_ts,
+            "icon_emoji": icon_emoji,
+            "icon_url": icon_url,
+            "link_names": link_names,
+            "username": username,
+            "parse": parse,
+            "markdown_text": markdown_text,
+        }
+    )
+    _parse_web_class_objects(kwargs)
+    kwargs = _remove_none_values(kwargs)
+    _warn_if_message_text_content_is_missing("chat.postEphemeral", kwargs)
+    # NOTE: intentionally using json over params for the API methods using blocks/attachments
+    return self.api_call("chat.postEphemeral", json=kwargs)
+
+

Sends an ephemeral message to a user in a channel. +https://docs.slack.dev/reference/methods/chat.postEphemeral

+
+
+def chat_postMessage(self,
*,
channel: str,
text: str | None = None,
as_user: bool | None = None,
attachments: str | Sequence[Dict | Attachment] | None = None,
blocks: str | Sequence[Dict | Block] | None = None,
thread_ts: str | None = None,
reply_broadcast: bool | None = None,
unfurl_links: bool | None = None,
unfurl_media: bool | None = None,
container_id: str | None = None,
icon_emoji: str | None = None,
icon_url: str | None = None,
mrkdwn: bool | None = None,
link_names: bool | None = None,
username: str | None = None,
parse: str | None = None,
metadata: Dict | Metadata | EventAndEntityMetadata | None = None,
markdown_text: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def chat_postMessage(
+    self,
+    *,
+    channel: str,
+    text: Optional[str] = None,
+    as_user: Optional[bool] = None,
+    attachments: Optional[Union[str, Sequence[Union[Dict, Attachment]]]] = None,
+    blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None,
+    thread_ts: Optional[str] = None,
+    reply_broadcast: Optional[bool] = None,
+    unfurl_links: Optional[bool] = None,
+    unfurl_media: Optional[bool] = None,
+    container_id: Optional[str] = None,
+    icon_emoji: Optional[str] = None,
+    icon_url: Optional[str] = None,
+    mrkdwn: Optional[bool] = None,
+    link_names: Optional[bool] = None,
+    username: Optional[str] = None,
+    parse: Optional[str] = None,  # none, full
+    metadata: Optional[Union[Dict, Metadata, EventAndEntityMetadata]] = None,
+    markdown_text: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Sends a message to a channel.
+    https://docs.slack.dev/reference/methods/chat.postMessage
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "text": text,
+            "as_user": as_user,
+            "attachments": attachments,
+            "blocks": blocks,
+            "thread_ts": thread_ts,
+            "reply_broadcast": reply_broadcast,
+            "unfurl_links": unfurl_links,
+            "unfurl_media": unfurl_media,
+            "container_id": container_id,
+            "icon_emoji": icon_emoji,
+            "icon_url": icon_url,
+            "mrkdwn": mrkdwn,
+            "link_names": link_names,
+            "username": username,
+            "parse": parse,
+            "metadata": metadata,
+            "markdown_text": markdown_text,
+        }
+    )
+    _parse_web_class_objects(kwargs)
+    kwargs = _remove_none_values(kwargs)
+    _warn_if_message_text_content_is_missing("chat.postMessage", kwargs)
+    # NOTE: intentionally using json over params for the API methods using blocks/attachments
+    return self.api_call("chat.postMessage", json=kwargs)
+
+ +
+
+def chat_scheduleMessage(self,
*,
channel: str,
post_at: str | int,
text: str | None = None,
as_user: bool | None = None,
attachments: str | Sequence[Dict | Attachment] | None = None,
blocks: str | Sequence[Dict | Block] | None = None,
thread_ts: str | None = None,
parse: str | None = None,
reply_broadcast: bool | None = None,
unfurl_links: bool | None = None,
unfurl_media: bool | None = None,
link_names: bool | None = None,
metadata: Dict | Metadata | None = None,
markdown_text: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def chat_scheduleMessage(
+    self,
+    *,
+    channel: str,
+    post_at: Union[str, int],
+    text: Optional[str] = None,
+    as_user: Optional[bool] = None,
+    attachments: Optional[Union[str, Sequence[Union[Dict, Attachment]]]] = None,
+    blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None,
+    thread_ts: Optional[str] = None,
+    parse: Optional[str] = None,
+    reply_broadcast: Optional[bool] = None,
+    unfurl_links: Optional[bool] = None,
+    unfurl_media: Optional[bool] = None,
+    link_names: Optional[bool] = None,
+    metadata: Optional[Union[Dict, Metadata]] = None,
+    markdown_text: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Schedules a message.
+    https://docs.slack.dev/reference/methods/chat.scheduleMessage
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "post_at": post_at,
+            "text": text,
+            "as_user": as_user,
+            "attachments": attachments,
+            "blocks": blocks,
+            "thread_ts": thread_ts,
+            "reply_broadcast": reply_broadcast,
+            "parse": parse,
+            "unfurl_links": unfurl_links,
+            "unfurl_media": unfurl_media,
+            "link_names": link_names,
+            "metadata": metadata,
+            "markdown_text": markdown_text,
+        }
+    )
+    _parse_web_class_objects(kwargs)
+    kwargs = _remove_none_values(kwargs)
+    _warn_if_message_text_content_is_missing("chat.scheduleMessage", kwargs)
+    # NOTE: intentionally using json over params for the API methods using blocks/attachments
+    return self.api_call("chat.scheduleMessage", json=kwargs)
+
+ +
+
+def chat_scheduledMessages_list(self,
*,
channel: str | None = None,
cursor: str | None = None,
latest: str | None = None,
limit: int | None = None,
oldest: str | None = None,
team_id: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def chat_scheduledMessages_list(
+    self,
+    *,
+    channel: Optional[str] = None,
+    cursor: Optional[str] = None,
+    latest: Optional[str] = None,
+    limit: Optional[int] = None,
+    oldest: Optional[str] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Lists all scheduled messages.
+    https://docs.slack.dev/reference/methods/chat.scheduledMessages.list
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "cursor": cursor,
+            "latest": latest,
+            "limit": limit,
+            "oldest": oldest,
+            "team_id": team_id,
+        }
+    )
+    return self.api_call("chat.scheduledMessages.list", params=kwargs)
+
+ +
+
+def chat_startStream(self,
*,
channel: str,
thread_ts: str,
markdown_text: str | None = None,
recipient_team_id: str | None = None,
recipient_user_id: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def chat_startStream(
+    self,
+    *,
+    channel: str,
+    thread_ts: str,
+    markdown_text: Optional[str] = None,
+    recipient_team_id: Optional[str] = None,
+    recipient_user_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Starts a new streaming conversation.
+    https://docs.slack.dev/reference/methods/chat.startStream
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "thread_ts": thread_ts,
+            "markdown_text": markdown_text,
+            "recipient_team_id": recipient_team_id,
+            "recipient_user_id": recipient_user_id,
+        }
+    )
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("chat.startStream", json=kwargs)
+
+

Starts a new streaming conversation. +https://docs.slack.dev/reference/methods/chat.startStream

+
+
+def chat_stopStream(self,
*,
channel: str,
ts: str,
markdown_text: str | None = None,
blocks: str | Sequence[Dict | Block] | None = None,
metadata: Dict | Metadata | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def chat_stopStream(
+    self,
+    *,
+    channel: str,
+    ts: str,
+    markdown_text: Optional[str] = None,
+    blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None,
+    metadata: Optional[Union[Dict, Metadata]] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Stops a streaming conversation.
+    https://docs.slack.dev/reference/methods/chat.stopStream
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "ts": ts,
+            "markdown_text": markdown_text,
+            "blocks": blocks,
+            "metadata": metadata,
+        }
+    )
+    _parse_web_class_objects(kwargs)
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("chat.stopStream", json=kwargs)
+
+ +
+
+def chat_stream(self,
*,
buffer_size: int = 256,
channel: str,
thread_ts: str,
recipient_team_id: str | None = None,
recipient_user_id: str | None = None,
**kwargs) ‑> ChatStream
+
+
+
+ +Expand source code + +
def chat_stream(
+    self,
+    *,
+    buffer_size: int = 256,
+    channel: str,
+    thread_ts: str,
+    recipient_team_id: Optional[str] = None,
+    recipient_user_id: Optional[str] = None,
+    **kwargs,
+) -> ChatStream:
+    """Stream markdown text into a conversation.
+
+    This method starts a new chat stream in a conversation that can be appended to. After appending an entire message,
+    the stream can be stopped with concluding arguments such as "blocks" for gathering feedback.
+
+    The following methods are used:
+
+    - chat.startStream: Starts a new streaming conversation.
+      [Reference](https://docs.slack.dev/reference/methods/chat.startStream).
+    - chat.appendStream: Appends text to an existing streaming conversation.
+      [Reference](https://docs.slack.dev/reference/methods/chat.appendStream).
+    - chat.stopStream: Stops a streaming conversation.
+      [Reference](https://docs.slack.dev/reference/methods/chat.stopStream).
+
+    Args:
+        buffer_size: The length of markdown_text to buffer in-memory before calling a stream method. Increasing this
+          value decreases the number of method calls made for the same amount of text, which is useful to avoid rate
+          limits. Default: 256.
+        channel: An encoded ID that represents a channel, private group, or DM.
+        thread_ts: Provide another message's ts value to reply to. Streamed messages should always be replies to a user
+          request.
+        recipient_team_id: The encoded ID of the team the user receiving the streaming text belongs to. Required when
+          streaming to channels.
+        recipient_user_id: The encoded ID of the user to receive the streaming text. Required when streaming to channels.
+        **kwargs: Additional arguments passed to the underlying API calls.
+
+    Returns:
+        ChatStream instance for managing the stream
+
+    Example:
+        ```python
+        streamer = client.chat_stream(
+            channel="C0123456789",
+            thread_ts="1700000001.123456",
+            recipient_team_id="T0123456789",
+            recipient_user_id="U0123456789",
+        )
+        streamer.append(markdown_text="**hello wo")
+        streamer.append(markdown_text="rld!**")
+        streamer.stop()
+        ```
+    """
+    return ChatStream(
+        self,
+        logger=self._logger,
+        channel=channel,
+        thread_ts=thread_ts,
+        recipient_team_id=recipient_team_id,
+        recipient_user_id=recipient_user_id,
+        buffer_size=buffer_size,
+        **kwargs,
+    )
+
+

Stream markdown text into a conversation.

+

This method starts a new chat stream in a conversation that can be appended to. After appending an entire message, +the stream can be stopped with concluding arguments such as "blocks" for gathering feedback.

+

The following methods are used:

+
    +
  • chat.startStream: Starts a new streaming conversation. +Reference.
  • +
  • chat.appendStream: Appends text to an existing streaming conversation. +Reference.
  • +
  • chat.stopStream: Stops a streaming conversation. +Reference.
  • +
+

Args

+
+
buffer_size
+
The length of markdown_text to buffer in-memory before calling a stream method. Increasing this +value decreases the number of method calls made for the same amount of text, which is useful to avoid rate +limits. Default: 256.
+
channel
+
An encoded ID that represents a channel, private group, or DM.
+
thread_ts
+
Provide another message's ts value to reply to. Streamed messages should always be replies to a user +request.
+
recipient_team_id
+
The encoded ID of the team the user receiving the streaming text belongs to. Required when +streaming to channels.
+
recipient_user_id
+
The encoded ID of the user to receive the streaming text. Required when streaming to channels.
+
**kwargs
+
Additional arguments passed to the underlying API calls.
+
+

Returns

+

ChatStream instance for managing the stream

+

Example

+
streamer = client.chat_stream(
+    channel="C0123456789",
+    thread_ts="1700000001.123456",
+    recipient_team_id="T0123456789",
+    recipient_user_id="U0123456789",
+)
+streamer.append(markdown_text="**hello wo")
+streamer.append(markdown_text="rld!**")
+streamer.stop()
+
+
+
+def chat_unfurl(self,
*,
channel: str | None = None,
ts: str | None = None,
source: str | None = None,
unfurl_id: str | None = None,
unfurls: Dict[str, Dict] | None = None,
metadata: Dict | EventAndEntityMetadata | None = None,
user_auth_blocks: str | Sequence[Dict | Block] | None = None,
user_auth_message: str | None = None,
user_auth_required: bool | None = None,
user_auth_url: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def chat_unfurl(
+    self,
+    *,
+    channel: Optional[str] = None,
+    ts: Optional[str] = None,
+    source: Optional[str] = None,
+    unfurl_id: Optional[str] = None,
+    unfurls: Optional[Dict[str, Dict]] = None,  # or user_auth_*
+    metadata: Optional[Union[Dict, EventAndEntityMetadata]] = None,
+    user_auth_blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None,
+    user_auth_message: Optional[str] = None,
+    user_auth_required: Optional[bool] = None,
+    user_auth_url: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Provide custom unfurl behavior for user-posted URLs.
+    https://docs.slack.dev/reference/methods/chat.unfurl
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "ts": ts,
+            "source": source,
+            "unfurl_id": unfurl_id,
+            "unfurls": unfurls,
+            "metadata": metadata,
+            "user_auth_blocks": user_auth_blocks,
+            "user_auth_message": user_auth_message,
+            "user_auth_required": user_auth_required,
+            "user_auth_url": user_auth_url,
+        }
+    )
+    _parse_web_class_objects(kwargs)  # for user_auth_blocks
+    kwargs = _remove_none_values(kwargs)
+    # NOTE: intentionally using json over params for API methods using blocks/attachments
+    return self.api_call("chat.unfurl", json=kwargs)
+
+

Provide custom unfurl behavior for user-posted URLs. +https://docs.slack.dev/reference/methods/chat.unfurl

+
+
+def chat_update(self,
*,
channel: str,
ts: str,
text: str | None = None,
attachments: str | Sequence[Dict | Attachment] | None = None,
blocks: str | Sequence[Dict | Block] | None = None,
as_user: bool | None = None,
file_ids: str | Sequence[str] | None = None,
link_names: bool | None = None,
parse: str | None = None,
reply_broadcast: bool | None = None,
metadata: Dict | Metadata | None = None,
markdown_text: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def chat_update(
+    self,
+    *,
+    channel: str,
+    ts: str,
+    text: Optional[str] = None,
+    attachments: Optional[Union[str, Sequence[Union[Dict, Attachment]]]] = None,
+    blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None,
+    as_user: Optional[bool] = None,
+    file_ids: Optional[Union[str, Sequence[str]]] = None,
+    link_names: Optional[bool] = None,
+    parse: Optional[str] = None,  # none, full
+    reply_broadcast: Optional[bool] = None,
+    metadata: Optional[Union[Dict, Metadata]] = None,
+    markdown_text: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Updates a message in a channel.
+    https://docs.slack.dev/reference/methods/chat.update
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "ts": ts,
+            "text": text,
+            "attachments": attachments,
+            "blocks": blocks,
+            "as_user": as_user,
+            "link_names": link_names,
+            "parse": parse,
+            "reply_broadcast": reply_broadcast,
+            "metadata": metadata,
+            "markdown_text": markdown_text,
+        }
+    )
+    if isinstance(file_ids, (list, tuple)):
+        kwargs.update({"file_ids": ",".join(file_ids)})
+    else:
+        kwargs.update({"file_ids": file_ids})
+    _parse_web_class_objects(kwargs)
+    kwargs = _remove_none_values(kwargs)
+    _warn_if_message_text_content_is_missing("chat.update", kwargs)
+    # NOTE: intentionally using json over params for API methods using blocks/attachments
+    return self.api_call("chat.update", json=kwargs)
+
+ +
+
+def conversations_acceptSharedInvite(self,
*,
channel_name: str,
channel_id: str | None = None,
invite_id: str | None = None,
free_trial_accepted: bool | None = None,
is_private: bool | None = None,
team_id: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def conversations_acceptSharedInvite(
+    self,
+    *,
+    channel_name: str,
+    channel_id: Optional[str] = None,
+    invite_id: Optional[str] = None,
+    free_trial_accepted: Optional[bool] = None,
+    is_private: Optional[bool] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Accepts an invitation to a Slack Connect channel.
+    https://docs.slack.dev/reference/methods/conversations.acceptSharedInvite
+    """
+    if channel_id is None and invite_id is None:
+        raise e.SlackRequestError("Either channel_id or invite_id must be provided.")
+    kwargs.update(
+        {
+            "channel_name": channel_name,
+            "channel_id": channel_id,
+            "invite_id": invite_id,
+            "free_trial_accepted": free_trial_accepted,
+            "is_private": is_private,
+            "team_id": team_id,
+        }
+    )
+    return self.api_call("conversations.acceptSharedInvite", http_verb="POST", params=kwargs)
+
+

Accepts an invitation to a Slack Connect channel. +https://docs.slack.dev/reference/methods/conversations.acceptSharedInvite

+
+
+def conversations_approveSharedInvite(self, *, invite_id: str, target_team: str | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def conversations_approveSharedInvite(
+    self,
+    *,
+    invite_id: str,
+    target_team: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Approves an invitation to a Slack Connect channel.
+    https://docs.slack.dev/reference/methods/conversations.approveSharedInvite
+    """
+    kwargs.update({"invite_id": invite_id, "target_team": target_team})
+    return self.api_call("conversations.approveSharedInvite", http_verb="POST", params=kwargs)
+
+

Approves an invitation to a Slack Connect channel. +https://docs.slack.dev/reference/methods/conversations.approveSharedInvite

+
+
+def conversations_archive(self, *, channel: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def conversations_archive(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> SlackResponse:
+    """Archives a conversation.
+    https://docs.slack.dev/reference/methods/conversations.archive
+    """
+    kwargs.update({"channel": channel})
+    return self.api_call("conversations.archive", params=kwargs)
+
+ +
+
+def conversations_canvases_create(self, *, channel_id: str, document_content: Dict[str, str], **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def conversations_canvases_create(
+    self,
+    *,
+    channel_id: str,
+    document_content: Dict[str, str],
+    **kwargs,
+) -> SlackResponse:
+    """Create a Channel Canvas for a channel
+    https://docs.slack.dev/reference/methods/conversations.canvases.create
+    """
+    kwargs.update({"channel_id": channel_id, "document_content": document_content})
+    return self.api_call("conversations.canvases.create", json=kwargs)
+
+ +
+
+def conversations_close(self, *, channel: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def conversations_close(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> SlackResponse:
+    """Closes a direct message or multi-person direct message.
+    https://docs.slack.dev/reference/methods/conversations.close
+    """
+    kwargs.update({"channel": channel})
+    return self.api_call("conversations.close", params=kwargs)
+
+

Closes a direct message or multi-person direct message. +https://docs.slack.dev/reference/methods/conversations.close

+
+
+def conversations_create(self,
*,
name: str,
is_private: bool | None = None,
team_id: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def conversations_create(
+    self,
+    *,
+    name: str,
+    is_private: Optional[bool] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Initiates a public or private channel-based conversation
+    https://docs.slack.dev/reference/methods/conversations.create
+    """
+    kwargs.update({"name": name, "is_private": is_private, "team_id": team_id})
+    return self.api_call("conversations.create", params=kwargs)
+
+

Initiates a public or private channel-based conversation +https://docs.slack.dev/reference/methods/conversations.create

+
+
+def conversations_declineSharedInvite(self, *, invite_id: str, target_team: str | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def conversations_declineSharedInvite(
+    self,
+    *,
+    invite_id: str,
+    target_team: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Declines a Slack Connect channel invite.
+    https://docs.slack.dev/reference/methods/conversations.declineSharedInvite
+    """
+    kwargs.update({"invite_id": invite_id, "target_team": target_team})
+    return self.api_call("conversations.declineSharedInvite", http_verb="GET", params=kwargs)
+
+ +
+
+def conversations_externalInvitePermissions_set(self, *, action: str, channel: str, target_team: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def conversations_externalInvitePermissions_set(
+    self, *, action: str, channel: str, target_team: str, **kwargs
+) -> SlackResponse:
+    """Sets a team in a shared External Limited channel to a shared Slack Connect channel or vice versa.
+    https://docs.slack.dev/reference/methods/conversations.externalInvitePermissions.set
+    """
+    kwargs.update(
+        {
+            "action": action,
+            "channel": channel,
+            "target_team": target_team,
+        }
+    )
+    return self.api_call("conversations.externalInvitePermissions.set", params=kwargs)
+
+

Sets a team in a shared External Limited channel to a shared Slack Connect channel or vice versa. +https://docs.slack.dev/reference/methods/conversations.externalInvitePermissions.set

+
+
+def conversations_history(self,
*,
channel: str,
cursor: str | None = None,
inclusive: bool | None = None,
include_all_metadata: bool | None = None,
latest: str | None = None,
limit: int | None = None,
oldest: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def conversations_history(
+    self,
+    *,
+    channel: str,
+    cursor: Optional[str] = None,
+    inclusive: Optional[bool] = None,
+    include_all_metadata: Optional[bool] = None,
+    latest: Optional[str] = None,
+    limit: Optional[int] = None,
+    oldest: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Fetches a conversation's history of messages and events.
+    https://docs.slack.dev/reference/methods/conversations.history
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "cursor": cursor,
+            "inclusive": inclusive,
+            "include_all_metadata": include_all_metadata,
+            "limit": limit,
+            "latest": latest,
+            "oldest": oldest,
+        }
+    )
+    return self.api_call("conversations.history", http_verb="GET", params=kwargs)
+
+

Fetches a conversation's history of messages and events. +https://docs.slack.dev/reference/methods/conversations.history

+
+
+def conversations_info(self,
*,
channel: str,
include_locale: bool | None = None,
include_num_members: bool | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def conversations_info(
+    self,
+    *,
+    channel: str,
+    include_locale: Optional[bool] = None,
+    include_num_members: Optional[bool] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Retrieve information about a conversation.
+    https://docs.slack.dev/reference/methods/conversations.info
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "include_locale": include_locale,
+            "include_num_members": include_num_members,
+        }
+    )
+    return self.api_call("conversations.info", http_verb="GET", params=kwargs)
+
+

Retrieve information about a conversation. +https://docs.slack.dev/reference/methods/conversations.info

+
+
+def conversations_invite(self,
*,
channel: str,
users: str | Sequence[str],
force: bool | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def conversations_invite(
+    self,
+    *,
+    channel: str,
+    users: Union[str, Sequence[str]],
+    force: Optional[bool] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Invites users to a channel.
+    https://docs.slack.dev/reference/methods/conversations.invite
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "force": force,
+        }
+    )
+    if isinstance(users, (list, tuple)):
+        kwargs.update({"users": ",".join(users)})
+    else:
+        kwargs.update({"users": users})
+    return self.api_call("conversations.invite", params=kwargs)
+
+ +
+
+def conversations_inviteShared(self,
*,
channel: str,
emails: str | Sequence[str] | None = None,
user_ids: str | Sequence[str] | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def conversations_inviteShared(
+    self,
+    *,
+    channel: str,
+    emails: Optional[Union[str, Sequence[str]]] = None,
+    user_ids: Optional[Union[str, Sequence[str]]] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Sends an invitation to a Slack Connect channel.
+    https://docs.slack.dev/reference/methods/conversations.inviteShared
+    """
+    if emails is None and user_ids is None:
+        raise e.SlackRequestError("Either emails or user ids must be provided.")
+    kwargs.update({"channel": channel})
+    if isinstance(emails, (list, tuple)):
+        kwargs.update({"emails": ",".join(emails)})
+    else:
+        kwargs.update({"emails": emails})
+    if isinstance(user_ids, (list, tuple)):
+        kwargs.update({"user_ids": ",".join(user_ids)})
+    else:
+        kwargs.update({"user_ids": user_ids})
+    return self.api_call("conversations.inviteShared", http_verb="GET", params=kwargs)
+
+

Sends an invitation to a Slack Connect channel. +https://docs.slack.dev/reference/methods/conversations.inviteShared

+
+
+def conversations_join(self, *, channel: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def conversations_join(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> SlackResponse:
+    """Joins an existing conversation.
+    https://docs.slack.dev/reference/methods/conversations.join
+    """
+    kwargs.update({"channel": channel})
+    return self.api_call("conversations.join", params=kwargs)
+
+ +
+
+def conversations_kick(self, *, channel: str, user: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def conversations_kick(
+    self,
+    *,
+    channel: str,
+    user: str,
+    **kwargs,
+) -> SlackResponse:
+    """Removes a user from a conversation.
+    https://docs.slack.dev/reference/methods/conversations.kick
+    """
+    kwargs.update({"channel": channel, "user": user})
+    return self.api_call("conversations.kick", params=kwargs)
+
+ +
+
+def conversations_leave(self, *, channel: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def conversations_leave(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> SlackResponse:
+    """Leaves a conversation.
+    https://docs.slack.dev/reference/methods/conversations.leave
+    """
+    kwargs.update({"channel": channel})
+    return self.api_call("conversations.leave", params=kwargs)
+
+ +
+
+def conversations_list(self,
*,
cursor: str | None = None,
exclude_archived: bool | None = None,
limit: int | None = None,
team_id: str | None = None,
types: str | Sequence[str] | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def conversations_list(
+    self,
+    *,
+    cursor: Optional[str] = None,
+    exclude_archived: Optional[bool] = None,
+    limit: Optional[int] = None,
+    team_id: Optional[str] = None,
+    types: Optional[Union[str, Sequence[str]]] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Lists all channels in a Slack team.
+    https://docs.slack.dev/reference/methods/conversations.list
+    """
+    kwargs.update(
+        {
+            "cursor": cursor,
+            "exclude_archived": exclude_archived,
+            "limit": limit,
+            "team_id": team_id,
+        }
+    )
+    if isinstance(types, (list, tuple)):
+        kwargs.update({"types": ",".join(types)})
+    else:
+        kwargs.update({"types": types})
+    return self.api_call("conversations.list", http_verb="GET", params=kwargs)
+
+ +
+
+def conversations_listConnectInvites(self,
*,
count: int | None = None,
cursor: str | None = None,
team_id: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def conversations_listConnectInvites(
+    self,
+    *,
+    count: Optional[int] = None,
+    cursor: Optional[str] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """List shared channel invites that have been generated
+    or received but have not yet been approved by all parties.
+    https://docs.slack.dev/reference/methods/conversations.listConnectInvites
+    """
+    kwargs.update({"count": count, "cursor": cursor, "team_id": team_id})
+    return self.api_call("conversations.listConnectInvites", params=kwargs)
+
+

List shared channel invites that have been generated +or received but have not yet been approved by all parties. +https://docs.slack.dev/reference/methods/conversations.listConnectInvites

+
+
+def conversations_mark(self, *, channel: str, ts: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def conversations_mark(
+    self,
+    *,
+    channel: str,
+    ts: str,
+    **kwargs,
+) -> SlackResponse:
+    """Sets the read cursor in a channel.
+    https://docs.slack.dev/reference/methods/conversations.mark
+    """
+    kwargs.update({"channel": channel, "ts": ts})
+    return self.api_call("conversations.mark", params=kwargs)
+
+ +
+
+def conversations_members(self, *, channel: str, cursor: str | None = None, limit: int | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def conversations_members(
+    self,
+    *,
+    channel: str,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Retrieve members of a conversation.
+    https://docs.slack.dev/reference/methods/conversations.members
+    """
+    kwargs.update({"channel": channel, "cursor": cursor, "limit": limit})
+    return self.api_call("conversations.members", http_verb="GET", params=kwargs)
+
+ +
+
+def conversations_open(self,
*,
channel: str | None = None,
return_im: bool | None = None,
users: str | Sequence[str] | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def conversations_open(
+    self,
+    *,
+    channel: Optional[str] = None,
+    return_im: Optional[bool] = None,
+    users: Optional[Union[str, Sequence[str]]] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Opens or resumes a direct message or multi-person direct message.
+    https://docs.slack.dev/reference/methods/conversations.open
+    """
+    if channel is None and users is None:
+        raise e.SlackRequestError("Either channel or users must be provided.")
+    kwargs.update({"channel": channel, "return_im": return_im})
+    if isinstance(users, (list, tuple)):
+        kwargs.update({"users": ",".join(users)})
+    else:
+        kwargs.update({"users": users})
+    return self.api_call("conversations.open", params=kwargs)
+
+

Opens or resumes a direct message or multi-person direct message. +https://docs.slack.dev/reference/methods/conversations.open

+
+
+def conversations_rename(self, *, channel: str, name: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def conversations_rename(
+    self,
+    *,
+    channel: str,
+    name: str,
+    **kwargs,
+) -> SlackResponse:
+    """Renames a conversation.
+    https://docs.slack.dev/reference/methods/conversations.rename
+    """
+    kwargs.update({"channel": channel, "name": name})
+    return self.api_call("conversations.rename", params=kwargs)
+
+ +
+
+def conversations_replies(self,
*,
channel: str,
ts: str,
cursor: str | None = None,
inclusive: bool | None = None,
include_all_metadata: bool | None = None,
latest: str | None = None,
limit: int | None = None,
oldest: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def conversations_replies(
+    self,
+    *,
+    channel: str,
+    ts: str,
+    cursor: Optional[str] = None,
+    inclusive: Optional[bool] = None,
+    include_all_metadata: Optional[bool] = None,
+    latest: Optional[str] = None,
+    limit: Optional[int] = None,
+    oldest: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Retrieve a thread of messages posted to a conversation
+    https://docs.slack.dev/reference/methods/conversations.replies
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "ts": ts,
+            "cursor": cursor,
+            "inclusive": inclusive,
+            "include_all_metadata": include_all_metadata,
+            "limit": limit,
+            "latest": latest,
+            "oldest": oldest,
+        }
+    )
+    return self.api_call("conversations.replies", http_verb="GET", params=kwargs)
+
+

Retrieve a thread of messages posted to a conversation +https://docs.slack.dev/reference/methods/conversations.replies

+
+
+def conversations_requestSharedInvite_approve(self,
*,
invite_id: str,
channel_id: str | None = None,
is_external_limited: str | None = None,
message: Dict[str, Any] | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def conversations_requestSharedInvite_approve(
+    self,
+    *,
+    invite_id: str,
+    channel_id: Optional[str] = None,
+    is_external_limited: Optional[str] = None,
+    message: Optional[Dict[str, Any]] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Approve a request to add an external user to a channel. This also sends them a Slack Connect invite.
+    https://docs.slack.dev/reference/methods/conversations.requestSharedInvite.approve
+    """
+    kwargs.update(
+        {
+            "invite_id": invite_id,
+            "channel_id": channel_id,
+            "is_external_limited": is_external_limited,
+        }
+    )
+    if message is not None:
+        kwargs.update({"message": json.dumps(message)})
+    return self.api_call("conversations.requestSharedInvite.approve", params=kwargs)
+
+

Approve a request to add an external user to a channel. This also sends them a Slack Connect invite. +https://docs.slack.dev/reference/methods/conversations.requestSharedInvite.approve

+
+
+def conversations_requestSharedInvite_deny(self, *, invite_id: str, message: str | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def conversations_requestSharedInvite_deny(
+    self,
+    *,
+    invite_id: str,
+    message: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Deny a request to invite an external user to a channel.
+    https://docs.slack.dev/reference/methods/conversations.requestSharedInvite.deny
+    """
+    kwargs.update({"invite_id": invite_id, "message": message})
+    return self.api_call("conversations.requestSharedInvite.deny", params=kwargs)
+
+

Deny a request to invite an external user to a channel. +https://docs.slack.dev/reference/methods/conversations.requestSharedInvite.deny

+
+
+def conversations_requestSharedInvite_list(self,
*,
cursor: str | None = None,
include_approved: bool | None = None,
include_denied: bool | None = None,
include_expired: bool | None = None,
invite_ids: str | Sequence[str] | None = None,
limit: int | None = None,
user_id: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def conversations_requestSharedInvite_list(
+    self,
+    *,
+    cursor: Optional[str] = None,
+    include_approved: Optional[bool] = None,
+    include_denied: Optional[bool] = None,
+    include_expired: Optional[bool] = None,
+    invite_ids: Optional[Union[str, Sequence[str]]] = None,
+    limit: Optional[int] = None,
+    user_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Lists requests to add external users to channels with ability to filter.
+    https://docs.slack.dev/reference/methods/conversations.requestSharedInvite.list
+    """
+    kwargs.update(
+        {
+            "cursor": cursor,
+            "include_approved": include_approved,
+            "include_denied": include_denied,
+            "include_expired": include_expired,
+            "limit": limit,
+            "user_id": user_id,
+        }
+    )
+    if invite_ids is not None:
+        if isinstance(invite_ids, (list, tuple)):
+            kwargs.update({"invite_ids": ",".join(invite_ids)})
+        else:
+            kwargs.update({"invite_ids": invite_ids})
+    return self.api_call("conversations.requestSharedInvite.list", params=kwargs)
+
+

Lists requests to add external users to channels with ability to filter. +https://docs.slack.dev/reference/methods/conversations.requestSharedInvite.list

+
+
+def conversations_setPurpose(self, *, channel: str, purpose: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def conversations_setPurpose(
+    self,
+    *,
+    channel: str,
+    purpose: str,
+    **kwargs,
+) -> SlackResponse:
+    """Sets the purpose for a conversation.
+    https://docs.slack.dev/reference/methods/conversations.setPurpose
+    """
+    kwargs.update({"channel": channel, "purpose": purpose})
+    return self.api_call("conversations.setPurpose", params=kwargs)
+
+ +
+
+def conversations_setTopic(self, *, channel: str, topic: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def conversations_setTopic(
+    self,
+    *,
+    channel: str,
+    topic: str,
+    **kwargs,
+) -> SlackResponse:
+    """Sets the topic for a conversation.
+    https://docs.slack.dev/reference/methods/conversations.setTopic
+    """
+    kwargs.update({"channel": channel, "topic": topic})
+    return self.api_call("conversations.setTopic", params=kwargs)
+
+ +
+
+def conversations_unarchive(self, *, channel: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def conversations_unarchive(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> SlackResponse:
+    """Reverses conversation archival.
+    https://docs.slack.dev/reference/methods/conversations.unarchive
+    """
+    kwargs.update({"channel": channel})
+    return self.api_call("conversations.unarchive", params=kwargs)
+
+ +
+
+def dialog_open(self, *, dialog: Dict[str, Any], trigger_id: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def dialog_open(
+    self,
+    *,
+    dialog: Dict[str, Any],
+    trigger_id: str,
+    **kwargs,
+) -> SlackResponse:
+    """Open a dialog with a user.
+    https://docs.slack.dev/reference/methods/dialog.open
+    """
+    kwargs.update({"dialog": dialog, "trigger_id": trigger_id})
+    kwargs = _remove_none_values(kwargs)
+    # NOTE: As the dialog can be a dict, this API call works only with json format.
+    return self.api_call("dialog.open", json=kwargs)
+
+ +
+
+def dnd_endDnd(self, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def dnd_endDnd(
+    self,
+    **kwargs,
+) -> SlackResponse:
+    """Ends the current user's Do Not Disturb session immediately.
+    https://docs.slack.dev/reference/methods/dnd.endDnd
+    """
+    return self.api_call("dnd.endDnd", params=kwargs)
+
+

Ends the current user's Do Not Disturb session immediately. +https://docs.slack.dev/reference/methods/dnd.endDnd

+
+
+def dnd_endSnooze(self, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def dnd_endSnooze(
+    self,
+    **kwargs,
+) -> SlackResponse:
+    """Ends the current user's snooze mode immediately.
+    https://docs.slack.dev/reference/methods/dnd.endSnooze
+    """
+    return self.api_call("dnd.endSnooze", params=kwargs)
+
+

Ends the current user's snooze mode immediately. +https://docs.slack.dev/reference/methods/dnd.endSnooze

+
+
+def dnd_info(self, *, team_id: str | None = None, user: str | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def dnd_info(
+    self,
+    *,
+    team_id: Optional[str] = None,
+    user: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Retrieves a user's current Do Not Disturb status.
+    https://docs.slack.dev/reference/methods/dnd.info
+    """
+    kwargs.update({"team_id": team_id, "user": user})
+    return self.api_call("dnd.info", http_verb="GET", params=kwargs)
+
+

Retrieves a user's current Do Not Disturb status. +https://docs.slack.dev/reference/methods/dnd.info

+
+
+def dnd_setSnooze(self, *, num_minutes: str | int, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def dnd_setSnooze(
+    self,
+    *,
+    num_minutes: Union[int, str],
+    **kwargs,
+) -> SlackResponse:
+    """Turns on Do Not Disturb mode for the current user, or changes its duration.
+    https://docs.slack.dev/reference/methods/dnd.setSnooze
+    """
+    kwargs.update({"num_minutes": num_minutes})
+    return self.api_call("dnd.setSnooze", http_verb="GET", params=kwargs)
+
+

Turns on Do Not Disturb mode for the current user, or changes its duration. +https://docs.slack.dev/reference/methods/dnd.setSnooze

+
+
+def dnd_teamInfo(self, users: str | Sequence[str], team_id: str | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def dnd_teamInfo(
+    self,
+    users: Union[str, Sequence[str]],
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Retrieves the Do Not Disturb status for users on a team.
+    https://docs.slack.dev/reference/methods/dnd.teamInfo
+    """
+    if isinstance(users, (list, tuple)):
+        kwargs.update({"users": ",".join(users)})
+    else:
+        kwargs.update({"users": users})
+    kwargs.update({"team_id": team_id})
+    return self.api_call("dnd.teamInfo", http_verb="GET", params=kwargs)
+
+

Retrieves the Do Not Disturb status for users on a team. +https://docs.slack.dev/reference/methods/dnd.teamInfo

+
+
+def emoji_list(self, include_categories: bool | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def emoji_list(
+    self,
+    include_categories: Optional[bool] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Lists custom emoji for a team.
+    https://docs.slack.dev/reference/methods/emoji.list
+    """
+    kwargs.update({"include_categories": include_categories})
+    return self.api_call("emoji.list", http_verb="GET", params=kwargs)
+
+ +
+
+def entity_presentDetails(self,
trigger_id: str,
metadata: Dict | EntityMetadata | None = None,
user_auth_required: bool | None = None,
user_auth_url: str | None = None,
error: Dict[str, Any] | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def entity_presentDetails(
+    self,
+    trigger_id: str,
+    metadata: Optional[Union[Dict, EntityMetadata]] = None,
+    user_auth_required: Optional[bool] = None,
+    user_auth_url: Optional[str] = None,
+    error: Optional[Dict[str, Any]] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Provides entity details for the flexpane.
+    https://docs.slack.dev/reference/methods/entity.presentDetails/
+    """
+    kwargs.update({"trigger_id": trigger_id})
+    if metadata is not None:
+        kwargs.update({"metadata": metadata})
+    if user_auth_required is not None:
+        kwargs.update({"user_auth_required": user_auth_required})
+    if user_auth_url is not None:
+        kwargs.update({"user_auth_url": user_auth_url})
+    if error is not None:
+        kwargs.update({"error": error})
+    _parse_web_class_objects(kwargs)
+    return self.api_call("entity.presentDetails", json=kwargs)
+
+

Provides entity details for the flexpane. +https://docs.slack.dev/reference/methods/entity.presentDetails/

+
+
+def files_comments_delete(self, *, file: str, id: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def files_comments_delete(
+    self,
+    *,
+    file: str,
+    id: str,
+    **kwargs,
+) -> SlackResponse:
+    """Deletes an existing comment on a file.
+    https://docs.slack.dev/reference/methods/files.comments.delete
+    """
+    kwargs.update({"file": file, "id": id})
+    return self.api_call("files.comments.delete", params=kwargs)
+
+ +
+
+def files_completeUploadExternal(self,
*,
files: List[Dict[str, str]],
channel_id: str | None = None,
channels: List[str] | None = None,
initial_comment: str | None = None,
thread_ts: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def files_completeUploadExternal(
+    self,
+    *,
+    files: List[Dict[str, str]],
+    channel_id: Optional[str] = None,
+    channels: Optional[List[str]] = None,
+    initial_comment: Optional[str] = None,
+    thread_ts: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Finishes an upload started with files.getUploadURLExternal.
+    https://docs.slack.dev/reference/methods/files.completeUploadExternal
+    """
+    _files = [{k: v for k, v in f.items() if v is not None} for f in files]
+    kwargs.update(
+        {
+            "files": json.dumps(_files),
+            "channel_id": channel_id,
+            "initial_comment": initial_comment,
+            "thread_ts": thread_ts,
+        }
+    )
+    if channels:
+        kwargs["channels"] = ",".join(channels)
+    return self.api_call("files.completeUploadExternal", params=kwargs)
+
+

Finishes an upload started with files.getUploadURLExternal. +https://docs.slack.dev/reference/methods/files.completeUploadExternal

+
+
+def files_delete(self, *, file: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def files_delete(
+    self,
+    *,
+    file: str,
+    **kwargs,
+) -> SlackResponse:
+    """Deletes a file.
+    https://docs.slack.dev/reference/methods/files.delete
+    """
+    kwargs.update({"file": file})
+    return self.api_call("files.delete", params=kwargs)
+
+ +
+
+def files_getUploadURLExternal(self,
*,
filename: str,
length: int,
alt_txt: str | None = None,
snippet_type: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def files_getUploadURLExternal(
+    self,
+    *,
+    filename: str,
+    length: int,
+    alt_txt: Optional[str] = None,
+    snippet_type: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Gets a URL for an edge external upload.
+    https://docs.slack.dev/reference/methods/files.getUploadURLExternal
+    """
+    kwargs.update(
+        {
+            "filename": filename,
+            "length": length,
+            "alt_txt": alt_txt,
+            "snippet_type": snippet_type,
+        }
+    )
+    return self.api_call("files.getUploadURLExternal", params=kwargs)
+
+ +
+
+def files_info(self,
*,
file: str,
count: int | None = None,
cursor: str | None = None,
limit: int | None = None,
page: int | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def files_info(
+    self,
+    *,
+    file: str,
+    count: Optional[int] = None,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    page: Optional[int] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Gets information about a team file.
+    https://docs.slack.dev/reference/methods/files.info
+    """
+    kwargs.update(
+        {
+            "file": file,
+            "count": count,
+            "cursor": cursor,
+            "limit": limit,
+            "page": page,
+        }
+    )
+    return self.api_call("files.info", http_verb="GET", params=kwargs)
+
+

Gets information about a team file. +https://docs.slack.dev/reference/methods/files.info

+
+
+def files_list(self,
*,
channel: str | None = None,
count: int | None = None,
page: int | None = None,
show_files_hidden_by_limit: bool | None = None,
team_id: str | None = None,
ts_from: str | None = None,
ts_to: str | None = None,
types: str | Sequence[str] | None = None,
user: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def files_list(
+    self,
+    *,
+    channel: Optional[str] = None,
+    count: Optional[int] = None,
+    page: Optional[int] = None,
+    show_files_hidden_by_limit: Optional[bool] = None,
+    team_id: Optional[str] = None,
+    ts_from: Optional[str] = None,
+    ts_to: Optional[str] = None,
+    types: Optional[Union[str, Sequence[str]]] = None,
+    user: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Lists & filters team files.
+    https://docs.slack.dev/reference/methods/files.list
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "count": count,
+            "page": page,
+            "show_files_hidden_by_limit": show_files_hidden_by_limit,
+            "team_id": team_id,
+            "ts_from": ts_from,
+            "ts_to": ts_to,
+            "user": user,
+        }
+    )
+    if isinstance(types, (list, tuple)):
+        kwargs.update({"types": ",".join(types)})
+    else:
+        kwargs.update({"types": types})
+    return self.api_call("files.list", http_verb="GET", params=kwargs)
+
+ +
+
+def files_remote_add(self,
*,
external_id: str,
external_url: str,
title: str,
filetype: str | None = None,
indexable_file_contents: str | bytes | io.IOBase | None = None,
preview_image: str | bytes | io.IOBase | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def files_remote_add(
+    self,
+    *,
+    external_id: str,
+    external_url: str,
+    title: str,
+    filetype: Optional[str] = None,
+    indexable_file_contents: Optional[Union[str, bytes, IOBase]] = None,
+    preview_image: Optional[Union[str, bytes, IOBase]] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Adds a file from a remote service.
+    https://docs.slack.dev/reference/methods/files.remote.add
+    """
+    kwargs.update(
+        {
+            "external_id": external_id,
+            "external_url": external_url,
+            "title": title,
+            "filetype": filetype,
+        }
+    )
+    files = None
+    # preview_image (file): Preview of the document via multipart/form-data.
+    if preview_image is not None or indexable_file_contents is not None:
+        files = {
+            "preview_image": preview_image,
+            "indexable_file_contents": indexable_file_contents,
+        }
+
+    return self.api_call(
+        # Intentionally using "POST" method over "GET" here
+        "files.remote.add",
+        http_verb="POST",
+        data=kwargs,
+        files=files,
+    )
+
+ +
+
+def files_remote_info(self, *, external_id: str | None = None, file: str | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def files_remote_info(
+    self,
+    *,
+    external_id: Optional[str] = None,
+    file: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Retrieve information about a remote file added to Slack.
+    https://docs.slack.dev/reference/methods/files.remote.info
+    """
+    kwargs.update({"external_id": external_id, "file": file})
+    return self.api_call("files.remote.info", http_verb="GET", params=kwargs)
+
+

Retrieve information about a remote file added to Slack. +https://docs.slack.dev/reference/methods/files.remote.info

+
+
+def files_remote_list(self,
*,
channel: str | None = None,
cursor: str | None = None,
limit: int | None = None,
ts_from: str | None = None,
ts_to: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def files_remote_list(
+    self,
+    *,
+    channel: Optional[str] = None,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    ts_from: Optional[str] = None,
+    ts_to: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Retrieve information about a remote file added to Slack.
+    https://docs.slack.dev/reference/methods/files.remote.list
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "cursor": cursor,
+            "limit": limit,
+            "ts_from": ts_from,
+            "ts_to": ts_to,
+        }
+    )
+    return self.api_call("files.remote.list", http_verb="GET", params=kwargs)
+
+

Retrieve information about a remote file added to Slack. +https://docs.slack.dev/reference/methods/files.remote.list

+
+
+def files_remote_remove(self, *, external_id: str | None = None, file: str | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def files_remote_remove(
+    self,
+    *,
+    external_id: Optional[str] = None,
+    file: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Remove a remote file.
+    https://docs.slack.dev/reference/methods/files.remote.remove
+    """
+    kwargs.update({"external_id": external_id, "file": file})
+    return self.api_call("files.remote.remove", http_verb="POST", params=kwargs)
+
+ +
+
+def files_remote_share(self,
*,
channels: str | Sequence[str],
external_id: str | None = None,
file: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def files_remote_share(
+    self,
+    *,
+    channels: Union[str, Sequence[str]],
+    external_id: Optional[str] = None,
+    file: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Share a remote file into a channel.
+    https://docs.slack.dev/reference/methods/files.remote.share
+    """
+    if external_id is None and file is None:
+        raise e.SlackRequestError("Either external_id or file must be provided.")
+    if isinstance(channels, (list, tuple)):
+        kwargs.update({"channels": ",".join(channels)})
+    else:
+        kwargs.update({"channels": channels})
+    kwargs.update({"external_id": external_id, "file": file})
+    return self.api_call("files.remote.share", http_verb="GET", params=kwargs)
+
+ +
+
+def files_remote_update(self,
*,
external_id: str | None = None,
external_url: str | None = None,
file: str | None = None,
title: str | None = None,
filetype: str | None = None,
indexable_file_contents: str | None = None,
preview_image: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def files_remote_update(
+    self,
+    *,
+    external_id: Optional[str] = None,
+    external_url: Optional[str] = None,
+    file: Optional[str] = None,
+    title: Optional[str] = None,
+    filetype: Optional[str] = None,
+    indexable_file_contents: Optional[str] = None,
+    preview_image: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Updates an existing remote file.
+    https://docs.slack.dev/reference/methods/files.remote.update
+    """
+    kwargs.update(
+        {
+            "external_id": external_id,
+            "external_url": external_url,
+            "file": file,
+            "title": title,
+            "filetype": filetype,
+        }
+    )
+    files = None
+    # preview_image (file): Preview of the document via multipart/form-data.
+    if preview_image is not None or indexable_file_contents is not None:
+        files = {
+            "preview_image": preview_image,
+            "indexable_file_contents": indexable_file_contents,
+        }
+
+    return self.api_call(
+        # Intentionally using "POST" method over "GET" here
+        "files.remote.update",
+        http_verb="POST",
+        data=kwargs,
+        files=files,
+    )
+
+ +
+
+def files_revokePublicURL(self, *, file: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def files_revokePublicURL(
+    self,
+    *,
+    file: str,
+    **kwargs,
+) -> SlackResponse:
+    """Revokes public/external sharing access for a file
+    https://docs.slack.dev/reference/methods/files.revokePublicURL
+    """
+    kwargs.update({"file": file})
+    return self.api_call("files.revokePublicURL", params=kwargs)
+
+

Revokes public/external sharing access for a file +https://docs.slack.dev/reference/methods/files.revokePublicURL

+
+
+def files_sharedPublicURL(self, *, file: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def files_sharedPublicURL(
+    self,
+    *,
+    file: str,
+    **kwargs,
+) -> SlackResponse:
+    """Enables a file for public/external sharing.
+    https://docs.slack.dev/reference/methods/files.sharedPublicURL
+    """
+    kwargs.update({"file": file})
+    return self.api_call("files.sharedPublicURL", params=kwargs)
+
+

Enables a file for public/external sharing. +https://docs.slack.dev/reference/methods/files.sharedPublicURL

+
+
+def files_upload(self,
*,
file: str | bytes | io.IOBase | None = None,
content: str | bytes | None = None,
filename: str | None = None,
filetype: str | None = None,
initial_comment: str | None = None,
thread_ts: str | None = None,
title: str | None = None,
channels: str | Sequence[str] | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def files_upload(
+    self,
+    *,
+    file: Optional[Union[str, bytes, IOBase]] = None,
+    content: Optional[Union[str, bytes]] = None,
+    filename: Optional[str] = None,
+    filetype: Optional[str] = None,
+    initial_comment: Optional[str] = None,
+    thread_ts: Optional[str] = None,
+    title: Optional[str] = None,
+    channels: Optional[Union[str, Sequence[str]]] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Uploads or creates a file.
+    https://docs.slack.dev/reference/methods/files.upload
+    """
+    _print_files_upload_v2_suggestion()
+
+    if file is None and content is None:
+        raise e.SlackRequestError("The file or content argument must be specified.")
+    if file is not None and content is not None:
+        raise e.SlackRequestError("You cannot specify both the file and the content argument.")
+
+    if isinstance(channels, (list, tuple)):
+        kwargs.update({"channels": ",".join(channels)})
+    else:
+        kwargs.update({"channels": channels})
+    kwargs.update(
+        {
+            "filename": filename,
+            "filetype": filetype,
+            "initial_comment": initial_comment,
+            "thread_ts": thread_ts,
+            "title": title,
+        }
+    )
+    if file:
+        if kwargs.get("filename") is None and isinstance(file, str):
+            # use the local filename if filename is missing
+            if kwargs.get("filename") is None:
+                kwargs["filename"] = file.split(os.path.sep)[-1]
+        return self.api_call("files.upload", files={"file": file}, data=kwargs)
+    else:
+        kwargs["content"] = content
+        return self.api_call("files.upload", data=kwargs)
+
+ +
+
+def files_upload_v2(self,
*,
filename: str | None = None,
file: str | bytes | io.IOBase | os.PathLike | None = None,
content: str | bytes | None = None,
title: str | None = None,
alt_txt: str | None = None,
snippet_type: str | None = None,
file_uploads: List[Dict[str, Any]] | None = None,
channel: str | None = None,
channels: List[str] | None = None,
initial_comment: str | None = None,
thread_ts: str | None = None,
request_file_info: bool = True,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def files_upload_v2(
+    self,
+    *,
+    # for sending a single file
+    filename: Optional[str] = None,  # you can skip this only when sending along with content parameter
+    file: Optional[Union[str, bytes, IOBase, os.PathLike]] = None,
+    content: Optional[Union[str, bytes]] = None,
+    title: Optional[str] = None,
+    alt_txt: Optional[str] = None,
+    snippet_type: Optional[str] = None,
+    # To upload multiple files at a time
+    file_uploads: Optional[List[Dict[str, Any]]] = None,
+    channel: Optional[str] = None,
+    channels: Optional[List[str]] = None,
+    initial_comment: Optional[str] = None,
+    thread_ts: Optional[str] = None,
+    request_file_info: bool = True,  # since v3.23, this flag is no longer necessary
+    **kwargs,
+) -> SlackResponse:
+    """This wrapper method provides an easy way to upload files using the following endpoints:
+
+    - step1: https://docs.slack.dev/reference/methods/files.getUploadURLExternal
+
+    - step2: "https://files.slack.com/upload/v1/..." URLs returned from files.getUploadURLExternal API
+
+    - step3: https://docs.slack.dev/reference/methods/files.completeUploadExternal
+        and https://docs.slack.dev/reference/methods/files.info
+
+    """
+    if file is None and content is None and file_uploads is None:
+        raise e.SlackRequestError("Any of file, content, and file_uploads must be specified.")
+    if file is not None and content is not None:
+        raise e.SlackRequestError("You cannot specify both the file and the content argument.")
+
+    # deprecated arguments:
+    filetype = kwargs.get("filetype")
+
+    if filetype is not None:
+        warnings.warn("The filetype parameter is no longer supported. Please remove it from the arguments.")
+
+    # step1: files.getUploadURLExternal per file
+    files: List[Dict[str, Any]] = []
+    if file_uploads is not None:
+        for f in file_uploads:
+            files.append(_to_v2_file_upload_item(f))
+    else:
+        f = _to_v2_file_upload_item(
+            {
+                "filename": filename,
+                "file": file,
+                "content": content,
+                "title": title,
+                "alt_txt": alt_txt,
+                "snippet_type": snippet_type,
+            }
+        )
+        files.append(f)
+
+    for f in files:
+        url_response = self.files_getUploadURLExternal(
+            filename=f.get("filename"),  # type: ignore[arg-type]
+            length=f.get("length"),  # type: ignore[arg-type]
+            alt_txt=f.get("alt_txt"),
+            snippet_type=f.get("snippet_type"),
+            token=kwargs.get("token"),
+        )
+        _validate_for_legacy_client(url_response)
+        f["file_id"] = url_response.get("file_id")  # type: ignore[union-attr, unused-ignore]
+        f["upload_url"] = url_response.get("upload_url")  # type: ignore[union-attr, unused-ignore]
+
+    # step2: "https://files.slack.com/upload/v1/..." per file
+    for f in files:
+        upload_result = self._upload_file(
+            url=f["upload_url"],
+            data=f["data"],
+            logger=self._logger,
+            timeout=self.timeout,
+            proxy=self.proxy,
+            ssl=self.ssl,
+        )
+        if upload_result.status != 200:
+            status = upload_result.status
+            body = upload_result.body
+            message = (
+                "Failed to upload a file "
+                f"(status: {status}, body: {body}, filename: {f.get('filename')}, title: {f.get('title')})"
+            )
+            raise e.SlackRequestError(message)
+
+    # step3: files.completeUploadExternal with all the sets of (file_id + title)
+    completion = self.files_completeUploadExternal(
+        files=[{"id": f["file_id"], "title": f["title"]} for f in files],
+        channel_id=channel,
+        channels=channels,
+        initial_comment=initial_comment,
+        thread_ts=thread_ts,
+        **kwargs,
+    )
+    if len(completion.get("files")) == 1:  # type: ignore[arg-type, union-attr, unused-ignore]
+        completion.data["file"] = completion.get("files")[0]  # type: ignore[index, union-attr, unused-ignore]
+    return completion
+
+

This wrapper method provides an easy way to upload files using the following endpoints:

+
+
+
+def functions_completeError(self, *, function_execution_id: str, error: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def functions_completeError(
+    self,
+    *,
+    function_execution_id: str,
+    error: str,
+    **kwargs,
+) -> SlackResponse:
+    """Signal the failure to execute a function
+    https://docs.slack.dev/reference/methods/functions.completeError
+    """
+    kwargs.update({"function_execution_id": function_execution_id, "error": error})
+    return self.api_call("functions.completeError", params=kwargs)
+
+ +
+
+def functions_completeSuccess(self, *, function_execution_id: str, outputs: Dict[str, Any], **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def functions_completeSuccess(
+    self,
+    *,
+    function_execution_id: str,
+    outputs: Dict[str, Any],
+    **kwargs,
+) -> SlackResponse:
+    """Signal the successful completion of a function
+    https://docs.slack.dev/reference/methods/functions.completeSuccess
+    """
+    kwargs.update({"function_execution_id": function_execution_id, "outputs": json.dumps(outputs)})
+    return self.api_call("functions.completeSuccess", params=kwargs)
+
+

Signal the successful completion of a function +https://docs.slack.dev/reference/methods/functions.completeSuccess

+
+
+def groups_archive(self, *, channel: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def groups_archive(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> SlackResponse:
+    """Archives a private channel."""
+    kwargs.update({"channel": channel})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("groups.archive", json=kwargs)
+
+

Archives a private channel.

+
+
+def groups_create(self, *, name: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def groups_create(
+    self,
+    *,
+    name: str,
+    **kwargs,
+) -> SlackResponse:
+    """Creates a private channel."""
+    kwargs.update({"name": name})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("groups.create", json=kwargs)
+
+

Creates a private channel.

+
+
+def groups_createChild(self, *, channel: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def groups_createChild(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> SlackResponse:
+    """Clones and archives a private channel."""
+    kwargs.update({"channel": channel})
+    return self.api_call("groups.createChild", http_verb="GET", params=kwargs)
+
+

Clones and archives a private channel.

+
+
+def groups_history(self, *, channel: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def groups_history(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> SlackResponse:
+    """Fetches history of messages and events from a private channel."""
+    kwargs.update({"channel": channel})
+    return self.api_call("groups.history", http_verb="GET", params=kwargs)
+
+

Fetches history of messages and events from a private channel.

+
+
+def groups_info(self, *, channel: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def groups_info(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> SlackResponse:
+    """Gets information about a private channel."""
+    kwargs.update({"channel": channel})
+    return self.api_call("groups.info", http_verb="GET", params=kwargs)
+
+

Gets information about a private channel.

+
+
+def groups_invite(self, *, channel: str, user: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def groups_invite(
+    self,
+    *,
+    channel: str,
+    user: str,
+    **kwargs,
+) -> SlackResponse:
+    """Invites a user to a private channel."""
+    kwargs.update({"channel": channel, "user": user})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("groups.invite", json=kwargs)
+
+

Invites a user to a private channel.

+
+
+def groups_kick(self, *, channel: str, user: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def groups_kick(
+    self,
+    *,
+    channel: str,
+    user: str,
+    **kwargs,
+) -> SlackResponse:
+    """Removes a user from a private channel."""
+    kwargs.update({"channel": channel, "user": user})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("groups.kick", json=kwargs)
+
+

Removes a user from a private channel.

+
+
+def groups_leave(self, *, channel: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def groups_leave(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> SlackResponse:
+    """Leaves a private channel."""
+    kwargs.update({"channel": channel})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("groups.leave", json=kwargs)
+
+

Leaves a private channel.

+
+
+def groups_list(self, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def groups_list(
+    self,
+    **kwargs,
+) -> SlackResponse:
+    """Lists private channels that the calling user has access to."""
+    return self.api_call("groups.list", http_verb="GET", params=kwargs)
+
+

Lists private channels that the calling user has access to.

+
+
+def groups_mark(self, *, channel: str, ts: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def groups_mark(
+    self,
+    *,
+    channel: str,
+    ts: str,
+    **kwargs,
+) -> SlackResponse:
+    """Sets the read cursor in a private channel."""
+    kwargs.update({"channel": channel, "ts": ts})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("groups.mark", json=kwargs)
+
+

Sets the read cursor in a private channel.

+
+
+def groups_open(self, *, channel: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def groups_open(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> SlackResponse:
+    """Opens a private channel."""
+    kwargs.update({"channel": channel})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("groups.open", json=kwargs)
+
+

Opens a private channel.

+
+
+def groups_rename(self, *, channel: str, name: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def groups_rename(
+    self,
+    *,
+    channel: str,
+    name: str,
+    **kwargs,
+) -> SlackResponse:
+    """Renames a private channel."""
+    kwargs.update({"channel": channel, "name": name})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("groups.rename", json=kwargs)
+
+

Renames a private channel.

+
+
+def groups_replies(self, *, channel: str, thread_ts: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def groups_replies(
+    self,
+    *,
+    channel: str,
+    thread_ts: str,
+    **kwargs,
+) -> SlackResponse:
+    """Retrieve a thread of messages posted to a private channel"""
+    kwargs.update({"channel": channel, "thread_ts": thread_ts})
+    return self.api_call("groups.replies", http_verb="GET", params=kwargs)
+
+

Retrieve a thread of messages posted to a private channel

+
+
+def groups_setPurpose(self, *, channel: str, purpose: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def groups_setPurpose(
+    self,
+    *,
+    channel: str,
+    purpose: str,
+    **kwargs,
+) -> SlackResponse:
+    """Sets the purpose for a private channel."""
+    kwargs.update({"channel": channel, "purpose": purpose})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("groups.setPurpose", json=kwargs)
+
+

Sets the purpose for a private channel.

+
+
+def groups_setTopic(self, *, channel: str, topic: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def groups_setTopic(
+    self,
+    *,
+    channel: str,
+    topic: str,
+    **kwargs,
+) -> SlackResponse:
+    """Sets the topic for a private channel."""
+    kwargs.update({"channel": channel, "topic": topic})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("groups.setTopic", json=kwargs)
+
+

Sets the topic for a private channel.

+
+
+def groups_unarchive(self, *, channel: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def groups_unarchive(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> SlackResponse:
+    """Unarchives a private channel."""
+    kwargs.update({"channel": channel})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("groups.unarchive", json=kwargs)
+
+

Unarchives a private channel.

+
+
+def im_close(self, *, channel: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def im_close(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> SlackResponse:
+    """Close a direct message channel."""
+    kwargs.update({"channel": channel})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("im.close", json=kwargs)
+
+

Close a direct message channel.

+
+
+def im_history(self, *, channel: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def im_history(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> SlackResponse:
+    """Fetches history of messages and events from direct message channel."""
+    kwargs.update({"channel": channel})
+    return self.api_call("im.history", http_verb="GET", params=kwargs)
+
+

Fetches history of messages and events from direct message channel.

+
+
+def im_list(self, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def im_list(
+    self,
+    **kwargs,
+) -> SlackResponse:
+    """Lists direct message channels for the calling user."""
+    return self.api_call("im.list", http_verb="GET", params=kwargs)
+
+

Lists direct message channels for the calling user.

+
+
+def im_mark(self, *, channel: str, ts: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def im_mark(
+    self,
+    *,
+    channel: str,
+    ts: str,
+    **kwargs,
+) -> SlackResponse:
+    """Sets the read cursor in a direct message channel."""
+    kwargs.update({"channel": channel, "ts": ts})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("im.mark", json=kwargs)
+
+

Sets the read cursor in a direct message channel.

+
+
+def im_open(self, *, user: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def im_open(
+    self,
+    *,
+    user: str,
+    **kwargs,
+) -> SlackResponse:
+    """Opens a direct message channel."""
+    kwargs.update({"user": user})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("im.open", json=kwargs)
+
+

Opens a direct message channel.

+
+
+def im_replies(self, *, channel: str, thread_ts: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def im_replies(
+    self,
+    *,
+    channel: str,
+    thread_ts: str,
+    **kwargs,
+) -> SlackResponse:
+    """Retrieve a thread of messages posted to a direct message conversation"""
+    kwargs.update({"channel": channel, "thread_ts": thread_ts})
+    return self.api_call("im.replies", http_verb="GET", params=kwargs)
+
+

Retrieve a thread of messages posted to a direct message conversation

+
+
+def migration_exchange(self,
*,
users: str | Sequence[str],
team_id: str | None = None,
to_old: bool | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def migration_exchange(
+    self,
+    *,
+    users: Union[str, Sequence[str]],
+    team_id: Optional[str] = None,
+    to_old: Optional[bool] = None,
+    **kwargs,
+) -> SlackResponse:
+    """For Enterprise Grid workspaces, map local user IDs to global user IDs
+    https://docs.slack.dev/reference/methods/migration.exchange
+    """
+    if isinstance(users, (list, tuple)):
+        kwargs.update({"users": ",".join(users)})
+    else:
+        kwargs.update({"users": users})
+    kwargs.update({"team_id": team_id, "to_old": to_old})
+    return self.api_call("migration.exchange", http_verb="GET", params=kwargs)
+
+

For Enterprise Grid workspaces, map local user IDs to global user IDs +https://docs.slack.dev/reference/methods/migration.exchange

+
+
+def mpim_close(self, *, channel: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def mpim_close(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> SlackResponse:
+    """Closes a multiparty direct message channel."""
+    kwargs.update({"channel": channel})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("mpim.close", json=kwargs)
+
+

Closes a multiparty direct message channel.

+
+
+def mpim_history(self, *, channel: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def mpim_history(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> SlackResponse:
+    """Fetches history of messages and events from a multiparty direct message."""
+    kwargs.update({"channel": channel})
+    return self.api_call("mpim.history", http_verb="GET", params=kwargs)
+
+

Fetches history of messages and events from a multiparty direct message.

+
+
+def mpim_list(self, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def mpim_list(
+    self,
+    **kwargs,
+) -> SlackResponse:
+    """Lists multiparty direct message channels for the calling user."""
+    return self.api_call("mpim.list", http_verb="GET", params=kwargs)
+
+

Lists multiparty direct message channels for the calling user.

+
+
+def mpim_mark(self, *, channel: str, ts: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def mpim_mark(
+    self,
+    *,
+    channel: str,
+    ts: str,
+    **kwargs,
+) -> SlackResponse:
+    """Sets the read cursor in a multiparty direct message channel."""
+    kwargs.update({"channel": channel, "ts": ts})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("mpim.mark", json=kwargs)
+
+

Sets the read cursor in a multiparty direct message channel.

+
+
+def mpim_open(self, *, users: str | Sequence[str], **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def mpim_open(
+    self,
+    *,
+    users: Union[str, Sequence[str]],
+    **kwargs,
+) -> SlackResponse:
+    """This method opens a multiparty direct message."""
+    if isinstance(users, (list, tuple)):
+        kwargs.update({"users": ",".join(users)})
+    else:
+        kwargs.update({"users": users})
+    return self.api_call("mpim.open", params=kwargs)
+
+

This method opens a multiparty direct message.

+
+
+def mpim_replies(self, *, channel: str, thread_ts: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def mpim_replies(
+    self,
+    *,
+    channel: str,
+    thread_ts: str,
+    **kwargs,
+) -> SlackResponse:
+    """Retrieve a thread of messages posted to a direct message conversation from a
+    multiparty direct message.
+    """
+    kwargs.update({"channel": channel, "thread_ts": thread_ts})
+    return self.api_call("mpim.replies", http_verb="GET", params=kwargs)
+
+

Retrieve a thread of messages posted to a direct message conversation from a +multiparty direct message.

+
+
+def oauth_access(self,
*,
client_id: str,
client_secret: str,
code: str,
redirect_uri: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def oauth_access(
+    self,
+    *,
+    client_id: str,
+    client_secret: str,
+    code: str,
+    redirect_uri: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Exchanges a temporary OAuth verifier code for an access token.
+    https://docs.slack.dev/reference/methods/oauth.access
+    """
+    if redirect_uri is not None:
+        kwargs.update({"redirect_uri": redirect_uri})
+    kwargs.update({"code": code})
+    return self.api_call(
+        "oauth.access",
+        data=kwargs,
+        auth={"client_id": client_id, "client_secret": client_secret},
+    )
+
+

Exchanges a temporary OAuth verifier code for an access token. +https://docs.slack.dev/reference/methods/oauth.access

+
+
+def oauth_v2_access(self,
*,
client_id: str,
client_secret: str,
code: str | None = None,
redirect_uri: str | None = None,
grant_type: str | None = None,
refresh_token: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def oauth_v2_access(
+    self,
+    *,
+    client_id: str,
+    client_secret: str,
+    # This field is required when processing the OAuth redirect URL requests
+    # while it's absent for token rotation
+    code: Optional[str] = None,
+    redirect_uri: Optional[str] = None,
+    # This field is required for token rotation
+    grant_type: Optional[str] = None,
+    # This field is required for token rotation
+    refresh_token: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Exchanges a temporary OAuth verifier code for an access token.
+    https://docs.slack.dev/reference/methods/oauth.v2.access
+    """
+    if redirect_uri is not None:
+        kwargs.update({"redirect_uri": redirect_uri})
+    if code is not None:
+        kwargs.update({"code": code})
+    if grant_type is not None:
+        kwargs.update({"grant_type": grant_type})
+    if refresh_token is not None:
+        kwargs.update({"refresh_token": refresh_token})
+    return self.api_call(
+        "oauth.v2.access",
+        data=kwargs,
+        auth={"client_id": client_id, "client_secret": client_secret},
+    )
+
+

Exchanges a temporary OAuth verifier code for an access token. +https://docs.slack.dev/reference/methods/oauth.v2.access

+
+
+def oauth_v2_exchange(self, *, token: str, client_id: str, client_secret: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def oauth_v2_exchange(
+    self,
+    *,
+    token: str,
+    client_id: str,
+    client_secret: str,
+    **kwargs,
+) -> SlackResponse:
+    """Exchanges a legacy access token for a new expiring access token and refresh token
+    https://docs.slack.dev/reference/methods/oauth.v2.exchange
+    """
+    kwargs.update({"client_id": client_id, "client_secret": client_secret, "token": token})
+    return self.api_call("oauth.v2.exchange", params=kwargs)
+
+

Exchanges a legacy access token for a new expiring access token and refresh token +https://docs.slack.dev/reference/methods/oauth.v2.exchange

+
+
+def openid_connect_token(self,
client_id: str,
client_secret: str,
code: str | None = None,
redirect_uri: str | None = None,
grant_type: str | None = None,
refresh_token: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def openid_connect_token(
+    self,
+    client_id: str,
+    client_secret: str,
+    code: Optional[str] = None,
+    redirect_uri: Optional[str] = None,
+    grant_type: Optional[str] = None,
+    refresh_token: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Exchanges a temporary OAuth verifier code for an access token for Sign in with Slack.
+    https://docs.slack.dev/reference/methods/openid.connect.token
+    """
+    if redirect_uri is not None:
+        kwargs.update({"redirect_uri": redirect_uri})
+    if code is not None:
+        kwargs.update({"code": code})
+    if grant_type is not None:
+        kwargs.update({"grant_type": grant_type})
+    if refresh_token is not None:
+        kwargs.update({"refresh_token": refresh_token})
+    return self.api_call(
+        "openid.connect.token",
+        data=kwargs,
+        auth={"client_id": client_id, "client_secret": client_secret},
+    )
+
+

Exchanges a temporary OAuth verifier code for an access token for Sign in with Slack. +https://docs.slack.dev/reference/methods/openid.connect.token

+
+
+def openid_connect_userInfo(self, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def openid_connect_userInfo(
+    self,
+    **kwargs,
+) -> SlackResponse:
+    """Get the identity of a user who has authorized Sign in with Slack.
+    https://docs.slack.dev/reference/methods/openid.connect.userInfo
+    """
+    return self.api_call("openid.connect.userInfo", params=kwargs)
+
+

Get the identity of a user who has authorized Sign in with Slack. +https://docs.slack.dev/reference/methods/openid.connect.userInfo

+
+
+def pins_add(self, *, channel: str, timestamp: str | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def pins_add(
+    self,
+    *,
+    channel: str,
+    timestamp: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Pins an item to a channel.
+    https://docs.slack.dev/reference/methods/pins.add
+    """
+    kwargs.update({"channel": channel, "timestamp": timestamp})
+    return self.api_call("pins.add", params=kwargs)
+
+ +
+
+def pins_list(self, *, channel: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def pins_list(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> SlackResponse:
+    """Lists items pinned to a channel.
+    https://docs.slack.dev/reference/methods/pins.list
+    """
+    kwargs.update({"channel": channel})
+    return self.api_call("pins.list", http_verb="GET", params=kwargs)
+
+

Lists items pinned to a channel. +https://docs.slack.dev/reference/methods/pins.list

+
+
+def pins_remove(self, *, channel: str, timestamp: str | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def pins_remove(
+    self,
+    *,
+    channel: str,
+    timestamp: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Un-pins an item from a channel.
+    https://docs.slack.dev/reference/methods/pins.remove
+    """
+    kwargs.update({"channel": channel, "timestamp": timestamp})
+    return self.api_call("pins.remove", params=kwargs)
+
+ +
+
+def reactions_add(self, *, channel: str, name: str, timestamp: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def reactions_add(
+    self,
+    *,
+    channel: str,
+    name: str,
+    timestamp: str,
+    **kwargs,
+) -> SlackResponse:
+    """Adds a reaction to an item.
+    https://docs.slack.dev/reference/methods/reactions.add
+    """
+    kwargs.update({"channel": channel, "name": name, "timestamp": timestamp})
+    return self.api_call("reactions.add", params=kwargs)
+
+ +
+
+def reactions_get(self,
*,
channel: str | None = None,
file: str | None = None,
file_comment: str | None = None,
full: bool | None = None,
timestamp: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def reactions_get(
+    self,
+    *,
+    channel: Optional[str] = None,
+    file: Optional[str] = None,
+    file_comment: Optional[str] = None,
+    full: Optional[bool] = None,
+    timestamp: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Gets reactions for an item.
+    https://docs.slack.dev/reference/methods/reactions.get
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "file": file,
+            "file_comment": file_comment,
+            "full": full,
+            "timestamp": timestamp,
+        }
+    )
+    return self.api_call("reactions.get", http_verb="GET", params=kwargs)
+
+ +
+
+def reactions_list(self,
*,
count: int | None = None,
cursor: str | None = None,
full: bool | None = None,
limit: int | None = None,
page: int | None = None,
team_id: str | None = None,
user: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def reactions_list(
+    self,
+    *,
+    count: Optional[int] = None,
+    cursor: Optional[str] = None,
+    full: Optional[bool] = None,
+    limit: Optional[int] = None,
+    page: Optional[int] = None,
+    team_id: Optional[str] = None,
+    user: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Lists reactions made by a user.
+    https://docs.slack.dev/reference/methods/reactions.list
+    """
+    kwargs.update(
+        {
+            "count": count,
+            "cursor": cursor,
+            "full": full,
+            "limit": limit,
+            "page": page,
+            "team_id": team_id,
+            "user": user,
+        }
+    )
+    return self.api_call("reactions.list", http_verb="GET", params=kwargs)
+
+ +
+
+def reactions_remove(self,
*,
name: str,
channel: str | None = None,
file: str | None = None,
file_comment: str | None = None,
timestamp: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def reactions_remove(
+    self,
+    *,
+    name: str,
+    channel: Optional[str] = None,
+    file: Optional[str] = None,
+    file_comment: Optional[str] = None,
+    timestamp: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Removes a reaction from an item.
+    https://docs.slack.dev/reference/methods/reactions.remove
+    """
+    kwargs.update(
+        {
+            "name": name,
+            "channel": channel,
+            "file": file,
+            "file_comment": file_comment,
+            "timestamp": timestamp,
+        }
+    )
+    return self.api_call("reactions.remove", params=kwargs)
+
+ +
+
+def reminders_add(self,
*,
text: str,
time: str,
team_id: str | None = None,
user: str | None = None,
recurrence: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def reminders_add(
+    self,
+    *,
+    text: str,
+    time: str,
+    team_id: Optional[str] = None,
+    user: Optional[str] = None,
+    recurrence: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Creates a reminder.
+    https://docs.slack.dev/reference/methods/reminders.add
+    """
+    kwargs.update(
+        {
+            "text": text,
+            "time": time,
+            "team_id": team_id,
+            "user": user,
+            "recurrence": recurrence,
+        }
+    )
+    return self.api_call("reminders.add", params=kwargs)
+
+ +
+
+def reminders_complete(self, *, reminder: str, team_id: str | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def reminders_complete(
+    self,
+    *,
+    reminder: str,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Marks a reminder as complete.
+    https://docs.slack.dev/reference/methods/reminders.complete
+    """
+    kwargs.update({"reminder": reminder, "team_id": team_id})
+    return self.api_call("reminders.complete", params=kwargs)
+
+ +
+
+def reminders_delete(self, *, reminder: str, team_id: str | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def reminders_delete(
+    self,
+    *,
+    reminder: str,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Deletes a reminder.
+    https://docs.slack.dev/reference/methods/reminders.delete
+    """
+    kwargs.update({"reminder": reminder, "team_id": team_id})
+    return self.api_call("reminders.delete", params=kwargs)
+
+ +
+
+def reminders_info(self, *, reminder: str, team_id: str | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def reminders_info(
+    self,
+    *,
+    reminder: str,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Gets information about a reminder.
+    https://docs.slack.dev/reference/methods/reminders.info
+    """
+    kwargs.update({"reminder": reminder, "team_id": team_id})
+    return self.api_call("reminders.info", http_verb="GET", params=kwargs)
+
+ +
+
+def reminders_list(self, *, team_id: str | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def reminders_list(
+    self,
+    *,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Lists all reminders created by or for a given user.
+    https://docs.slack.dev/reference/methods/reminders.list
+    """
+    kwargs.update({"team_id": team_id})
+    return self.api_call("reminders.list", http_verb="GET", params=kwargs)
+
+

Lists all reminders created by or for a given user. +https://docs.slack.dev/reference/methods/reminders.list

+
+
+def rtm_connect(self,
*,
batch_presence_aware: bool | None = None,
presence_sub: bool | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def rtm_connect(
+    self,
+    *,
+    batch_presence_aware: Optional[bool] = None,
+    presence_sub: Optional[bool] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Starts a Real Time Messaging session.
+    https://docs.slack.dev/reference/methods/rtm.connect
+    """
+    kwargs.update({"batch_presence_aware": batch_presence_aware, "presence_sub": presence_sub})
+    return self.api_call("rtm.connect", http_verb="GET", params=kwargs)
+
+

Starts a Real Time Messaging session. +https://docs.slack.dev/reference/methods/rtm.connect

+
+
+def rtm_start(self,
*,
batch_presence_aware: bool | None = None,
include_locale: bool | None = None,
mpim_aware: bool | None = None,
no_latest: bool | None = None,
no_unreads: bool | None = None,
presence_sub: bool | None = None,
simple_latest: bool | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def rtm_start(
+    self,
+    *,
+    batch_presence_aware: Optional[bool] = None,
+    include_locale: Optional[bool] = None,
+    mpim_aware: Optional[bool] = None,
+    no_latest: Optional[bool] = None,
+    no_unreads: Optional[bool] = None,
+    presence_sub: Optional[bool] = None,
+    simple_latest: Optional[bool] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Starts a Real Time Messaging session.
+    https://docs.slack.dev/reference/methods/rtm.start
+    """
+    kwargs.update(
+        {
+            "batch_presence_aware": batch_presence_aware,
+            "include_locale": include_locale,
+            "mpim_aware": mpim_aware,
+            "no_latest": no_latest,
+            "no_unreads": no_unreads,
+            "presence_sub": presence_sub,
+            "simple_latest": simple_latest,
+        }
+    )
+    return self.api_call("rtm.start", http_verb="GET", params=kwargs)
+
+

Starts a Real Time Messaging session. +https://docs.slack.dev/reference/methods/rtm.start

+
+
+def search_all(self,
*,
query: str,
count: int | None = None,
highlight: bool | None = None,
page: int | None = None,
sort: str | None = None,
sort_dir: str | None = None,
team_id: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def search_all(
+    self,
+    *,
+    query: str,
+    count: Optional[int] = None,
+    highlight: Optional[bool] = None,
+    page: Optional[int] = None,
+    sort: Optional[str] = None,
+    sort_dir: Optional[str] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Searches for messages and files matching a query.
+    https://docs.slack.dev/reference/methods/search.all
+    """
+    kwargs.update(
+        {
+            "query": query,
+            "count": count,
+            "highlight": highlight,
+            "page": page,
+            "sort": sort,
+            "sort_dir": sort_dir,
+            "team_id": team_id,
+        }
+    )
+    return self.api_call("search.all", http_verb="GET", params=kwargs)
+
+

Searches for messages and files matching a query. +https://docs.slack.dev/reference/methods/search.all

+
+
+def search_files(self,
*,
query: str,
count: int | None = None,
highlight: bool | None = None,
page: int | None = None,
sort: str | None = None,
sort_dir: str | None = None,
team_id: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def search_files(
+    self,
+    *,
+    query: str,
+    count: Optional[int] = None,
+    highlight: Optional[bool] = None,
+    page: Optional[int] = None,
+    sort: Optional[str] = None,
+    sort_dir: Optional[str] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Searches for files matching a query.
+    https://docs.slack.dev/reference/methods/search.files
+    """
+    kwargs.update(
+        {
+            "query": query,
+            "count": count,
+            "highlight": highlight,
+            "page": page,
+            "sort": sort,
+            "sort_dir": sort_dir,
+            "team_id": team_id,
+        }
+    )
+    return self.api_call("search.files", http_verb="GET", params=kwargs)
+
+

Searches for files matching a query. +https://docs.slack.dev/reference/methods/search.files

+
+
+def search_messages(self,
*,
query: str,
count: int | None = None,
cursor: str | None = None,
highlight: bool | None = None,
page: int | None = None,
sort: str | None = None,
sort_dir: str | None = None,
team_id: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def search_messages(
+    self,
+    *,
+    query: str,
+    count: Optional[int] = None,
+    cursor: Optional[str] = None,
+    highlight: Optional[bool] = None,
+    page: Optional[int] = None,
+    sort: Optional[str] = None,
+    sort_dir: Optional[str] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Searches for messages matching a query.
+    https://docs.slack.dev/reference/methods/search.messages
+    """
+    kwargs.update(
+        {
+            "query": query,
+            "count": count,
+            "cursor": cursor,
+            "highlight": highlight,
+            "page": page,
+            "sort": sort,
+            "sort_dir": sort_dir,
+            "team_id": team_id,
+        }
+    )
+    return self.api_call("search.messages", http_verb="GET", params=kwargs)
+
+

Searches for messages matching a query. +https://docs.slack.dev/reference/methods/search.messages

+
+
+def slackLists_access_delete(self,
*,
list_id: str,
channel_ids: List[str] | None = None,
user_ids: List[str] | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def slackLists_access_delete(
+    self,
+    *,
+    list_id: str,
+    channel_ids: Optional[List[str]] = None,
+    user_ids: Optional[List[str]] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Revoke access to a List for specified entities.
+    https://docs.slack.dev/reference/methods/slackLists.access.delete
+    """
+    kwargs.update({"list_id": list_id, "channel_ids": channel_ids, "user_ids": user_ids})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("slackLists.access.delete", json=kwargs)
+
+

Revoke access to a List for specified entities. +https://docs.slack.dev/reference/methods/slackLists.access.delete

+
+
+def slackLists_access_set(self,
*,
list_id: str,
access_level: str,
channel_ids: List[str] | None = None,
user_ids: List[str] | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def slackLists_access_set(
+    self,
+    *,
+    list_id: str,
+    access_level: str,
+    channel_ids: Optional[List[str]] = None,
+    user_ids: Optional[List[str]] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Set the access level to a List for specified entities.
+    https://docs.slack.dev/reference/methods/slackLists.access.set
+    """
+    kwargs.update({"list_id": list_id, "access_level": access_level, "channel_ids": channel_ids, "user_ids": user_ids})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("slackLists.access.set", json=kwargs)
+
+

Set the access level to a List for specified entities. +https://docs.slack.dev/reference/methods/slackLists.access.set

+
+
+def slackLists_create(self,
*,
name: str,
description_blocks: str | Sequence[Dict | RichTextBlock] | None = None,
schema: List[Dict[str, Any]] | None = None,
copy_from_list_id: str | None = None,
include_copied_list_records: bool | None = None,
todo_mode: bool | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def slackLists_create(
+    self,
+    *,
+    name: str,
+    description_blocks: Optional[Union[str, Sequence[Union[Dict, RichTextBlock]]]] = None,
+    schema: Optional[List[Dict[str, Any]]] = None,
+    copy_from_list_id: Optional[str] = None,
+    include_copied_list_records: Optional[bool] = None,
+    todo_mode: Optional[bool] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Creates a List.
+    https://docs.slack.dev/reference/methods/slackLists.create
+    """
+    kwargs.update(
+        {
+            "name": name,
+            "description_blocks": description_blocks,
+            "schema": schema,
+            "copy_from_list_id": copy_from_list_id,
+            "include_copied_list_records": include_copied_list_records,
+            "todo_mode": todo_mode,
+        }
+    )
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("slackLists.create", json=kwargs)
+
+ +
+
+def slackLists_download_get(self, *, list_id: str, job_id: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def slackLists_download_get(
+    self,
+    *,
+    list_id: str,
+    job_id: str,
+    **kwargs,
+) -> SlackResponse:
+    """Retrieve List download URL from an export job to download List contents.
+    https://docs.slack.dev/reference/methods/slackLists.download.get
+    """
+    kwargs.update(
+        {
+            "list_id": list_id,
+            "job_id": job_id,
+        }
+    )
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("slackLists.download.get", json=kwargs)
+
+

Retrieve List download URL from an export job to download List contents. +https://docs.slack.dev/reference/methods/slackLists.download.get

+
+
+def slackLists_download_start(self, *, list_id: str, include_archived: bool | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def slackLists_download_start(
+    self,
+    *,
+    list_id: str,
+    include_archived: Optional[bool] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Initiate a job to export List contents.
+    https://docs.slack.dev/reference/methods/slackLists.download.start
+    """
+    kwargs.update(
+        {
+            "list_id": list_id,
+            "include_archived": include_archived,
+        }
+    )
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("slackLists.download.start", json=kwargs)
+
+ +
+
+def slackLists_items_create(self,
*,
list_id: str,
duplicated_item_id: str | None = None,
parent_item_id: str | None = None,
initial_fields: List[Dict[str, Any]] | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def slackLists_items_create(
+    self,
+    *,
+    list_id: str,
+    duplicated_item_id: Optional[str] = None,
+    parent_item_id: Optional[str] = None,
+    initial_fields: Optional[List[Dict[str, Any]]] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Add a new item to an existing List.
+    https://docs.slack.dev/reference/methods/slackLists.items.create
+    """
+    kwargs.update(
+        {
+            "list_id": list_id,
+            "duplicated_item_id": duplicated_item_id,
+            "parent_item_id": parent_item_id,
+            "initial_fields": initial_fields,
+        }
+    )
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("slackLists.items.create", json=kwargs)
+
+ +
+
+def slackLists_items_delete(self, *, list_id: str, id: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def slackLists_items_delete(
+    self,
+    *,
+    list_id: str,
+    id: str,
+    **kwargs,
+) -> SlackResponse:
+    """Deletes an item from an existing List.
+    https://docs.slack.dev/reference/methods/slackLists.items.delete
+    """
+    kwargs.update(
+        {
+            "list_id": list_id,
+            "id": id,
+        }
+    )
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("slackLists.items.delete", json=kwargs)
+
+ +
+
+def slackLists_items_deleteMultiple(self, *, list_id: str, ids: List[str], **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def slackLists_items_deleteMultiple(
+    self,
+    *,
+    list_id: str,
+    ids: List[str],
+    **kwargs,
+) -> SlackResponse:
+    """Deletes multiple items from an existing List.
+    https://docs.slack.dev/reference/methods/slackLists.items.deleteMultiple
+    """
+    kwargs.update(
+        {
+            "list_id": list_id,
+            "ids": ids,
+        }
+    )
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("slackLists.items.deleteMultiple", json=kwargs)
+
+ +
+
+def slackLists_items_info(self, *, list_id: str, id: str, include_is_subscribed: bool | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def slackLists_items_info(
+    self,
+    *,
+    list_id: str,
+    id: str,
+    include_is_subscribed: Optional[bool] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Get a row from a List.
+    https://docs.slack.dev/reference/methods/slackLists.items.info
+    """
+    kwargs.update(
+        {
+            "list_id": list_id,
+            "id": id,
+            "include_is_subscribed": include_is_subscribed,
+        }
+    )
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("slackLists.items.info", json=kwargs)
+
+ +
+
+def slackLists_items_list(self,
*,
list_id: str,
limit: int | None = None,
cursor: str | None = None,
archived: bool | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def slackLists_items_list(
+    self,
+    *,
+    list_id: str,
+    limit: Optional[int] = None,
+    cursor: Optional[str] = None,
+    archived: Optional[bool] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Get records from a List.
+    https://docs.slack.dev/reference/methods/slackLists.items.list
+    """
+    kwargs.update(
+        {
+            "list_id": list_id,
+            "limit": limit,
+            "cursor": cursor,
+            "archived": archived,
+        }
+    )
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("slackLists.items.list", json=kwargs)
+
+ +
+
+def slackLists_items_update(self, *, list_id: str, cells: List[Dict[str, Any]], **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def slackLists_items_update(
+    self,
+    *,
+    list_id: str,
+    cells: List[Dict[str, Any]],
+    **kwargs,
+) -> SlackResponse:
+    """Updates cells in a List.
+    https://docs.slack.dev/reference/methods/slackLists.items.update
+    """
+    kwargs.update(
+        {
+            "list_id": list_id,
+            "cells": cells,
+        }
+    )
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("slackLists.items.update", json=kwargs)
+
+ +
+
+def slackLists_update(self,
*,
id: str,
name: str | None = None,
description_blocks: str | Sequence[Dict | RichTextBlock] | None = None,
todo_mode: bool | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def slackLists_update(
+    self,
+    *,
+    id: str,
+    name: Optional[str] = None,
+    description_blocks: Optional[Union[str, Sequence[Union[Dict, RichTextBlock]]]] = None,
+    todo_mode: Optional[bool] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Update a List.
+    https://docs.slack.dev/reference/methods/slackLists.update
+    """
+    kwargs.update(
+        {
+            "id": id,
+            "name": name,
+            "description_blocks": description_blocks,
+            "todo_mode": todo_mode,
+        }
+    )
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("slackLists.update", json=kwargs)
+
+ +
+
+def stars_add(self,
*,
channel: str | None = None,
file: str | None = None,
file_comment: str | None = None,
timestamp: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def stars_add(
+    self,
+    *,
+    channel: Optional[str] = None,
+    file: Optional[str] = None,
+    file_comment: Optional[str] = None,
+    timestamp: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Adds a star to an item.
+    https://docs.slack.dev/reference/methods/stars.add
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "file": file,
+            "file_comment": file_comment,
+            "timestamp": timestamp,
+        }
+    )
+    return self.api_call("stars.add", params=kwargs)
+
+ +
+
+def stars_list(self,
*,
count: int | None = None,
cursor: str | None = None,
limit: int | None = None,
page: int | None = None,
team_id: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def stars_list(
+    self,
+    *,
+    count: Optional[int] = None,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    page: Optional[int] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Lists stars for a user.
+    https://docs.slack.dev/reference/methods/stars.list
+    """
+    kwargs.update(
+        {
+            "count": count,
+            "cursor": cursor,
+            "limit": limit,
+            "page": page,
+            "team_id": team_id,
+        }
+    )
+    return self.api_call("stars.list", http_verb="GET", params=kwargs)
+
+ +
+
+def stars_remove(self,
*,
channel: str | None = None,
file: str | None = None,
file_comment: str | None = None,
timestamp: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def stars_remove(
+    self,
+    *,
+    channel: Optional[str] = None,
+    file: Optional[str] = None,
+    file_comment: Optional[str] = None,
+    timestamp: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Removes a star from an item.
+    https://docs.slack.dev/reference/methods/stars.remove
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "file": file,
+            "file_comment": file_comment,
+            "timestamp": timestamp,
+        }
+    )
+    return self.api_call("stars.remove", params=kwargs)
+
+ +
+
+def team_accessLogs(self,
*,
before: str | int | None = None,
count: str | int | None = None,
page: str | int | None = None,
team_id: str | None = None,
cursor: str | None = None,
limit: int | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def team_accessLogs(
+    self,
+    *,
+    before: Optional[Union[int, str]] = None,
+    count: Optional[Union[int, str]] = None,
+    page: Optional[Union[int, str]] = None,
+    team_id: Optional[str] = None,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Gets the access logs for the current team.
+    https://docs.slack.dev/reference/methods/team.accessLogs
+    """
+    kwargs.update(
+        {
+            "before": before,
+            "count": count,
+            "page": page,
+            "team_id": team_id,
+            "cursor": cursor,
+            "limit": limit,
+        }
+    )
+    return self.api_call("team.accessLogs", http_verb="GET", params=kwargs)
+
+

Gets the access logs for the current team. +https://docs.slack.dev/reference/methods/team.accessLogs

+
+
+def team_billableInfo(self, *, team_id: str | None = None, user: str | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def team_billableInfo(
+    self,
+    *,
+    team_id: Optional[str] = None,
+    user: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Gets billable users information for the current team.
+    https://docs.slack.dev/reference/methods/team.billableInfo
+    """
+    kwargs.update({"team_id": team_id, "user": user})
+    return self.api_call("team.billableInfo", http_verb="GET", params=kwargs)
+
+

Gets billable users information for the current team. +https://docs.slack.dev/reference/methods/team.billableInfo

+
+
+def team_billing_info(self, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def team_billing_info(
+    self,
+    **kwargs,
+) -> SlackResponse:
+    """Reads a workspace's billing plan information.
+    https://docs.slack.dev/reference/methods/team.billing.info
+    """
+    return self.api_call("team.billing.info", params=kwargs)
+
+

Reads a workspace's billing plan information. +https://docs.slack.dev/reference/methods/team.billing.info

+
+
+def team_externalTeams_disconnect(self, *, target_team: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def team_externalTeams_disconnect(
+    self,
+    *,
+    target_team: str,
+    **kwargs,
+) -> SlackResponse:
+    """Disconnects an external organization.
+    https://docs.slack.dev/reference/methods/team.externalTeams.disconnect
+    """
+    kwargs.update(
+        {
+            "target_team": target_team,
+        }
+    )
+    return self.api_call("team.externalTeams.disconnect", params=kwargs)
+
+ +
+
+def team_externalTeams_list(self,
*,
connection_status_filter: str | None = None,
slack_connect_pref_filter: Sequence[str] | None = None,
sort_direction: str | None = None,
sort_field: str | None = None,
workspace_filter: Sequence[str] | None = None,
cursor: str | None = None,
limit: int | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def team_externalTeams_list(
+    self,
+    *,
+    connection_status_filter: Optional[str] = None,
+    slack_connect_pref_filter: Optional[Sequence[str]] = None,
+    sort_direction: Optional[str] = None,
+    sort_field: Optional[str] = None,
+    workspace_filter: Optional[Sequence[str]] = None,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Returns a list of all the external teams connected and details about the connection.
+    https://docs.slack.dev/reference/methods/team.externalTeams.list
+    """
+    kwargs.update(
+        {
+            "connection_status_filter": connection_status_filter,
+            "sort_direction": sort_direction,
+            "sort_field": sort_field,
+            "cursor": cursor,
+            "limit": limit,
+        }
+    )
+    if slack_connect_pref_filter is not None:
+        if isinstance(slack_connect_pref_filter, (list, tuple)):
+            kwargs.update({"slack_connect_pref_filter": ",".join(slack_connect_pref_filter)})
+        else:
+            kwargs.update({"slack_connect_pref_filter": slack_connect_pref_filter})
+    if workspace_filter is not None:
+        if isinstance(workspace_filter, (list, tuple)):
+            kwargs.update({"workspace_filter": ",".join(workspace_filter)})
+        else:
+            kwargs.update({"workspace_filter": workspace_filter})
+    return self.api_call("team.externalTeams.list", http_verb="GET", params=kwargs)
+
+

Returns a list of all the external teams connected and details about the connection. +https://docs.slack.dev/reference/methods/team.externalTeams.list

+
+
+def team_info(self, *, team: str | None = None, domain: str | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def team_info(
+    self,
+    *,
+    team: Optional[str] = None,
+    domain: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Gets information about the current team.
+    https://docs.slack.dev/reference/methods/team.info
+    """
+    kwargs.update({"team": team, "domain": domain})
+    return self.api_call("team.info", http_verb="GET", params=kwargs)
+
+

Gets information about the current team. +https://docs.slack.dev/reference/methods/team.info

+
+
+def team_integrationLogs(self,
*,
app_id: str | None = None,
change_type: str | None = None,
count: str | int | None = None,
page: str | int | None = None,
service_id: str | None = None,
team_id: str | None = None,
user: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def team_integrationLogs(
+    self,
+    *,
+    app_id: Optional[str] = None,
+    change_type: Optional[str] = None,
+    count: Optional[Union[int, str]] = None,
+    page: Optional[Union[int, str]] = None,
+    service_id: Optional[str] = None,
+    team_id: Optional[str] = None,
+    user: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Gets the integration logs for the current team.
+    https://docs.slack.dev/reference/methods/team.integrationLogs
+    """
+    kwargs.update(
+        {
+            "app_id": app_id,
+            "change_type": change_type,
+            "count": count,
+            "page": page,
+            "service_id": service_id,
+            "team_id": team_id,
+            "user": user,
+        }
+    )
+    return self.api_call("team.integrationLogs", http_verb="GET", params=kwargs)
+
+

Gets the integration logs for the current team. +https://docs.slack.dev/reference/methods/team.integrationLogs

+
+
+def team_preferences_list(self, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def team_preferences_list(
+    self,
+    **kwargs,
+) -> SlackResponse:
+    """Retrieve a list of a workspace's team preferences.
+    https://docs.slack.dev/reference/methods/team.preferences.list
+    """
+    return self.api_call("team.preferences.list", params=kwargs)
+
+

Retrieve a list of a workspace's team preferences. +https://docs.slack.dev/reference/methods/team.preferences.list

+
+
+def team_profile_get(self, *, visibility: str | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def team_profile_get(
+    self,
+    *,
+    visibility: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Retrieve a team's profile.
+    https://docs.slack.dev/reference/methods/team.profile.get
+    """
+    kwargs.update({"visibility": visibility})
+    return self.api_call("team.profile.get", http_verb="GET", params=kwargs)
+
+ +
+
+def tooling_tokens_rotate(self, *, refresh_token: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def tooling_tokens_rotate(
+    self,
+    *,
+    refresh_token: str,
+    **kwargs,
+) -> SlackResponse:
+    """Exchanges a refresh token for a new app configuration token
+    https://docs.slack.dev/reference/methods/tooling.tokens.rotate
+    """
+    kwargs.update({"refresh_token": refresh_token})
+    return self.api_call("tooling.tokens.rotate", params=kwargs)
+
+

Exchanges a refresh token for a new app configuration token +https://docs.slack.dev/reference/methods/tooling.tokens.rotate

+
+
+def usergroups_create(self,
*,
name: str,
channels: str | Sequence[str] | None = None,
description: str | None = None,
handle: str | None = None,
include_count: bool | None = None,
team_id: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def usergroups_create(
+    self,
+    *,
+    name: str,
+    channels: Optional[Union[str, Sequence[str]]] = None,
+    description: Optional[str] = None,
+    handle: Optional[str] = None,
+    include_count: Optional[bool] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Create a User Group
+    https://docs.slack.dev/reference/methods/usergroups.create
+    """
+    kwargs.update(
+        {
+            "name": name,
+            "description": description,
+            "handle": handle,
+            "include_count": include_count,
+            "team_id": team_id,
+        }
+    )
+    if isinstance(channels, (list, tuple)):
+        kwargs.update({"channels": ",".join(channels)})
+    else:
+        kwargs.update({"channels": channels})
+    return self.api_call("usergroups.create", params=kwargs)
+
+ +
+
+def usergroups_disable(self,
*,
usergroup: str,
include_count: bool | None = None,
team_id: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def usergroups_disable(
+    self,
+    *,
+    usergroup: str,
+    include_count: Optional[bool] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Disable an existing User Group
+    https://docs.slack.dev/reference/methods/usergroups.disable
+    """
+    kwargs.update({"usergroup": usergroup, "include_count": include_count, "team_id": team_id})
+    return self.api_call("usergroups.disable", params=kwargs)
+
+ +
+
+def usergroups_enable(self,
*,
usergroup: str,
include_count: bool | None = None,
team_id: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def usergroups_enable(
+    self,
+    *,
+    usergroup: str,
+    include_count: Optional[bool] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Enable a User Group
+    https://docs.slack.dev/reference/methods/usergroups.enable
+    """
+    kwargs.update({"usergroup": usergroup, "include_count": include_count, "team_id": team_id})
+    return self.api_call("usergroups.enable", params=kwargs)
+
+ +
+
+def usergroups_list(self,
*,
include_count: bool | None = None,
include_disabled: bool | None = None,
include_users: bool | None = None,
team_id: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def usergroups_list(
+    self,
+    *,
+    include_count: Optional[bool] = None,
+    include_disabled: Optional[bool] = None,
+    include_users: Optional[bool] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """List all User Groups for a team
+    https://docs.slack.dev/reference/methods/usergroups.list
+    """
+    kwargs.update(
+        {
+            "include_count": include_count,
+            "include_disabled": include_disabled,
+            "include_users": include_users,
+            "team_id": team_id,
+        }
+    )
+    return self.api_call("usergroups.list", http_verb="GET", params=kwargs)
+
+ +
+
+def usergroups_update(self,
*,
usergroup: str,
channels: str | Sequence[str] | None = None,
description: str | None = None,
handle: str | None = None,
include_count: bool | None = None,
name: str | None = None,
team_id: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def usergroups_update(
+    self,
+    *,
+    usergroup: str,
+    channels: Optional[Union[str, Sequence[str]]] = None,
+    description: Optional[str] = None,
+    handle: Optional[str] = None,
+    include_count: Optional[bool] = None,
+    name: Optional[str] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Update an existing User Group
+    https://docs.slack.dev/reference/methods/usergroups.update
+    """
+    kwargs.update(
+        {
+            "usergroup": usergroup,
+            "description": description,
+            "handle": handle,
+            "include_count": include_count,
+            "name": name,
+            "team_id": team_id,
+        }
+    )
+    if isinstance(channels, (list, tuple)):
+        kwargs.update({"channels": ",".join(channels)})
+    else:
+        kwargs.update({"channels": channels})
+    return self.api_call("usergroups.update", params=kwargs)
+
+ +
+
+def usergroups_users_list(self,
*,
usergroup: str,
include_disabled: bool | None = None,
team_id: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def usergroups_users_list(
+    self,
+    *,
+    usergroup: str,
+    include_disabled: Optional[bool] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """List all users in a User Group
+    https://docs.slack.dev/reference/methods/usergroups.users.list
+    """
+    kwargs.update(
+        {
+            "usergroup": usergroup,
+            "include_disabled": include_disabled,
+            "team_id": team_id,
+        }
+    )
+    return self.api_call("usergroups.users.list", http_verb="GET", params=kwargs)
+
+ +
+
+def usergroups_users_update(self,
*,
usergroup: str,
users: str | Sequence[str],
include_count: bool | None = None,
team_id: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def usergroups_users_update(
+    self,
+    *,
+    usergroup: str,
+    users: Union[str, Sequence[str]],
+    include_count: Optional[bool] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Update the list of users for a User Group
+    https://docs.slack.dev/reference/methods/usergroups.users.update
+    """
+    kwargs.update(
+        {
+            "usergroup": usergroup,
+            "include_count": include_count,
+            "team_id": team_id,
+        }
+    )
+    if isinstance(users, (list, tuple)):
+        kwargs.update({"users": ",".join(users)})
+    else:
+        kwargs.update({"users": users})
+    return self.api_call("usergroups.users.update", params=kwargs)
+
+

Update the list of users for a User Group +https://docs.slack.dev/reference/methods/usergroups.users.update

+
+
+def users_conversations(self,
*,
cursor: str | None = None,
exclude_archived: bool | None = None,
limit: int | None = None,
team_id: str | None = None,
types: str | Sequence[str] | None = None,
user: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def users_conversations(
+    self,
+    *,
+    cursor: Optional[str] = None,
+    exclude_archived: Optional[bool] = None,
+    limit: Optional[int] = None,
+    team_id: Optional[str] = None,
+    types: Optional[Union[str, Sequence[str]]] = None,
+    user: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """List conversations the calling user may access.
+    https://docs.slack.dev/reference/methods/users.conversations
+    """
+    kwargs.update(
+        {
+            "cursor": cursor,
+            "exclude_archived": exclude_archived,
+            "limit": limit,
+            "team_id": team_id,
+            "user": user,
+        }
+    )
+    if isinstance(types, (list, tuple)):
+        kwargs.update({"types": ",".join(types)})
+    else:
+        kwargs.update({"types": types})
+    return self.api_call("users.conversations", http_verb="GET", params=kwargs)
+
+

List conversations the calling user may access. +https://docs.slack.dev/reference/methods/users.conversations

+
+
+def users_deletePhoto(self, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def users_deletePhoto(
+    self,
+    **kwargs,
+) -> SlackResponse:
+    """Delete the user profile photo
+    https://docs.slack.dev/reference/methods/users.deletePhoto
+    """
+    return self.api_call("users.deletePhoto", http_verb="GET", params=kwargs)
+
+ +
+
+def users_discoverableContacts_lookup(self, email: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def users_discoverableContacts_lookup(
+    self,
+    email: str,
+    **kwargs,
+) -> SlackResponse:
+    """Lookup an email address to see if someone is on Slack
+    https://docs.slack.dev/reference/methods/users.discoverableContacts.lookup
+    """
+    kwargs.update({"email": email})
+    return self.api_call("users.discoverableContacts.lookup", params=kwargs)
+
+

Lookup an email address to see if someone is on Slack +https://docs.slack.dev/reference/methods/users.discoverableContacts.lookup

+
+
+def users_getPresence(self, *, user: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def users_getPresence(
+    self,
+    *,
+    user: str,
+    **kwargs,
+) -> SlackResponse:
+    """Gets user presence information.
+    https://docs.slack.dev/reference/methods/users.getPresence
+    """
+    kwargs.update({"user": user})
+    return self.api_call("users.getPresence", http_verb="GET", params=kwargs)
+
+ +
+
+def users_identity(self, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def users_identity(
+    self,
+    **kwargs,
+) -> SlackResponse:
+    """Get a user's identity.
+    https://docs.slack.dev/reference/methods/users.identity
+    """
+    return self.api_call("users.identity", http_verb="GET", params=kwargs)
+
+ +
+
+def users_info(self, *, user: str, include_locale: bool | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def users_info(
+    self,
+    *,
+    user: str,
+    include_locale: Optional[bool] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Gets information about a user.
+    https://docs.slack.dev/reference/methods/users.info
+    """
+    kwargs.update({"user": user, "include_locale": include_locale})
+    return self.api_call("users.info", http_verb="GET", params=kwargs)
+
+ +
+
+def users_list(self,
*,
cursor: str | None = None,
include_locale: bool | None = None,
limit: int | None = None,
team_id: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def users_list(
+    self,
+    *,
+    cursor: Optional[str] = None,
+    include_locale: Optional[bool] = None,
+    limit: Optional[int] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Lists all users in a Slack team.
+    https://docs.slack.dev/reference/methods/users.list
+    """
+    kwargs.update(
+        {
+            "cursor": cursor,
+            "include_locale": include_locale,
+            "limit": limit,
+            "team_id": team_id,
+        }
+    )
+    return self.api_call("users.list", http_verb="GET", params=kwargs)
+
+

Lists all users in a Slack team. +https://docs.slack.dev/reference/methods/users.list

+
+
+def users_lookupByEmail(self, *, email: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def users_lookupByEmail(
+    self,
+    *,
+    email: str,
+    **kwargs,
+) -> SlackResponse:
+    """Find a user with an email address.
+    https://docs.slack.dev/reference/methods/users.lookupByEmail
+    """
+    kwargs.update({"email": email})
+    return self.api_call("users.lookupByEmail", http_verb="GET", params=kwargs)
+
+ +
+
+def users_profile_get(self, *, user: str | None = None, include_labels: bool | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def users_profile_get(
+    self,
+    *,
+    user: Optional[str] = None,
+    include_labels: Optional[bool] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Retrieves a user's profile information.
+    https://docs.slack.dev/reference/methods/users.profile.get
+    """
+    kwargs.update({"user": user, "include_labels": include_labels})
+    return self.api_call("users.profile.get", http_verb="GET", params=kwargs)
+
+

Retrieves a user's profile information. +https://docs.slack.dev/reference/methods/users.profile.get

+
+
+def users_profile_set(self,
*,
name: str | None = None,
value: str | None = None,
user: str | None = None,
profile: Dict | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def users_profile_set(
+    self,
+    *,
+    name: Optional[str] = None,
+    value: Optional[str] = None,
+    user: Optional[str] = None,
+    profile: Optional[Dict] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Set the profile information for a user.
+    https://docs.slack.dev/reference/methods/users.profile.set
+    """
+    kwargs.update(
+        {
+            "name": name,
+            "profile": profile,
+            "user": user,
+            "value": value,
+        }
+    )
+    kwargs = _remove_none_values(kwargs)
+    # NOTE: Intentionally using json for the "profile" parameter
+    return self.api_call("users.profile.set", json=kwargs)
+
+

Set the profile information for a user. +https://docs.slack.dev/reference/methods/users.profile.set

+
+
+def users_setPhoto(self,
*,
image: str | io.IOBase,
crop_w: str | int | None = None,
crop_x: str | int | None = None,
crop_y: str | int | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def users_setPhoto(
+    self,
+    *,
+    image: Union[str, IOBase],
+    crop_w: Optional[Union[int, str]] = None,
+    crop_x: Optional[Union[int, str]] = None,
+    crop_y: Optional[Union[int, str]] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Set the user profile photo
+    https://docs.slack.dev/reference/methods/users.setPhoto
+    """
+    kwargs.update({"crop_w": crop_w, "crop_x": crop_x, "crop_y": crop_y})
+    return self.api_call("users.setPhoto", files={"image": image}, data=kwargs)
+
+ +
+
+def users_setPresence(self, *, presence: str, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def users_setPresence(
+    self,
+    *,
+    presence: str,
+    **kwargs,
+) -> SlackResponse:
+    """Manually sets user presence.
+    https://docs.slack.dev/reference/methods/users.setPresence
+    """
+    kwargs.update({"presence": presence})
+    return self.api_call("users.setPresence", params=kwargs)
+
+ +
+
+def views_open(self,
*,
trigger_id: str | None = None,
interactivity_pointer: str | None = None,
view: dict | View,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def views_open(
+    self,
+    *,
+    trigger_id: Optional[str] = None,
+    interactivity_pointer: Optional[str] = None,
+    view: Union[dict, View],
+    **kwargs,
+) -> SlackResponse:
+    """Open a view for a user.
+    https://docs.slack.dev/reference/methods/views.open
+    See https://docs.slack.dev/surfaces/modals/ for details.
+    """
+    kwargs.update({"trigger_id": trigger_id, "interactivity_pointer": interactivity_pointer})
+    if isinstance(view, View):
+        kwargs.update({"view": view.to_dict()})
+    else:
+        kwargs.update({"view": view})
+    kwargs = _remove_none_values(kwargs)
+    # NOTE: Intentionally using json for the "view" parameter
+    return self.api_call("views.open", json=kwargs)
+
+ +
+
+def views_publish(self,
*,
user_id: str,
view: dict | View,
hash: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def views_publish(
+    self,
+    *,
+    user_id: str,
+    view: Union[dict, View],
+    hash: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Publish a static view for a User.
+    Create or update the view that comprises an
+    app's Home tab (https://docs.slack.dev/surfaces/app-home/)
+    https://docs.slack.dev/reference/methods/views.publish
+    """
+    kwargs.update({"user_id": user_id, "hash": hash})
+    if isinstance(view, View):
+        kwargs.update({"view": view.to_dict()})
+    else:
+        kwargs.update({"view": view})
+    kwargs = _remove_none_values(kwargs)
+    # NOTE: Intentionally using json for the "view" parameter
+    return self.api_call("views.publish", json=kwargs)
+
+

Publish a static view for a User. +Create or update the view that comprises an +app's Home tab (https://docs.slack.dev/surfaces/app-home/) +https://docs.slack.dev/reference/methods/views.publish

+
+
+def views_push(self,
*,
trigger_id: str | None = None,
interactivity_pointer: str | None = None,
view: dict | View,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def views_push(
+    self,
+    *,
+    trigger_id: Optional[str] = None,
+    interactivity_pointer: Optional[str] = None,
+    view: Union[dict, View],
+    **kwargs,
+) -> SlackResponse:
+    """Push a view onto the stack of a root view.
+    Push a new view onto the existing view stack by passing a view
+    payload and a valid trigger_id generated from an interaction
+    within the existing modal.
+    Read the modals documentation (https://docs.slack.dev/surfaces/modals/)
+    to learn more about the lifecycle and intricacies of views.
+    https://docs.slack.dev/reference/methods/views.push
+    """
+    kwargs.update({"trigger_id": trigger_id, "interactivity_pointer": interactivity_pointer})
+    if isinstance(view, View):
+        kwargs.update({"view": view.to_dict()})
+    else:
+        kwargs.update({"view": view})
+    kwargs = _remove_none_values(kwargs)
+    # NOTE: Intentionally using json for the "view" parameter
+    return self.api_call("views.push", json=kwargs)
+
+

Push a view onto the stack of a root view. +Push a new view onto the existing view stack by passing a view +payload and a valid trigger_id generated from an interaction +within the existing modal. +Read the modals documentation (https://docs.slack.dev/surfaces/modals/) +to learn more about the lifecycle and intricacies of views. +https://docs.slack.dev/reference/methods/views.push

+
+
+def views_update(self,
*,
view: dict | View,
external_id: str | None = None,
view_id: str | None = None,
hash: str | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def views_update(
+    self,
+    *,
+    view: Union[dict, View],
+    external_id: Optional[str] = None,
+    view_id: Optional[str] = None,
+    hash: Optional[str] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Update an existing view.
+    Update a view by passing a new view definition along with the
+    view_id returned in views.open or the external_id.
+    See the modals documentation (https://docs.slack.dev/surfaces/modals/#updating_views)
+    to learn more about updating views and avoiding race conditions with the hash argument.
+    https://docs.slack.dev/reference/methods/views.update
+    """
+    if isinstance(view, View):
+        kwargs.update({"view": view.to_dict()})
+    else:
+        kwargs.update({"view": view})
+    if external_id:
+        kwargs.update({"external_id": external_id})
+    elif view_id:
+        kwargs.update({"view_id": view_id})
+    else:
+        raise e.SlackRequestError("Either view_id or external_id is required.")
+    kwargs.update({"hash": hash})
+    kwargs = _remove_none_values(kwargs)
+    # NOTE: Intentionally using json for the "view" parameter
+    return self.api_call("views.update", json=kwargs)
+
+

Update an existing view. +Update a view by passing a new view definition along with the +view_id returned in views.open or the external_id. +See the modals documentation (https://docs.slack.dev/surfaces/modals/#updating_views) +to learn more about updating views and avoiding race conditions with the hash argument. +https://docs.slack.dev/reference/methods/views.update

+
+ +
+
+ +Expand source code + +
def workflows_featured_add(
+    self,
+    *,
+    channel_id: str,
+    trigger_ids: Union[str, Sequence[str]],
+    **kwargs,
+) -> SlackResponse:
+    """Add featured workflows to a channel.
+    https://docs.slack.dev/reference/methods/workflows.featured.add
+    """
+    kwargs.update({"channel_id": channel_id})
+    if isinstance(trigger_ids, (list, tuple)):
+        kwargs.update({"trigger_ids": ",".join(trigger_ids)})
+    else:
+        kwargs.update({"trigger_ids": trigger_ids})
+    return self.api_call("workflows.featured.add", params=kwargs)
+
+ +
+ +
+
+ +Expand source code + +
def workflows_featured_list(
+    self,
+    *,
+    channel_ids: Union[str, Sequence[str]],
+    **kwargs,
+) -> SlackResponse:
+    """List the featured workflows for specified channels.
+    https://docs.slack.dev/reference/methods/workflows.featured.list
+    """
+    if isinstance(channel_ids, (list, tuple)):
+        kwargs.update({"channel_ids": ",".join(channel_ids)})
+    else:
+        kwargs.update({"channel_ids": channel_ids})
+    return self.api_call("workflows.featured.list", params=kwargs)
+
+

List the featured workflows for specified channels. +https://docs.slack.dev/reference/methods/workflows.featured.list

+
+ +
+
+ +Expand source code + +
def workflows_featured_remove(
+    self,
+    *,
+    channel_id: str,
+    trigger_ids: Union[str, Sequence[str]],
+    **kwargs,
+) -> SlackResponse:
+    """Remove featured workflows from a channel.
+    https://docs.slack.dev/reference/methods/workflows.featured.remove
+    """
+    kwargs.update({"channel_id": channel_id})
+    if isinstance(trigger_ids, (list, tuple)):
+        kwargs.update({"trigger_ids": ",".join(trigger_ids)})
+    else:
+        kwargs.update({"trigger_ids": trigger_ids})
+    return self.api_call("workflows.featured.remove", params=kwargs)
+
+ +
+ +
+
+ +Expand source code + +
def workflows_featured_set(
+    self,
+    *,
+    channel_id: str,
+    trigger_ids: Union[str, Sequence[str]],
+    **kwargs,
+) -> SlackResponse:
+    """Set featured workflows for a channel.
+    https://docs.slack.dev/reference/methods/workflows.featured.set
+    """
+    kwargs.update({"channel_id": channel_id})
+    if isinstance(trigger_ids, (list, tuple)):
+        kwargs.update({"trigger_ids": ",".join(trigger_ids)})
+    else:
+        kwargs.update({"trigger_ids": trigger_ids})
+    return self.api_call("workflows.featured.set", params=kwargs)
+
+ +
+
+def workflows_stepCompleted(self, *, workflow_step_execute_id: str, outputs: dict | None = None, **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def workflows_stepCompleted(
+    self,
+    *,
+    workflow_step_execute_id: str,
+    outputs: Optional[dict] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Indicate a successful outcome of a workflow step's execution.
+    https://docs.slack.dev/reference/methods/workflows.stepCompleted
+    """
+    kwargs.update({"workflow_step_execute_id": workflow_step_execute_id})
+    if outputs is not None:
+        kwargs.update({"outputs": outputs})
+    kwargs = _remove_none_values(kwargs)
+    # NOTE: Intentionally using json for the "outputs" parameter
+    return self.api_call("workflows.stepCompleted", json=kwargs)
+
+

Indicate a successful outcome of a workflow step's execution. +https://docs.slack.dev/reference/methods/workflows.stepCompleted

+
+
+def workflows_stepFailed(self, *, workflow_step_execute_id: str, error: Dict[str, str], **kwargs) ‑> SlackResponse +
+
+
+ +Expand source code + +
def workflows_stepFailed(
+    self,
+    *,
+    workflow_step_execute_id: str,
+    error: Dict[str, str],
+    **kwargs,
+) -> SlackResponse:
+    """Indicate an unsuccessful outcome of a workflow step's execution.
+    https://docs.slack.dev/reference/methods/workflows.stepFailed
+    """
+    kwargs.update(
+        {
+            "workflow_step_execute_id": workflow_step_execute_id,
+            "error": error,
+        }
+    )
+    kwargs = _remove_none_values(kwargs)
+    # NOTE: Intentionally using json for the "error" parameter
+    return self.api_call("workflows.stepFailed", json=kwargs)
+
+

Indicate an unsuccessful outcome of a workflow step's execution. +https://docs.slack.dev/reference/methods/workflows.stepFailed

+
+
+def workflows_updateStep(self,
*,
workflow_step_edit_id: str,
inputs: Dict[str, Any] | None = None,
outputs: List[Dict[str, str]] | None = None,
**kwargs) ‑> SlackResponse
+
+
+
+ +Expand source code + +
def workflows_updateStep(
+    self,
+    *,
+    workflow_step_edit_id: str,
+    inputs: Optional[Dict[str, Any]] = None,
+    outputs: Optional[List[Dict[str, str]]] = None,
+    **kwargs,
+) -> SlackResponse:
+    """Update the configuration for a workflow extension step.
+    https://docs.slack.dev/reference/methods/workflows.updateStep
+    """
+    kwargs.update({"workflow_step_edit_id": workflow_step_edit_id})
+    if inputs is not None:
+        kwargs.update({"inputs": inputs})
+    if outputs is not None:
+        kwargs.update({"outputs": outputs})
+    kwargs = _remove_none_values(kwargs)
+    # NOTE: Intentionally using json for the "inputs" / "outputs" parameters
+    return self.api_call("workflows.updateStep", json=kwargs)
+
+

Update the configuration for a workflow extension step. +https://docs.slack.dev/reference/methods/workflows.updateStep

+
+
+

Inherited members

+ +
+
+
+
+ +
+ + + diff --git a/docs/reference/web/internal_utils.html b/docs/reference/web/internal_utils.html new file mode 100644 index 000000000..89227edce --- /dev/null +++ b/docs/reference/web/internal_utils.html @@ -0,0 +1,143 @@ + + + + + + +slack_sdk.web.internal_utils API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.web.internal_utils

+
+
+
+
+
+
+
+
+

Functions

+
+
+def convert_bool_to_0_or_1(params: Dict[str, Any] | None) ‑> Dict[str, Any] | None +
+
+
+ +Expand source code + +
def convert_bool_to_0_or_1(params: Optional[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
+    """Converts all bool values in dict to "0" or "1".
+
+    Slack APIs safely accept "0"/"1" as boolean values.
+    Using True/False (bool in Python) doesn't work with aiohttp.
+    This method converts only the bool values in top-level of a given dict.
+
+    Args:
+        params: params as a dict
+
+    Returns:
+        Modified dict
+    """
+    if params:
+        return {k: _to_0_or_1_if_bool(v) for k, v in params.items()}
+    return None
+
+

Converts all bool values in dict to "0" or "1".

+

Slack APIs safely accept "0"/"1" as boolean values. +Using True/False (bool in Python) doesn't work with aiohttp. +This method converts only the bool values in top-level of a given dict.

+

Args

+
+
params
+
params as a dict
+
+

Returns

+

Modified dict

+
+
+def get_user_agent(prefix: str | None = None, suffix: str | None = None) +
+
+
+ +Expand source code + +
def get_user_agent(prefix: Optional[str] = None, suffix: Optional[str] = None):
+    """Construct the user-agent header with the package info,
+    Python version and OS version.
+
+    Returns:
+        The user agent string.
+        e.g. 'Python/3.7.17 slackclient/2.0.0 Darwin/17.7.0'
+    """
+    # __name__ returns all classes, we only want the client
+    client = "{0}/{1}".format("slackclient", version.__version__)
+    python_version = "Python/{v.major}.{v.minor}.{v.micro}".format(v=sys.version_info)
+    system_info = "{0}/{1}".format(platform.system(), platform.release())
+    user_agent_string = " ".join([python_version, client, system_info])
+    prefix = f"{prefix} " if prefix else ""
+    suffix = f" {suffix}" if suffix else ""
+    return prefix + user_agent_string + suffix
+
+

Construct the user-agent header with the package info, +Python version and OS version.

+

Returns

+

The user agent string. +e.g. 'Python/3.7.17 slackclient/2.0.0 Darwin/17.7.0'

+
+
+
+
+
+
+ +
+ + + diff --git a/docs/reference/web/legacy_base_client.html b/docs/reference/web/legacy_base_client.html new file mode 100644 index 000000000..2abe94377 --- /dev/null +++ b/docs/reference/web/legacy_base_client.html @@ -0,0 +1,896 @@ + + + + + + +slack_sdk.web.legacy_base_client API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.web.legacy_base_client

+
+
+

A Python module for interacting with Slack's Web API.

+
+
+
+
+
+
+
+
+

Classes

+
+
+class LegacyBaseClient +(token: str | None = None,
base_url: str = 'https://slack.com/api/',
timeout: int = 30,
loop: asyncio.events.AbstractEventLoop | None = None,
ssl: ssl.SSLContext | None = None,
proxy: str | None = None,
run_async: bool = False,
use_sync_aiohttp: bool = False,
session: aiohttp.client.ClientSession | None = None,
headers: dict | None = None,
user_agent_prefix: str | None = None,
user_agent_suffix: str | None = None,
team_id: str | None = None,
logger: logging.Logger | None = None)
+
+
+
+ +Expand source code + +
class LegacyBaseClient:
+    BASE_URL = "https://slack.com/api/"
+
+    def __init__(
+        self,
+        token: Optional[str] = None,
+        base_url: str = BASE_URL,
+        timeout: int = 30,
+        loop: Optional[asyncio.AbstractEventLoop] = None,
+        ssl: Optional[SSLContext] = None,
+        proxy: Optional[str] = None,
+        run_async: bool = False,
+        use_sync_aiohttp: bool = False,
+        session: Optional[aiohttp.ClientSession] = None,
+        headers: Optional[dict] = None,
+        user_agent_prefix: Optional[str] = None,
+        user_agent_suffix: Optional[str] = None,
+        # for Org-Wide App installation
+        team_id: Optional[str] = None,
+        logger: Optional[logging.Logger] = None,
+    ):
+        self.token = None if token is None else token.strip()
+        """A string specifying an `xoxp-*` or `xoxb-*` token."""
+        if not base_url.endswith("/"):
+            base_url += "/"
+        self.base_url = base_url
+        """A string representing the Slack API base URL.
+        Default is `'https://slack.com/api/'`."""
+        self.timeout = timeout
+        """The maximum number of seconds the client will wait
+        to connect and receive a response from Slack.
+        Default is 30 seconds."""
+        self.ssl = ssl
+        """An [`ssl.SSLContext`](https://docs.python.org/3/library/ssl.html#ssl.SSLContext)
+        instance, helpful for specifying your own custom
+        certificate chain."""
+        self.proxy = proxy
+        """String representing a fully-qualified URL to a proxy through which
+        to route all requests to the Slack API. Even if this parameter
+        is not specified, if any of the following environment variables are
+        present, they will be loaded into this parameter: `HTTPS_PROXY`,
+        `https_proxy`, `HTTP_PROXY` or `http_proxy`."""
+        self.run_async = run_async
+        self.use_sync_aiohttp = use_sync_aiohttp
+        self.session = session
+        self.headers = headers or {}
+        """`dict` representing additional request headers to attach to all requests."""
+        self.headers["User-Agent"] = get_user_agent(user_agent_prefix, user_agent_suffix)
+        self.default_params = {}
+        if team_id is not None:
+            self.default_params["team_id"] = team_id
+        self._logger = logger if logger is not None else logging.getLogger(__name__)
+        if self.proxy is None or len(self.proxy.strip()) == 0:
+            env_variable = load_http_proxy_from_env(self._logger)
+            if env_variable is not None:
+                self.proxy = env_variable
+
+        self._event_loop = loop
+
+    def api_call(
+        self,
+        api_method: str,
+        *,
+        http_verb: str = "POST",
+        files: Optional[dict] = None,
+        data: Union[dict, FormData] = None,
+        params: Optional[dict] = None,
+        json: Optional[dict] = None,
+        headers: Optional[dict] = None,
+        auth: Optional[dict] = None,
+    ) -> Union[asyncio.Future, SlackResponse]:
+        """Create a request and execute the API call to Slack.
+        Args:
+            api_method (str): The target Slack API method.
+                e.g. 'chat.postMessage'
+            http_verb (str): HTTP Verb. e.g. 'POST'
+            files (dict): Files to multipart upload.
+                e.g. {image OR file: file_object OR file_path}
+            data: The body to attach to the request. If a dictionary is
+                provided, form-encoding will take place.
+                e.g. {'key1': 'value1', 'key2': 'value2'}
+            params (dict): The URL parameters to append to the URL.
+                e.g. {'key1': 'value1', 'key2': 'value2'}
+            json (dict): JSON for the body to attach to the request
+                (if files or data is not specified).
+                e.g. {'key1': 'value1', 'key2': 'value2'}
+            headers (dict): Additional request headers
+            auth (dict): A dictionary that consists of client_id and client_secret
+        Returns:
+            (SlackResponse)
+                The server's response to an HTTP request. Data
+                from the response can be accessed like a dict.
+                If the response included 'next_cursor' it can
+                be iterated on to execute subsequent requests.
+        Raises:
+            SlackApiError: The following Slack API call failed:
+                'chat.postMessage'.
+            SlackRequestError: Json data can only be submitted as
+                POST requests.
+        """
+
+        api_url = _get_url(self.base_url, api_method)
+
+        headers = headers or {}
+        headers.update(self.headers)
+
+        if auth is not None:
+            if isinstance(auth, dict):
+                auth = BasicAuth(auth["client_id"], auth["client_secret"])
+            elif isinstance(auth, BasicAuth):
+                headers["Authorization"] = auth.encode()
+
+        req_args = _build_req_args(
+            token=self.token,
+            http_verb=http_verb,
+            files=files,
+            data=data,
+            default_params=self.default_params,
+            params=params,
+            json=json,
+            headers=headers,
+            auth=auth,
+            ssl=self.ssl,
+            proxy=self.proxy,
+        )
+
+        show_deprecation_warning_if_any(api_method)
+
+        if self.run_async or self.use_sync_aiohttp:
+            if self._event_loop is None:
+                self._event_loop = _get_event_loop()
+
+            future = asyncio.ensure_future(
+                self._send(http_verb=http_verb, api_url=api_url, req_args=req_args),
+                loop=self._event_loop,
+            )
+            if self.run_async:
+                return future
+            if self.use_sync_aiohttp:
+                # Using this is no longer recommended - just keep this for backward-compatibility
+                return self._event_loop.run_until_complete(future)
+
+        return self._sync_send(api_url=api_url, req_args=req_args)
+
+    # =================================================================
+    # aiohttp based async WebClient
+    # =================================================================
+
+    async def _send(self, http_verb: str, api_url: str, req_args: dict) -> SlackResponse:
+        """Sends the request out for transmission.
+        Args:
+            http_verb (str): The HTTP verb. e.g. 'GET' or 'POST'.
+            api_url (str): The Slack API url. e.g. 'https://slack.com/api/chat.postMessage'
+            req_args (dict): The request arguments to be attached to the request.
+            e.g.
+            {
+                json: {
+                    'attachments': [{"pretext": "pre-hello", "text": "text-world"}],
+                    'channel': '#random'
+                }
+            }
+        Returns:
+            The response parsed into a SlackResponse object.
+        """
+        open_files = _files_to_data(req_args)
+        try:
+            if "params" in req_args:
+                # True/False -> "1"/"0"
+                req_args["params"] = convert_bool_to_0_or_1(req_args["params"])
+
+            res = await self._request(http_verb=http_verb, api_url=api_url, req_args=req_args)
+        finally:
+            for f in open_files:
+                f.close()
+
+        data = {
+            "client": self,
+            "http_verb": http_verb,
+            "api_url": api_url,
+            "req_args": req_args,
+            "use_sync_aiohttp": self.use_sync_aiohttp,
+        }
+        return SlackResponse(**{**data, **res}).validate()
+
+    async def _request(self, *, http_verb, api_url, req_args) -> Dict[str, Any]:
+        """Submit the HTTP request with the running session or a new session.
+        Returns:
+            A dictionary of the response data.
+        """
+        return await _request_with_session(
+            current_session=self.session,
+            timeout=self.timeout,
+            logger=self._logger,
+            http_verb=http_verb,
+            api_url=api_url,
+            req_args=req_args,
+        )
+
+    # =================================================================
+    # urllib based WebClient
+    # =================================================================
+
+    def _sync_send(self, api_url, req_args) -> SlackResponse:
+        params = req_args["params"] if "params" in req_args else None
+        data = req_args["data"] if "data" in req_args else None
+        files = req_args["files"] if "files" in req_args else None
+        _json = req_args["json"] if "json" in req_args else None
+        headers = req_args["headers"] if "headers" in req_args else None
+        token = params.get("token") if params and "token" in params else None
+        auth = req_args["auth"] if "auth" in req_args else None  # Basic Auth for oauth.v2.access / oauth.access
+        if auth is not None:
+            headers = {}
+            if isinstance(auth, BasicAuth):
+                headers["Authorization"] = auth.encode()
+            elif isinstance(auth, str):
+                headers["Authorization"] = auth
+            else:
+                self._logger.warning(f"As the auth: {auth}: {type(auth)} is unsupported, skipped")
+
+        body_params = {}
+        if params:
+            body_params.update(params)
+        if data:
+            body_params.update(data)
+
+        return self._urllib_api_call(
+            token=token,
+            url=api_url,
+            query_params={},
+            body_params=body_params,
+            files=files,
+            json_body=_json,
+            additional_headers=headers,
+        )
+
+    def _request_for_pagination(self, api_url: str, req_args: Dict[str, Dict[str, Any]]) -> Dict[str, Any]:
+        """This method is supposed to be used only for SlackResponse pagination
+        You can paginate using Python's for iterator as below:
+          for response in client.conversations_list(limit=100):
+              # do something with each response here
+        """
+        response = self._perform_urllib_http_request(url=api_url, args=req_args)
+        return {
+            "status_code": int(response["status"]),
+            "headers": dict(response["headers"]),
+            "data": json.loads(response["body"]),
+        }
+
+    def _urllib_api_call(
+        self,
+        *,
+        token: Optional[str] = None,
+        url: str,
+        query_params: Dict[str, str],
+        json_body: Dict,
+        body_params: Dict[str, str],
+        files: Dict[str, io.BytesIO],
+        additional_headers: Dict[str, str],
+    ) -> SlackResponse:
+        """Performs a Slack API request and returns the result.
+
+        Args:
+            token: Slack API Token (either bot token or user token)
+            url: Complete URL (e.g., https://slack.com/api/chat.postMessage)
+            query_params: Query string
+            json_body: JSON data structure (it's still a dict at this point),
+                if you give this argument, body_params and files will be skipped
+            body_params: Form body params
+            files: Files to upload
+            additional_headers: Request headers to append
+        Returns:
+            API response
+        """
+        files_to_close: List[BinaryIO] = []
+        try:
+            # True/False -> "1"/"0"
+            query_params = convert_bool_to_0_or_1(query_params)
+            body_params = convert_bool_to_0_or_1(body_params)
+
+            if self._logger.level <= logging.DEBUG:
+
+                def convert_params(values: dict) -> dict:
+                    if not values or not isinstance(values, dict):
+                        return {}
+                    return {k: ("(bytes)" if isinstance(v, bytes) else v) for k, v in values.items()}
+
+                headers = {k: "(redacted)" if k.lower() == "authorization" else v for k, v in additional_headers.items()}
+                self._logger.debug(
+                    f"Sending a request - url: {url}, "
+                    f"query_params: {convert_params(query_params)}, "
+                    f"body_params: {convert_params(body_params)}, "
+                    f"files: {convert_params(files)}, "
+                    f"json_body: {json_body}, "
+                    f"headers: {headers}"
+                )
+
+            request_data = {}
+            if files is not None and isinstance(files, dict) and len(files) > 0:
+                if body_params:
+                    for k, v in body_params.items():
+                        request_data.update({k: v})
+
+                for k, v in files.items():
+                    if isinstance(v, str):
+                        f: BinaryIO = open(v.encode("utf-8", "ignore"), "rb")
+                        files_to_close.append(f)
+                        request_data.update({k: f})
+                    elif isinstance(v, (bytearray, bytes)):
+                        request_data.update({k: io.BytesIO(v)})
+                    else:
+                        request_data.update({k: v})
+
+            request_headers = self._build_urllib_request_headers(
+                token=token or self.token,
+                has_json=json is not None,
+                has_files=files is not None,
+                additional_headers=additional_headers,
+            )
+            request_args = {
+                "headers": request_headers,
+                "data": request_data,
+                "params": body_params,
+                "files": files,
+                "json": json_body,
+            }
+            if query_params:
+                q = urlencode(query_params)
+                url = f"{url}&{q}" if "?" in url else f"{url}?{q}"
+
+            response = self._perform_urllib_http_request(url=url, args=request_args)
+            body = response.get("body", None)
+            response_body_data: Optional[Union[dict, bytes]] = body
+            if body is not None and not isinstance(body, bytes):
+                try:
+                    response_body_data = json.loads(response["body"])
+                except json.decoder.JSONDecodeError:
+                    message = _build_unexpected_body_error_message(response.get("body", ""))
+                    raise err.SlackApiError(message, response)
+
+            all_params: Dict[str, Any] = copy.copy(body_params) if body_params is not None else {}
+            if query_params:
+                all_params.update(query_params)
+            request_args["params"] = all_params  # for backward-compatibility
+
+            return SlackResponse(
+                client=self,
+                http_verb="POST",  # you can use POST method for all the Web APIs
+                api_url=url,
+                req_args=request_args,
+                data=response_body_data,
+                headers=dict(response["headers"]),
+                status_code=response["status"],
+                use_sync_aiohttp=False,
+            ).validate()
+        finally:
+            for f in files_to_close:
+                if not f.closed:
+                    f.close()
+
+    def _perform_urllib_http_request(self, *, url: str, args: Dict[str, Dict[str, Any]]) -> Dict[str, Any]:
+        """Performs an HTTP request and parses the response.
+
+        Args:
+            url: Complete URL (e.g., https://slack.com/api/chat.postMessage)
+            args: args has "headers", "data", "params", and "json"
+                "headers": Dict[str, str]
+                "data": Dict[str, Any]
+                "params": Dict[str, str],
+                "json": Dict[str, Any],
+
+        Returns:
+            dict {status: int, headers: Headers, body: str}
+        """
+        headers = args["headers"]
+        if args["json"]:
+            body = json.dumps(args["json"])
+            headers["Content-Type"] = "application/json;charset=utf-8"
+        elif args["data"]:
+            boundary = f"--------------{uuid.uuid4()}"
+            sep_boundary = b"\r\n--" + boundary.encode("ascii")
+            end_boundary = sep_boundary + b"--\r\n"
+            body = io.BytesIO()
+            data = args["data"]
+            for key, value in data.items():
+                readable = getattr(value, "readable", None)
+                if readable and value.readable():
+                    filename = "Uploaded file"
+                    name_attr = getattr(value, "name", None)
+                    if name_attr:
+                        filename = name_attr.decode("utf-8") if isinstance(name_attr, bytes) else name_attr
+                    if "filename" in data:
+                        filename = data["filename"]
+                    mimetype = mimetypes.guess_type(filename)[0] or "application/octet-stream"
+                    title = (
+                        f'\r\nContent-Disposition: form-data; name="{key}"; filename="{filename}"\r\n'
+                        + f"Content-Type: {mimetype}\r\n"
+                    )
+                    value = value.read()
+                else:
+                    title = f'\r\nContent-Disposition: form-data; name="{key}"\r\n'
+                    value = str(value).encode("utf-8")
+                body.write(sep_boundary)
+                body.write(title.encode("utf-8"))
+                body.write(b"\r\n")
+                body.write(value)
+
+            body.write(end_boundary)
+            body = body.getvalue()
+            headers["Content-Type"] = f"multipart/form-data; boundary={boundary}"
+            headers["Content-Length"] = len(body)
+        elif args["params"]:
+            body = urlencode(args["params"])
+            headers["Content-Type"] = "application/x-www-form-urlencoded"
+        else:
+            body = None
+
+        if isinstance(body, str):
+            body = body.encode("utf-8")
+
+        # NOTE: Intentionally ignore the `http_verb` here
+        # Slack APIs accepts any API method requests with POST methods
+        try:
+            # urllib not only opens http:// or https:// URLs, but also ftp:// and file://.
+            # With this it might be possible to open local files on the executing machine
+            # which might be a security risk if the URL to open can be manipulated by an external user.
+            # (BAN-B310)
+            if url.lower().startswith("http"):
+                req = Request(method="POST", url=url, data=body, headers=headers)
+                opener: Optional[OpenerDirector] = None
+                if self.proxy is not None:
+                    if isinstance(self.proxy, str):
+                        opener = urllib.request.build_opener(
+                            ProxyHandler({"http": self.proxy, "https": self.proxy}),
+                            HTTPSHandler(context=self.ssl),
+                        )
+                    else:
+                        raise SlackRequestError(f"Invalid proxy detected: {self.proxy} must be a str value")
+
+                # NOTE: BAN-B310 is already checked above
+                resp: Optional[HTTPResponse] = None
+                if opener:
+                    resp = opener.open(req, timeout=self.timeout)
+                else:
+                    resp = urlopen(req, context=self.ssl, timeout=self.timeout)
+                if resp.headers.get_content_type() == "application/gzip":
+                    # admin.analytics.getFile
+                    body: bytes = resp.read()
+                    return {"status": resp.code, "headers": resp.headers, "body": body}
+
+                charset = resp.headers.get_content_charset() or "utf-8"
+                body: str = resp.read().decode(charset)  # read the response body here
+                return {"status": resp.code, "headers": resp.headers, "body": body}
+            raise SlackRequestError(f"Invalid URL detected: {url}")
+        except HTTPError as e:
+            # As adding new values to HTTPError#headers can be ignored, building a new dict object here
+            response_headers = dict(e.headers.items())
+            resp = {"status": e.code, "headers": response_headers}
+            if e.code == 429:
+                # for compatibility with aiohttp
+                if "retry-after" not in response_headers and "Retry-After" in response_headers:
+                    response_headers["retry-after"] = response_headers["Retry-After"]
+                if "Retry-After" not in response_headers and "retry-after" in response_headers:
+                    response_headers["Retry-After"] = response_headers["retry-after"]
+
+            # read the response body here
+            charset = e.headers.get_content_charset() or "utf-8"
+            body: str = e.read().decode(charset)
+            resp["body"] = body
+            return resp
+
+        except Exception as err:
+            self._logger.error(f"Failed to send a request to Slack API server: {err}")
+            raise err
+
+    def _build_urllib_request_headers(
+        self, token: str, has_json: bool, has_files: bool, additional_headers: dict
+    ) -> Dict[str, str]:
+        headers = {"Content-Type": "application/x-www-form-urlencoded"}
+        headers.update(self.headers)
+        if token:
+            headers.update({"Authorization": "Bearer {}".format(token)})
+        if additional_headers:
+            headers.update(additional_headers)
+        if has_json:
+            headers.update({"Content-Type": "application/json;charset=utf-8"})
+        if has_files:
+            # will be set afterward
+            headers.pop("Content-Type", None)
+        return headers
+
+    def _upload_file(
+        self,
+        *,
+        url: str,
+        data: bytes,
+        logger: logging.Logger,
+        timeout: int,
+        proxy: Optional[str],
+        ssl: Optional[SSLContext],
+    ) -> FileUploadV2Result:
+        result = _upload_file_via_v2_url(
+            url=url,
+            data=data,
+            logger=logger,
+            timeout=timeout,
+            proxy=proxy,
+            ssl=ssl,
+        )
+        return FileUploadV2Result(
+            status=result.get("status"),
+            body=result.get("body"),
+        )
+
+    # =================================================================
+
+    @staticmethod
+    def validate_slack_signature(*, signing_secret: str, data: str, timestamp: str, signature: str) -> bool:
+        """
+        Slack creates a unique string for your app and shares it with you. Verify
+        requests from Slack with confidence by verifying signatures using your
+        signing secret.
+        On each HTTP request that Slack sends, we add an X-Slack-Signature HTTP
+        header. The signature is created by combining the signing secret with the
+        body of the request we're sending using a standard HMAC-SHA256 keyed hash.
+        https://docs.slack.dev/authentication/verifying-requests-from-slack/#how_to_make_a_request_signature_in_4_easy_steps__an_overview
+        Args:
+            signing_secret: Your application's signing secret, available in the
+                Slack API dashboard
+            data: The raw body of the incoming request - no headers, just the body.
+            timestamp: from the 'X-Slack-Request-Timestamp' header
+            signature: from the 'X-Slack-Signature' header - the calculated signature
+                should match this.
+        Returns:
+            True if signatures matches
+        """
+        warnings.warn(
+            "As this method is deprecated since slackclient 2.6.0, "
+            "use `from slack.signature import SignatureVerifier` instead",
+            DeprecationWarning,
+        )
+        format_req = str.encode(f"v0:{timestamp}:{data}")
+        encoded_secret = str.encode(signing_secret)
+        request_hash = hmac.new(encoded_secret, format_req, hashlib.sha256).hexdigest()
+        calculated_signature = f"v0={request_hash}"
+        return hmac.compare_digest(calculated_signature, signature)
+
+
+

Subclasses

+ +

Class variables

+
+
var BASE_URL
+
+

The type of the None singleton.

+
+
+

Static methods

+
+
+def validate_slack_signature(*, signing_secret: str, data: str, timestamp: str, signature: str) ‑> bool +
+
+
+ +Expand source code + +
@staticmethod
+def validate_slack_signature(*, signing_secret: str, data: str, timestamp: str, signature: str) -> bool:
+    """
+    Slack creates a unique string for your app and shares it with you. Verify
+    requests from Slack with confidence by verifying signatures using your
+    signing secret.
+    On each HTTP request that Slack sends, we add an X-Slack-Signature HTTP
+    header. The signature is created by combining the signing secret with the
+    body of the request we're sending using a standard HMAC-SHA256 keyed hash.
+    https://docs.slack.dev/authentication/verifying-requests-from-slack/#how_to_make_a_request_signature_in_4_easy_steps__an_overview
+    Args:
+        signing_secret: Your application's signing secret, available in the
+            Slack API dashboard
+        data: The raw body of the incoming request - no headers, just the body.
+        timestamp: from the 'X-Slack-Request-Timestamp' header
+        signature: from the 'X-Slack-Signature' header - the calculated signature
+            should match this.
+    Returns:
+        True if signatures matches
+    """
+    warnings.warn(
+        "As this method is deprecated since slackclient 2.6.0, "
+        "use `from slack.signature import SignatureVerifier` instead",
+        DeprecationWarning,
+    )
+    format_req = str.encode(f"v0:{timestamp}:{data}")
+    encoded_secret = str.encode(signing_secret)
+    request_hash = hmac.new(encoded_secret, format_req, hashlib.sha256).hexdigest()
+    calculated_signature = f"v0={request_hash}"
+    return hmac.compare_digest(calculated_signature, signature)
+
+

Slack creates a unique string for your app and shares it with you. Verify +requests from Slack with confidence by verifying signatures using your +signing secret. +On each HTTP request that Slack sends, we add an X-Slack-Signature HTTP +header. The signature is created by combining the signing secret with the +body of the request we're sending using a standard HMAC-SHA256 keyed hash. +https://docs.slack.dev/authentication/verifying-requests-from-slack/#how_to_make_a_request_signature_in_4_easy_steps__an_overview

+

Args

+
+
signing_secret
+
Your application's signing secret, available in the +Slack API dashboard
+
data
+
The raw body of the incoming request - no headers, just the body.
+
timestamp
+
from the 'X-Slack-Request-Timestamp' header
+
signature
+
from the 'X-Slack-Signature' header - the calculated signature +should match this.
+
+

Returns

+

True if signatures matches

+
+
+

Instance variables

+
+
var base_url
+
+

A string representing the Slack API base URL. +Default is 'https://slack.com/api/'.

+
+
var headers
+
+

dict representing additional request headers to attach to all requests.

+
+
var proxy
+
+

String representing a fully-qualified URL to a proxy through which +to route all requests to the Slack API. Even if this parameter +is not specified, if any of the following environment variables are +present, they will be loaded into this parameter: HTTPS_PROXY, +https_proxy, HTTP_PROXY or http_proxy.

+
+
var ssl
+
+

An ssl.SSLContext +instance, helpful for specifying your own custom +certificate chain.

+
+
var timeout
+
+

The maximum number of seconds the client will wait +to connect and receive a response from Slack. +Default is 30 seconds.

+
+
var token
+
+

A string specifying an xoxp-* or xoxb-* token.

+
+
+

Methods

+
+
+def api_call(self,
api_method: str,
*,
http_verb: str = 'POST',
files: dict | None = None,
data: dict | aiohttp.formdata.FormData = None,
params: dict | None = None,
json: dict | None = None,
headers: dict | None = None,
auth: dict | None = None) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def api_call(
+    self,
+    api_method: str,
+    *,
+    http_verb: str = "POST",
+    files: Optional[dict] = None,
+    data: Union[dict, FormData] = None,
+    params: Optional[dict] = None,
+    json: Optional[dict] = None,
+    headers: Optional[dict] = None,
+    auth: Optional[dict] = None,
+) -> Union[asyncio.Future, SlackResponse]:
+    """Create a request and execute the API call to Slack.
+    Args:
+        api_method (str): The target Slack API method.
+            e.g. 'chat.postMessage'
+        http_verb (str): HTTP Verb. e.g. 'POST'
+        files (dict): Files to multipart upload.
+            e.g. {image OR file: file_object OR file_path}
+        data: The body to attach to the request. If a dictionary is
+            provided, form-encoding will take place.
+            e.g. {'key1': 'value1', 'key2': 'value2'}
+        params (dict): The URL parameters to append to the URL.
+            e.g. {'key1': 'value1', 'key2': 'value2'}
+        json (dict): JSON for the body to attach to the request
+            (if files or data is not specified).
+            e.g. {'key1': 'value1', 'key2': 'value2'}
+        headers (dict): Additional request headers
+        auth (dict): A dictionary that consists of client_id and client_secret
+    Returns:
+        (SlackResponse)
+            The server's response to an HTTP request. Data
+            from the response can be accessed like a dict.
+            If the response included 'next_cursor' it can
+            be iterated on to execute subsequent requests.
+    Raises:
+        SlackApiError: The following Slack API call failed:
+            'chat.postMessage'.
+        SlackRequestError: Json data can only be submitted as
+            POST requests.
+    """
+
+    api_url = _get_url(self.base_url, api_method)
+
+    headers = headers or {}
+    headers.update(self.headers)
+
+    if auth is not None:
+        if isinstance(auth, dict):
+            auth = BasicAuth(auth["client_id"], auth["client_secret"])
+        elif isinstance(auth, BasicAuth):
+            headers["Authorization"] = auth.encode()
+
+    req_args = _build_req_args(
+        token=self.token,
+        http_verb=http_verb,
+        files=files,
+        data=data,
+        default_params=self.default_params,
+        params=params,
+        json=json,
+        headers=headers,
+        auth=auth,
+        ssl=self.ssl,
+        proxy=self.proxy,
+    )
+
+    show_deprecation_warning_if_any(api_method)
+
+    if self.run_async or self.use_sync_aiohttp:
+        if self._event_loop is None:
+            self._event_loop = _get_event_loop()
+
+        future = asyncio.ensure_future(
+            self._send(http_verb=http_verb, api_url=api_url, req_args=req_args),
+            loop=self._event_loop,
+        )
+        if self.run_async:
+            return future
+        if self.use_sync_aiohttp:
+            # Using this is no longer recommended - just keep this for backward-compatibility
+            return self._event_loop.run_until_complete(future)
+
+    return self._sync_send(api_url=api_url, req_args=req_args)
+
+

Create a request and execute the API call to Slack.

+

Args

+
+
api_method : str
+
The target Slack API method. +e.g. 'chat.postMessage'
+
http_verb : str
+
HTTP Verb. e.g. 'POST'
+
files : dict
+
Files to multipart upload. +e.g. {image OR file: file_object OR file_path}
+
data
+
The body to attach to the request. If a dictionary is +provided, form-encoding will take place. +e.g. {'key1': 'value1', 'key2': 'value2'}
+
params : dict
+
The URL parameters to append to the URL. +e.g. {'key1': 'value1', 'key2': 'value2'}
+
json : dict
+
JSON for the body to attach to the request +(if files or data is not specified). +e.g. {'key1': 'value1', 'key2': 'value2'}
+
headers : dict
+
Additional request headers
+
auth : dict
+
A dictionary that consists of client_id and client_secret
+
+

Returns

+

(SlackResponse) +The server's response to an HTTP request. Data +from the response can be accessed like a dict. +If the response included 'next_cursor' it can +be iterated on to execute subsequent requests.

+

Raises

+
+
SlackApiError
+
The following Slack API call failed: +'chat.postMessage'.
+
SlackRequestError
+
Json data can only be submitted as +POST requests.
+
+
+
+
+
+
+
+ +
+ + + diff --git a/docs/reference/web/legacy_client.html b/docs/reference/web/legacy_client.html new file mode 100644 index 000000000..d100178dc --- /dev/null +++ b/docs/reference/web/legacy_client.html @@ -0,0 +1,15605 @@ + + + + + + +slack_sdk.web.legacy_client API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.web.legacy_client

+
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class LegacyWebClient +(token: str | None = None,
base_url: str = 'https://slack.com/api/',
timeout: int = 30,
loop: asyncio.events.AbstractEventLoop | None = None,
ssl: ssl.SSLContext | None = None,
proxy: str | None = None,
run_async: bool = False,
use_sync_aiohttp: bool = False,
session: aiohttp.client.ClientSession | None = None,
headers: dict | None = None,
user_agent_prefix: str | None = None,
user_agent_suffix: str | None = None,
team_id: str | None = None,
logger: logging.Logger | None = None)
+
+
+
+ +Expand source code + +
class LegacyWebClient(LegacyBaseClient):
+    """A WebClient allows apps to communicate with the Slack Platform's Web API.
+
+    https://docs.slack.dev/reference/methods
+
+    The Slack Web API is an interface for querying information from
+    and enacting change in a Slack workspace.
+
+    This client handles constructing and sending HTTP requests to Slack
+    as well as parsing any responses received into a `SlackResponse`.
+
+    Attributes:
+        token (str): A string specifying an `xoxp-*` or `xoxb-*` token.
+        base_url (str): A string representing the Slack API base URL.
+            Default is `'https://slack.com/api/'`
+        timeout (int): The maximum number of seconds the client will wait
+            to connect and receive a response from Slack.
+            Default is 30 seconds.
+        ssl (SSLContext): An [`ssl.SSLContext`][1] instance, helpful for specifying
+            your own custom certificate chain.
+        proxy (str): String representing a fully-qualified URL to a proxy through
+            which to route all requests to the Slack API. Even if this parameter
+            is not specified, if any of the following environment variables are
+            present, they will be loaded into this parameter: `HTTPS_PROXY`,
+            `https_proxy`, `HTTP_PROXY` or `http_proxy`.
+        headers (dict): Additional request headers to attach to all requests.
+
+    Methods:
+        `api_call`: Constructs a request and executes the API call to Slack.
+
+    Example of recommended usage:
+    ```python
+        import os
+        from slack_sdk.web.legacy_client import LegacyWebClient
+
+        client = LegacyWebClient(token=os.environ['SLACK_API_TOKEN'])
+        response = client.chat_postMessage(
+            channel='#random',
+            text="Hello world!")
+        assert response["ok"]
+        assert response["message"]["text"] == "Hello world!"
+    ```
+
+    Example manually creating an API request:
+    ```python
+        import os
+        from slack_sdk.web.legacy_client import LegacyWebClient
+
+        client = LegacyWebClient(token=os.environ['SLACK_API_TOKEN'])
+        response = client.api_call(
+            api_method='chat.postMessage',
+            json={'channel': '#random','text': "Hello world!"}
+        )
+        assert response["ok"]
+        assert response["message"]["text"] == "Hello world!"
+    ```
+
+    Note:
+        Any attributes or methods prefixed with _underscores are
+        intended to be "private" internal use only. They may be changed or
+        removed at anytime.
+
+    [1]: https://docs.python.org/3/library/ssl.html#ssl.SSLContext
+    """
+
+    def admin_analytics_getFile(
+        self,
+        *,
+        type: str,
+        date: Optional[str] = None,
+        metadata_only: Optional[bool] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Retrieve analytics data for a given date, presented as a compressed JSON file
+        https://docs.slack.dev/reference/methods/admin.analytics.getFile
+        """
+        kwargs.update({"type": type})
+        if date is not None:
+            kwargs.update({"date": date})
+        if metadata_only is not None:
+            kwargs.update({"metadata_only": metadata_only})
+        return self.api_call("admin.analytics.getFile", params=kwargs)
+
+    def admin_apps_approve(
+        self,
+        *,
+        app_id: Optional[str] = None,
+        request_id: Optional[str] = None,
+        enterprise_id: Optional[str] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Approve an app for installation on a workspace.
+        Either app_id or request_id is required.
+        These IDs can be obtained either directly via the app_requested event,
+        or by the admin.apps.requests.list method.
+        https://docs.slack.dev/reference/methods/admin.apps.approve
+        """
+        if app_id:
+            kwargs.update({"app_id": app_id})
+        elif request_id:
+            kwargs.update({"request_id": request_id})
+        else:
+            raise e.SlackRequestError("The app_id or request_id argument must be specified.")
+
+        kwargs.update(
+            {
+                "enterprise_id": enterprise_id,
+                "team_id": team_id,
+            }
+        )
+        return self.api_call("admin.apps.approve", params=kwargs)
+
+    def admin_apps_approved_list(
+        self,
+        *,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        enterprise_id: Optional[str] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """List approved apps for an org or workspace.
+        https://docs.slack.dev/reference/methods/admin.apps.approved.list
+        """
+        kwargs.update(
+            {
+                "cursor": cursor,
+                "limit": limit,
+                "enterprise_id": enterprise_id,
+                "team_id": team_id,
+            }
+        )
+        return self.api_call("admin.apps.approved.list", http_verb="GET", params=kwargs)
+
+    def admin_apps_clearResolution(
+        self,
+        *,
+        app_id: str,
+        enterprise_id: Optional[str] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Clear an app resolution
+        https://docs.slack.dev/reference/methods/admin.apps.clearResolution
+        """
+        kwargs.update(
+            {
+                "app_id": app_id,
+                "enterprise_id": enterprise_id,
+                "team_id": team_id,
+            }
+        )
+        return self.api_call("admin.apps.clearResolution", http_verb="POST", params=kwargs)
+
+    def admin_apps_requests_cancel(
+        self,
+        *,
+        request_id: str,
+        enterprise_id: Optional[str] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """List app requests for a team/workspace.
+        https://docs.slack.dev/reference/methods/admin.apps.requests.cancel
+        """
+        kwargs.update(
+            {
+                "request_id": request_id,
+                "enterprise_id": enterprise_id,
+                "team_id": team_id,
+            }
+        )
+        return self.api_call("admin.apps.requests.cancel", http_verb="POST", params=kwargs)
+
+    def admin_apps_requests_list(
+        self,
+        *,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """List app requests for a team/workspace.
+        https://docs.slack.dev/reference/methods/admin.apps.requests.list
+        """
+        kwargs.update(
+            {
+                "cursor": cursor,
+                "limit": limit,
+                "team_id": team_id,
+            }
+        )
+        return self.api_call("admin.apps.requests.list", http_verb="GET", params=kwargs)
+
+    def admin_apps_restrict(
+        self,
+        *,
+        app_id: Optional[str] = None,
+        request_id: Optional[str] = None,
+        enterprise_id: Optional[str] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Restrict an app for installation on a workspace.
+        Exactly one of the team_id or enterprise_id arguments is required, not both.
+        Either app_id or request_id is required. These IDs can be obtained either directly
+        via the app_requested event, or by the admin.apps.requests.list method.
+        https://docs.slack.dev/reference/methods/admin.apps.restrict
+        """
+        if app_id:
+            kwargs.update({"app_id": app_id})
+        elif request_id:
+            kwargs.update({"request_id": request_id})
+        else:
+            raise e.SlackRequestError("The app_id or request_id argument must be specified.")
+
+        kwargs.update(
+            {
+                "enterprise_id": enterprise_id,
+                "team_id": team_id,
+            }
+        )
+        return self.api_call("admin.apps.restrict", params=kwargs)
+
+    def admin_apps_restricted_list(
+        self,
+        *,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        enterprise_id: Optional[str] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """List restricted apps for an org or workspace.
+        https://docs.slack.dev/reference/methods/admin.apps.restricted.list
+        """
+        kwargs.update(
+            {
+                "cursor": cursor,
+                "limit": limit,
+                "enterprise_id": enterprise_id,
+                "team_id": team_id,
+            }
+        )
+        return self.api_call("admin.apps.restricted.list", http_verb="GET", params=kwargs)
+
+    def admin_apps_uninstall(
+        self,
+        *,
+        app_id: str,
+        enterprise_id: Optional[str] = None,
+        team_ids: Optional[Union[str, Sequence[str]]] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Uninstall an app from one or many workspaces, or an entire enterprise organization.
+        With an org-level token, enterprise_id or team_ids is required.
+        https://docs.slack.dev/reference/methods/admin.apps.uninstall
+        """
+        kwargs.update({"app_id": app_id})
+        if enterprise_id is not None:
+            kwargs.update({"enterprise_id": enterprise_id})
+        if team_ids is not None:
+            if isinstance(team_ids, (list, tuple)):
+                kwargs.update({"team_ids": ",".join(team_ids)})
+            else:
+                kwargs.update({"team_ids": team_ids})
+        return self.api_call("admin.apps.uninstall", http_verb="POST", params=kwargs)
+
+    def admin_apps_activities_list(
+        self,
+        *,
+        app_id: Optional[str] = None,
+        component_id: Optional[str] = None,
+        component_type: Optional[str] = None,
+        log_event_type: Optional[str] = None,
+        max_date_created: Optional[int] = None,
+        min_date_created: Optional[int] = None,
+        min_log_level: Optional[str] = None,
+        sort_direction: Optional[str] = None,
+        source: Optional[str] = None,
+        team_id: Optional[str] = None,
+        trace_id: Optional[str] = None,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Get logs for a specified team/org
+        https://docs.slack.dev/reference/methods/admin.apps.activities.list
+        """
+        kwargs.update(
+            {
+                "app_id": app_id,
+                "component_id": component_id,
+                "component_type": component_type,
+                "log_event_type": log_event_type,
+                "max_date_created": max_date_created,
+                "min_date_created": min_date_created,
+                "min_log_level": min_log_level,
+                "sort_direction": sort_direction,
+                "source": source,
+                "team_id": team_id,
+                "trace_id": trace_id,
+                "cursor": cursor,
+                "limit": limit,
+            }
+        )
+        return self.api_call("admin.apps.activities.list", params=kwargs)
+
+    def admin_apps_config_lookup(
+        self,
+        *,
+        app_ids: Union[str, Sequence[str]],
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Look up the app config for connectors by their IDs
+        https://docs.slack.dev/reference/methods/admin.apps.config.lookup
+        """
+        if isinstance(app_ids, (list, tuple)):
+            kwargs.update({"app_ids": ",".join(app_ids)})
+        else:
+            kwargs.update({"app_ids": app_ids})
+        return self.api_call("admin.apps.config.lookup", params=kwargs)
+
+    def admin_apps_config_set(
+        self,
+        *,
+        app_id: str,
+        domain_restrictions: Optional[Dict[str, Any]] = None,
+        workflow_auth_strategy: Optional[str] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Set the app config for a connector
+        https://docs.slack.dev/reference/methods/admin.apps.config.set
+        """
+        kwargs.update(
+            {
+                "app_id": app_id,
+                "workflow_auth_strategy": workflow_auth_strategy,
+            }
+        )
+        if domain_restrictions is not None:
+            kwargs.update({"domain_restrictions": json.dumps(domain_restrictions)})
+        return self.api_call("admin.apps.config.set", params=kwargs)
+
+    def admin_auth_policy_getEntities(
+        self,
+        *,
+        policy_name: str,
+        cursor: Optional[str] = None,
+        entity_type: Optional[str] = None,
+        limit: Optional[int] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Fetch all the entities assigned to a particular authentication policy by name.
+        https://docs.slack.dev/reference/methods/admin.auth.policy.getEntities
+        """
+        kwargs.update({"policy_name": policy_name})
+        if cursor is not None:
+            kwargs.update({"cursor": cursor})
+        if entity_type is not None:
+            kwargs.update({"entity_type": entity_type})
+        if limit is not None:
+            kwargs.update({"limit": limit})
+        return self.api_call("admin.auth.policy.getEntities", http_verb="POST", params=kwargs)
+
+    def admin_auth_policy_assignEntities(
+        self,
+        *,
+        entity_ids: Union[str, Sequence[str]],
+        policy_name: str,
+        entity_type: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Assign entities to a particular authentication policy.
+        https://docs.slack.dev/reference/methods/admin.auth.policy.assignEntities
+        """
+        if isinstance(entity_ids, (list, tuple)):
+            kwargs.update({"entity_ids": ",".join(entity_ids)})
+        else:
+            kwargs.update({"entity_ids": entity_ids})
+        kwargs.update({"policy_name": policy_name})
+        kwargs.update({"entity_type": entity_type})
+        return self.api_call("admin.auth.policy.assignEntities", http_verb="POST", params=kwargs)
+
+    def admin_auth_policy_removeEntities(
+        self,
+        *,
+        entity_ids: Union[str, Sequence[str]],
+        policy_name: str,
+        entity_type: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Remove specified entities from a specified authentication policy.
+        https://docs.slack.dev/reference/methods/admin.auth.policy.removeEntities
+        """
+        if isinstance(entity_ids, (list, tuple)):
+            kwargs.update({"entity_ids": ",".join(entity_ids)})
+        else:
+            kwargs.update({"entity_ids": entity_ids})
+        kwargs.update({"policy_name": policy_name})
+        kwargs.update({"entity_type": entity_type})
+        return self.api_call("admin.auth.policy.removeEntities", http_verb="POST", params=kwargs)
+
+    def admin_conversations_createForObjects(
+        self,
+        *,
+        object_id: str,
+        salesforce_org_id: str,
+        invite_object_team: Optional[bool] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Create a Salesforce channel for the corresponding object provided.
+        https://docs.slack.dev/reference/methods/admin.conversations.createForObjects
+        """
+        kwargs.update(
+            {"object_id": object_id, "salesforce_org_id": salesforce_org_id, "invite_object_team": invite_object_team}
+        )
+        return self.api_call("admin.conversations.createForObjects", params=kwargs)
+
+    def admin_conversations_linkObjects(
+        self,
+        *,
+        channel: str,
+        record_id: str,
+        salesforce_org_id: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Link a Salesforce record to a channel.
+        https://docs.slack.dev/reference/methods/admin.conversations.linkObjects
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "record_id": record_id,
+                "salesforce_org_id": salesforce_org_id,
+            }
+        )
+        return self.api_call("admin.conversations.linkObjects", params=kwargs)
+
+    def admin_conversations_unlinkObjects(
+        self,
+        *,
+        channel: str,
+        new_name: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Unlink a Salesforce record from a channel.
+        https://docs.slack.dev/reference/methods/admin.conversations.unlinkObjects
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "new_name": new_name,
+            }
+        )
+        return self.api_call("admin.conversations.unlinkObjects", params=kwargs)
+
+    def admin_barriers_create(
+        self,
+        *,
+        barriered_from_usergroup_ids: Union[str, Sequence[str]],
+        primary_usergroup_id: str,
+        restricted_subjects: Union[str, Sequence[str]],
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Create an Information Barrier
+        https://docs.slack.dev/reference/methods/admin.barriers.create
+        """
+        kwargs.update({"primary_usergroup_id": primary_usergroup_id})
+        if isinstance(barriered_from_usergroup_ids, (list, tuple)):
+            kwargs.update({"barriered_from_usergroup_ids": ",".join(barriered_from_usergroup_ids)})
+        else:
+            kwargs.update({"barriered_from_usergroup_ids": barriered_from_usergroup_ids})
+        if isinstance(restricted_subjects, (list, tuple)):
+            kwargs.update({"restricted_subjects": ",".join(restricted_subjects)})
+        else:
+            kwargs.update({"restricted_subjects": restricted_subjects})
+        return self.api_call("admin.barriers.create", http_verb="POST", params=kwargs)
+
+    def admin_barriers_delete(
+        self,
+        *,
+        barrier_id: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Delete an existing Information Barrier
+        https://docs.slack.dev/reference/methods/admin.barriers.delete
+        """
+        kwargs.update({"barrier_id": barrier_id})
+        return self.api_call("admin.barriers.delete", http_verb="POST", params=kwargs)
+
+    def admin_barriers_update(
+        self,
+        *,
+        barrier_id: str,
+        barriered_from_usergroup_ids: Union[str, Sequence[str]],
+        primary_usergroup_id: str,
+        restricted_subjects: Union[str, Sequence[str]],
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Update an existing Information Barrier
+        https://docs.slack.dev/reference/methods/admin.barriers.update
+        """
+        kwargs.update({"barrier_id": barrier_id, "primary_usergroup_id": primary_usergroup_id})
+        if isinstance(barriered_from_usergroup_ids, (list, tuple)):
+            kwargs.update({"barriered_from_usergroup_ids": ",".join(barriered_from_usergroup_ids)})
+        else:
+            kwargs.update({"barriered_from_usergroup_ids": barriered_from_usergroup_ids})
+        if isinstance(restricted_subjects, (list, tuple)):
+            kwargs.update({"restricted_subjects": ",".join(restricted_subjects)})
+        else:
+            kwargs.update({"restricted_subjects": restricted_subjects})
+        return self.api_call("admin.barriers.update", http_verb="POST", params=kwargs)
+
+    def admin_barriers_list(
+        self,
+        *,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Get all Information Barriers for your organization
+        https://docs.slack.dev/reference/methods/admin.barriers.list"""
+        kwargs.update(
+            {
+                "cursor": cursor,
+                "limit": limit,
+            }
+        )
+        return self.api_call("admin.barriers.list", http_verb="GET", params=kwargs)
+
+    def admin_conversations_create(
+        self,
+        *,
+        is_private: bool,
+        name: str,
+        description: Optional[str] = None,
+        org_wide: Optional[bool] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Create a public or private channel-based conversation.
+        https://docs.slack.dev/reference/methods/admin.conversations.create
+        """
+        kwargs.update(
+            {
+                "is_private": is_private,
+                "name": name,
+                "description": description,
+                "org_wide": org_wide,
+                "team_id": team_id,
+            }
+        )
+        return self.api_call("admin.conversations.create", params=kwargs)
+
+    def admin_conversations_delete(
+        self,
+        *,
+        channel_id: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Delete a public or private channel.
+        https://docs.slack.dev/reference/methods/admin.conversations.delete
+        """
+        kwargs.update({"channel_id": channel_id})
+        return self.api_call("admin.conversations.delete", params=kwargs)
+
+    def admin_conversations_invite(
+        self,
+        *,
+        channel_id: str,
+        user_ids: Union[str, Sequence[str]],
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Invite a user to a public or private channel.
+        https://docs.slack.dev/reference/methods/admin.conversations.invite
+        """
+        kwargs.update({"channel_id": channel_id})
+        if isinstance(user_ids, (list, tuple)):
+            kwargs.update({"user_ids": ",".join(user_ids)})
+        else:
+            kwargs.update({"user_ids": user_ids})
+        # NOTE: the endpoint is unable to handle Content-Type: application/json as of Sep 3, 2020.
+        return self.api_call("admin.conversations.invite", params=kwargs)
+
+    def admin_conversations_archive(
+        self,
+        *,
+        channel_id: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Archive a public or private channel.
+        https://docs.slack.dev/reference/methods/admin.conversations.archive
+        """
+        kwargs.update({"channel_id": channel_id})
+        return self.api_call("admin.conversations.archive", params=kwargs)
+
+    def admin_conversations_unarchive(
+        self,
+        *,
+        channel_id: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Unarchive a public or private channel.
+        https://docs.slack.dev/reference/methods/admin.conversations.archive
+        """
+        kwargs.update({"channel_id": channel_id})
+        return self.api_call("admin.conversations.unarchive", params=kwargs)
+
+    def admin_conversations_rename(
+        self,
+        *,
+        channel_id: str,
+        name: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Rename a public or private channel.
+        https://docs.slack.dev/reference/methods/admin.conversations.rename
+        """
+        kwargs.update({"channel_id": channel_id, "name": name})
+        return self.api_call("admin.conversations.rename", params=kwargs)
+
+    def admin_conversations_search(
+        self,
+        *,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        query: Optional[str] = None,
+        search_channel_types: Optional[Union[str, Sequence[str]]] = None,
+        sort: Optional[str] = None,
+        sort_dir: Optional[str] = None,
+        team_ids: Optional[Union[str, Sequence[str]]] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Search for public or private channels in an Enterprise organization.
+        https://docs.slack.dev/reference/methods/admin.conversations.search
+        """
+        kwargs.update(
+            {
+                "cursor": cursor,
+                "limit": limit,
+                "query": query,
+                "sort": sort,
+                "sort_dir": sort_dir,
+            }
+        )
+
+        if isinstance(search_channel_types, (list, tuple)):
+            kwargs.update({"search_channel_types": ",".join(search_channel_types)})
+        else:
+            kwargs.update({"search_channel_types": search_channel_types})
+
+        if isinstance(team_ids, (list, tuple)):
+            kwargs.update({"team_ids": ",".join(team_ids)})
+        else:
+            kwargs.update({"team_ids": team_ids})
+
+        return self.api_call("admin.conversations.search", params=kwargs)
+
+    def admin_conversations_convertToPrivate(
+        self,
+        *,
+        channel_id: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Convert a public channel to a private channel.
+        https://docs.slack.dev/reference/methods/admin.conversations.convertToPrivate
+        """
+        kwargs.update({"channel_id": channel_id})
+        return self.api_call("admin.conversations.convertToPrivate", params=kwargs)
+
+    def admin_conversations_convertToPublic(
+        self,
+        *,
+        channel_id: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Convert a privte channel to a public channel.
+        https://docs.slack.dev/reference/methods/admin.conversations.convertToPublic
+        """
+        kwargs.update({"channel_id": channel_id})
+        return self.api_call("admin.conversations.convertToPublic", params=kwargs)
+
+    def admin_conversations_setConversationPrefs(
+        self,
+        *,
+        channel_id: str,
+        prefs: Union[str, Dict[str, str]],
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Set the posting permissions for a public or private channel.
+        https://docs.slack.dev/reference/methods/admin.conversations.setConversationPrefs
+        """
+        kwargs.update({"channel_id": channel_id})
+        if isinstance(prefs, dict):
+            kwargs.update({"prefs": json.dumps(prefs)})
+        else:
+            kwargs.update({"prefs": prefs})
+        return self.api_call("admin.conversations.setConversationPrefs", params=kwargs)
+
+    def admin_conversations_getConversationPrefs(
+        self,
+        *,
+        channel_id: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Get conversation preferences for a public or private channel.
+        https://docs.slack.dev/reference/methods/admin.conversations.getConversationPrefs
+        """
+        kwargs.update({"channel_id": channel_id})
+        return self.api_call("admin.conversations.getConversationPrefs", params=kwargs)
+
+    def admin_conversations_disconnectShared(
+        self,
+        *,
+        channel_id: str,
+        leaving_team_ids: Optional[Union[str, Sequence[str]]] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Disconnect a connected channel from one or more workspaces.
+        https://docs.slack.dev/reference/methods/admin.conversations.disconnectShared
+        """
+        kwargs.update({"channel_id": channel_id})
+        if isinstance(leaving_team_ids, (list, tuple)):
+            kwargs.update({"leaving_team_ids": ",".join(leaving_team_ids)})
+        else:
+            kwargs.update({"leaving_team_ids": leaving_team_ids})
+        return self.api_call("admin.conversations.disconnectShared", params=kwargs)
+
+    def admin_conversations_lookup(
+        self,
+        *,
+        last_message_activity_before: int,
+        team_ids: Union[str, Sequence[str]],
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        max_member_count: Optional[int] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Returns channels on the given team using the filters.
+        https://docs.slack.dev/reference/methods/admin.conversations.lookup
+        """
+        kwargs.update(
+            {
+                "last_message_activity_before": last_message_activity_before,
+                "cursor": cursor,
+                "limit": limit,
+                "max_member_count": max_member_count,
+            }
+        )
+        if isinstance(team_ids, (list, tuple)):
+            kwargs.update({"team_ids": ",".join(team_ids)})
+        else:
+            kwargs.update({"team_ids": team_ids})
+        return self.api_call("admin.conversations.lookup", params=kwargs)
+
+    def admin_conversations_ekm_listOriginalConnectedChannelInfo(
+        self,
+        *,
+        channel_ids: Optional[Union[str, Sequence[str]]] = None,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        team_ids: Optional[Union[str, Sequence[str]]] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """List all disconnected channels—i.e.,
+        channels that were once connected to other workspaces and then disconnected—and
+        the corresponding original channel IDs for key revocation with EKM.
+        https://docs.slack.dev/reference/methods/admin.conversations.ekm.listOriginalConnectedChannelInfo
+        """
+        kwargs.update(
+            {
+                "cursor": cursor,
+                "limit": limit,
+            }
+        )
+        if isinstance(channel_ids, (list, tuple)):
+            kwargs.update({"channel_ids": ",".join(channel_ids)})
+        else:
+            kwargs.update({"channel_ids": channel_ids})
+        if isinstance(team_ids, (list, tuple)):
+            kwargs.update({"team_ids": ",".join(team_ids)})
+        else:
+            kwargs.update({"team_ids": team_ids})
+        return self.api_call("admin.conversations.ekm.listOriginalConnectedChannelInfo", params=kwargs)
+
+    def admin_conversations_restrictAccess_addGroup(
+        self,
+        *,
+        channel_id: str,
+        group_id: str,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Add an allowlist of IDP groups for accessing a channel.
+        https://docs.slack.dev/reference/methods/admin.conversations.restrictAccess.addGroup
+        """
+        kwargs.update(
+            {
+                "channel_id": channel_id,
+                "group_id": group_id,
+                "team_id": team_id,
+            }
+        )
+        return self.api_call(
+            "admin.conversations.restrictAccess.addGroup",
+            http_verb="GET",
+            params=kwargs,
+        )
+
+    def admin_conversations_restrictAccess_listGroups(
+        self,
+        *,
+        channel_id: str,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """List all IDP Groups linked to a channel.
+        https://docs.slack.dev/reference/methods/admin.conversations.restrictAccess.listGroups
+        """
+        kwargs.update(
+            {
+                "channel_id": channel_id,
+                "team_id": team_id,
+            }
+        )
+        return self.api_call(
+            "admin.conversations.restrictAccess.listGroups",
+            http_verb="GET",
+            params=kwargs,
+        )
+
+    def admin_conversations_restrictAccess_removeGroup(
+        self,
+        *,
+        channel_id: str,
+        group_id: str,
+        team_id: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Remove a linked IDP group linked from a private channel.
+        https://docs.slack.dev/reference/methods/admin.conversations.restrictAccess.removeGroup
+        """
+        kwargs.update(
+            {
+                "channel_id": channel_id,
+                "group_id": group_id,
+                "team_id": team_id,
+            }
+        )
+        return self.api_call(
+            "admin.conversations.restrictAccess.removeGroup",
+            http_verb="GET",
+            params=kwargs,
+        )
+
+    def admin_conversations_setTeams(
+        self,
+        *,
+        channel_id: str,
+        org_channel: Optional[bool] = None,
+        target_team_ids: Optional[Union[str, Sequence[str]]] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Set the workspaces in an Enterprise grid org that connect to a public or private channel.
+        https://docs.slack.dev/reference/methods/admin.conversations.setTeams
+        """
+        kwargs.update(
+            {
+                "channel_id": channel_id,
+                "org_channel": org_channel,
+                "team_id": team_id,
+            }
+        )
+        if isinstance(target_team_ids, (list, tuple)):
+            kwargs.update({"target_team_ids": ",".join(target_team_ids)})
+        else:
+            kwargs.update({"target_team_ids": target_team_ids})
+        return self.api_call("admin.conversations.setTeams", params=kwargs)
+
+    def admin_conversations_getTeams(
+        self,
+        *,
+        channel_id: str,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Set the workspaces in an Enterprise grid org that connect to a channel.
+        https://docs.slack.dev/reference/methods/admin.conversations.getTeams
+        """
+        kwargs.update(
+            {
+                "channel_id": channel_id,
+                "cursor": cursor,
+                "limit": limit,
+            }
+        )
+        return self.api_call("admin.conversations.getTeams", params=kwargs)
+
+    def admin_conversations_getCustomRetention(
+        self,
+        *,
+        channel_id: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Get a channel's retention policy
+        https://docs.slack.dev/reference/methods/admin.conversations.getCustomRetention
+        """
+        kwargs.update({"channel_id": channel_id})
+        return self.api_call("admin.conversations.getCustomRetention", params=kwargs)
+
+    def admin_conversations_removeCustomRetention(
+        self,
+        *,
+        channel_id: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Remove a channel's retention policy
+        https://docs.slack.dev/reference/methods/admin.conversations.removeCustomRetention
+        """
+        kwargs.update({"channel_id": channel_id})
+        return self.api_call("admin.conversations.removeCustomRetention", params=kwargs)
+
+    def admin_conversations_setCustomRetention(
+        self,
+        *,
+        channel_id: str,
+        duration_days: int,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Set a channel's retention policy
+        https://docs.slack.dev/reference/methods/admin.conversations.setCustomRetention
+        """
+        kwargs.update({"channel_id": channel_id, "duration_days": duration_days})
+        return self.api_call("admin.conversations.setCustomRetention", params=kwargs)
+
+    def admin_conversations_bulkArchive(
+        self,
+        *,
+        channel_ids: Union[Sequence[str], str],
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Archive public or private channels in bulk.
+        https://docs.slack.dev/reference/methods/admin.conversations.bulkArchive
+        """
+        kwargs.update({"channel_ids": ",".join(channel_ids) if isinstance(channel_ids, (list, tuple)) else channel_ids})
+        return self.api_call("admin.conversations.bulkArchive", params=kwargs)
+
+    def admin_conversations_bulkDelete(
+        self,
+        *,
+        channel_ids: Union[Sequence[str], str],
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Delete public or private channels in bulk.
+        https://slack.com/api/admin.conversations.bulkDelete
+        """
+        kwargs.update({"channel_ids": ",".join(channel_ids) if isinstance(channel_ids, (list, tuple)) else channel_ids})
+        return self.api_call("admin.conversations.bulkDelete", params=kwargs)
+
+    def admin_conversations_bulkMove(
+        self,
+        *,
+        channel_ids: Union[Sequence[str], str],
+        target_team_id: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Move public or private channels in bulk.
+        https://docs.slack.dev/reference/methods/admin.conversations.bulkMove
+        """
+        kwargs.update(
+            {
+                "target_team_id": target_team_id,
+                "channel_ids": ",".join(channel_ids) if isinstance(channel_ids, (list, tuple)) else channel_ids,
+            }
+        )
+        return self.api_call("admin.conversations.bulkMove", params=kwargs)
+
+    def admin_emoji_add(
+        self,
+        *,
+        name: str,
+        url: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Add an emoji.
+        https://docs.slack.dev/reference/methods/admin.emoji.add
+        """
+        kwargs.update({"name": name, "url": url})
+        return self.api_call("admin.emoji.add", http_verb="GET", params=kwargs)
+
+    def admin_emoji_addAlias(
+        self,
+        *,
+        alias_for: str,
+        name: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Add an emoji alias.
+        https://docs.slack.dev/reference/methods/admin.emoji.addAlias
+        """
+        kwargs.update({"alias_for": alias_for, "name": name})
+        return self.api_call("admin.emoji.addAlias", http_verb="GET", params=kwargs)
+
+    def admin_emoji_list(
+        self,
+        *,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """List emoji for an Enterprise Grid organization.
+        https://docs.slack.dev/reference/methods/admin.emoji.list
+        """
+        kwargs.update({"cursor": cursor, "limit": limit})
+        return self.api_call("admin.emoji.list", http_verb="GET", params=kwargs)
+
+    def admin_emoji_remove(
+        self,
+        *,
+        name: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Remove an emoji across an Enterprise Grid organization.
+        https://docs.slack.dev/reference/methods/admin.emoji.remove
+        """
+        kwargs.update({"name": name})
+        return self.api_call("admin.emoji.remove", http_verb="GET", params=kwargs)
+
+    def admin_emoji_rename(
+        self,
+        *,
+        name: str,
+        new_name: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Rename an emoji.
+        https://docs.slack.dev/reference/methods/admin.emoji.rename
+        """
+        kwargs.update({"name": name, "new_name": new_name})
+        return self.api_call("admin.emoji.rename", http_verb="GET", params=kwargs)
+
+    def admin_functions_list(
+        self,
+        *,
+        app_ids: Union[str, Sequence[str]],
+        team_id: Optional[str] = None,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Look up functions by a set of apps
+        https://docs.slack.dev/reference/methods/admin.functions.list
+        """
+        if isinstance(app_ids, (list, tuple)):
+            kwargs.update({"app_ids": ",".join(app_ids)})
+        else:
+            kwargs.update({"app_ids": app_ids})
+        kwargs.update(
+            {
+                "team_id": team_id,
+                "cursor": cursor,
+                "limit": limit,
+            }
+        )
+        return self.api_call("admin.functions.list", params=kwargs)
+
+    def admin_functions_permissions_lookup(
+        self,
+        *,
+        function_ids: Union[str, Sequence[str]],
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Lookup the visibility of multiple Slack functions
+        and include the users if it is limited to particular named entities.
+        https://docs.slack.dev/reference/methods/admin.functions.permissions.lookup
+        """
+        if isinstance(function_ids, (list, tuple)):
+            kwargs.update({"function_ids": ",".join(function_ids)})
+        else:
+            kwargs.update({"function_ids": function_ids})
+        return self.api_call("admin.functions.permissions.lookup", params=kwargs)
+
+    def admin_functions_permissions_set(
+        self,
+        *,
+        function_id: str,
+        visibility: str,
+        user_ids: Optional[Union[str, Sequence[str]]] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Set the visibility of a Slack function
+        and define the users or workspaces if it is set to named_entities
+        https://docs.slack.dev/reference/methods/admin.functions.permissions.set
+        """
+        kwargs.update(
+            {
+                "function_id": function_id,
+                "visibility": visibility,
+            }
+        )
+        if user_ids is not None:
+            if isinstance(user_ids, (list, tuple)):
+                kwargs.update({"user_ids": ",".join(user_ids)})
+            else:
+                kwargs.update({"user_ids": user_ids})
+        return self.api_call("admin.functions.permissions.set", params=kwargs)
+
+    def admin_roles_addAssignments(
+        self,
+        *,
+        role_id: str,
+        entity_ids: Union[str, Sequence[str]],
+        user_ids: Union[str, Sequence[str]],
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Adds members to the specified role with the specified scopes
+        https://docs.slack.dev/reference/methods/admin.roles.addAssignments
+        """
+        kwargs.update({"role_id": role_id})
+        if isinstance(entity_ids, (list, tuple)):
+            kwargs.update({"entity_ids": ",".join(entity_ids)})
+        else:
+            kwargs.update({"entity_ids": entity_ids})
+        if isinstance(user_ids, (list, tuple)):
+            kwargs.update({"user_ids": ",".join(user_ids)})
+        else:
+            kwargs.update({"user_ids": user_ids})
+        return self.api_call("admin.roles.addAssignments", params=kwargs)
+
+    def admin_roles_listAssignments(
+        self,
+        *,
+        role_ids: Optional[Union[str, Sequence[str]]] = None,
+        entity_ids: Optional[Union[str, Sequence[str]]] = None,
+        cursor: Optional[str] = None,
+        limit: Optional[Union[str, int]] = None,
+        sort_dir: Optional[str] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Lists assignments for all roles across entities.
+            Options to scope results by any combination of roles or entities
+        https://docs.slack.dev/reference/methods/admin.roles.listAssignments
+        """
+        kwargs.update({"cursor": cursor, "limit": limit, "sort_dir": sort_dir})
+        if isinstance(entity_ids, (list, tuple)):
+            kwargs.update({"entity_ids": ",".join(entity_ids)})
+        else:
+            kwargs.update({"entity_ids": entity_ids})
+        if isinstance(role_ids, (list, tuple)):
+            kwargs.update({"role_ids": ",".join(role_ids)})
+        else:
+            kwargs.update({"role_ids": role_ids})
+        return self.api_call("admin.roles.listAssignments", params=kwargs)
+
+    def admin_roles_removeAssignments(
+        self,
+        *,
+        role_id: str,
+        entity_ids: Union[str, Sequence[str]],
+        user_ids: Union[str, Sequence[str]],
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Removes a set of users from a role for the given scopes and entities
+        https://docs.slack.dev/reference/methods/admin.roles.removeAssignments
+        """
+        kwargs.update({"role_id": role_id})
+        if isinstance(entity_ids, (list, tuple)):
+            kwargs.update({"entity_ids": ",".join(entity_ids)})
+        else:
+            kwargs.update({"entity_ids": entity_ids})
+        if isinstance(user_ids, (list, tuple)):
+            kwargs.update({"user_ids": ",".join(user_ids)})
+        else:
+            kwargs.update({"user_ids": user_ids})
+        return self.api_call("admin.roles.removeAssignments", params=kwargs)
+
+    def admin_users_session_reset(
+        self,
+        *,
+        user_id: str,
+        mobile_only: Optional[bool] = None,
+        web_only: Optional[bool] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Wipes all valid sessions on all devices for a given user.
+        https://docs.slack.dev/reference/methods/admin.users.session.reset
+        """
+        kwargs.update(
+            {
+                "user_id": user_id,
+                "mobile_only": mobile_only,
+                "web_only": web_only,
+            }
+        )
+        return self.api_call("admin.users.session.reset", params=kwargs)
+
+    def admin_users_session_resetBulk(
+        self,
+        *,
+        user_ids: Union[str, Sequence[str]],
+        mobile_only: Optional[bool] = None,
+        web_only: Optional[bool] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Enqueues an asynchronous job to wipe all valid sessions on all devices for a given list of users
+        https://docs.slack.dev/reference/methods/admin.users.session.resetBulk
+        """
+        if isinstance(user_ids, (list, tuple)):
+            kwargs.update({"user_ids": ",".join(user_ids)})
+        else:
+            kwargs.update({"user_ids": user_ids})
+        kwargs.update(
+            {
+                "mobile_only": mobile_only,
+                "web_only": web_only,
+            }
+        )
+        return self.api_call("admin.users.session.resetBulk", params=kwargs)
+
+    def admin_users_session_invalidate(
+        self,
+        *,
+        session_id: str,
+        team_id: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Invalidate a single session for a user by session_id.
+        https://docs.slack.dev/reference/methods/admin.users.session.invalidate
+        """
+        kwargs.update({"session_id": session_id, "team_id": team_id})
+        return self.api_call("admin.users.session.invalidate", params=kwargs)
+
+    def admin_users_session_list(
+        self,
+        *,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        team_id: Optional[str] = None,
+        user_id: Optional[str] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Lists all active user sessions for an organization
+        https://docs.slack.dev/reference/methods/admin.users.session.list
+        """
+        kwargs.update(
+            {
+                "cursor": cursor,
+                "limit": limit,
+                "team_id": team_id,
+                "user_id": user_id,
+            }
+        )
+        return self.api_call("admin.users.session.list", params=kwargs)
+
+    def admin_teams_settings_setDefaultChannels(
+        self,
+        *,
+        team_id: str,
+        channel_ids: Union[str, Sequence[str]],
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Set the default channels of a workspace.
+        https://docs.slack.dev/reference/methods/admin.teams.settings.setDefaultChannels
+        """
+        kwargs.update({"team_id": team_id})
+        if isinstance(channel_ids, (list, tuple)):
+            kwargs.update({"channel_ids": ",".join(channel_ids)})
+        else:
+            kwargs.update({"channel_ids": channel_ids})
+        return self.api_call("admin.teams.settings.setDefaultChannels", http_verb="GET", params=kwargs)
+
+    def admin_users_session_getSettings(
+        self,
+        *,
+        user_ids: Union[str, Sequence[str]],
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Get user-specific session settings—the session duration
+        and what happens when the client closes—given a list of users.
+        https://docs.slack.dev/reference/methods/admin.users.session.getSettings
+        """
+        if isinstance(user_ids, (list, tuple)):
+            kwargs.update({"user_ids": ",".join(user_ids)})
+        else:
+            kwargs.update({"user_ids": user_ids})
+        return self.api_call("admin.users.session.getSettings", params=kwargs)
+
+    def admin_users_session_setSettings(
+        self,
+        *,
+        user_ids: Union[str, Sequence[str]],
+        desktop_app_browser_quit: Optional[bool] = None,
+        duration: Optional[int] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Configure the user-level session settings—the session duration
+        and what happens when the client closes—for one or more users.
+        https://docs.slack.dev/reference/methods/admin.users.session.setSettings
+        """
+        if isinstance(user_ids, (list, tuple)):
+            kwargs.update({"user_ids": ",".join(user_ids)})
+        else:
+            kwargs.update({"user_ids": user_ids})
+        kwargs.update(
+            {
+                "desktop_app_browser_quit": desktop_app_browser_quit,
+                "duration": duration,
+            }
+        )
+        return self.api_call("admin.users.session.setSettings", params=kwargs)
+
+    def admin_users_session_clearSettings(
+        self,
+        *,
+        user_ids: Union[str, Sequence[str]],
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Clear user-specific session settings—the session duration
+        and what happens when the client closes—for a list of users.
+        https://docs.slack.dev/reference/methods/admin.users.session.clearSettings
+        """
+        if isinstance(user_ids, (list, tuple)):
+            kwargs.update({"user_ids": ",".join(user_ids)})
+        else:
+            kwargs.update({"user_ids": user_ids})
+        return self.api_call("admin.users.session.clearSettings", params=kwargs)
+
+    def admin_users_unsupportedVersions_export(
+        self,
+        *,
+        date_end_of_support: Optional[Union[str, int]] = None,
+        date_sessions_started: Optional[Union[str, int]] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Ask Slackbot to send you an export listing all workspace members using unsupported software,
+        presented as a zipped CSV file.
+        https://docs.slack.dev/reference/methods/admin.users.unsupportedVersions.export
+        """
+        kwargs.update(
+            {
+                "date_end_of_support": date_end_of_support,
+                "date_sessions_started": date_sessions_started,
+            }
+        )
+        return self.api_call("admin.users.unsupportedVersions.export", params=kwargs)
+
+    def admin_inviteRequests_approve(
+        self,
+        *,
+        invite_request_id: str,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Approve a workspace invite request.
+        https://docs.slack.dev/reference/methods/admin.inviteRequests.approve
+        """
+        kwargs.update({"invite_request_id": invite_request_id, "team_id": team_id})
+        return self.api_call("admin.inviteRequests.approve", params=kwargs)
+
+    def admin_inviteRequests_approved_list(
+        self,
+        *,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """List all approved workspace invite requests.
+        https://docs.slack.dev/reference/methods/admin.inviteRequests.approved.list
+        """
+        kwargs.update(
+            {
+                "cursor": cursor,
+                "limit": limit,
+                "team_id": team_id,
+            }
+        )
+        return self.api_call("admin.inviteRequests.approved.list", params=kwargs)
+
+    def admin_inviteRequests_denied_list(
+        self,
+        *,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """List all denied workspace invite requests.
+        https://docs.slack.dev/reference/methods/admin.inviteRequests.denied.list
+        """
+        kwargs.update(
+            {
+                "cursor": cursor,
+                "limit": limit,
+                "team_id": team_id,
+            }
+        )
+        return self.api_call("admin.inviteRequests.denied.list", params=kwargs)
+
+    def admin_inviteRequests_deny(
+        self,
+        *,
+        invite_request_id: str,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Deny a workspace invite request.
+        https://docs.slack.dev/reference/methods/admin.inviteRequests.deny
+        """
+        kwargs.update({"invite_request_id": invite_request_id, "team_id": team_id})
+        return self.api_call("admin.inviteRequests.deny", params=kwargs)
+
+    def admin_inviteRequests_list(
+        self,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """List all pending workspace invite requests."""
+        return self.api_call("admin.inviteRequests.list", params=kwargs)
+
+    def admin_teams_admins_list(
+        self,
+        *,
+        team_id: str,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """List all of the admins on a given workspace.
+        https://docs.slack.dev/reference/methods/admin.inviteRequests.list
+        """
+        kwargs.update(
+            {
+                "cursor": cursor,
+                "limit": limit,
+                "team_id": team_id,
+            }
+        )
+        return self.api_call("admin.teams.admins.list", http_verb="GET", params=kwargs)
+
+    def admin_teams_create(
+        self,
+        *,
+        team_domain: str,
+        team_name: str,
+        team_description: Optional[str] = None,
+        team_discoverability: Optional[str] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Create an Enterprise team.
+        https://docs.slack.dev/reference/methods/admin.teams.create
+        """
+        kwargs.update(
+            {
+                "team_domain": team_domain,
+                "team_name": team_name,
+                "team_description": team_description,
+                "team_discoverability": team_discoverability,
+            }
+        )
+        return self.api_call("admin.teams.create", params=kwargs)
+
+    def admin_teams_list(
+        self,
+        *,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """List all teams on an Enterprise organization.
+        https://docs.slack.dev/reference/methods/admin.teams.list
+        """
+        kwargs.update({"cursor": cursor, "limit": limit})
+        return self.api_call("admin.teams.list", params=kwargs)
+
+    def admin_teams_owners_list(
+        self,
+        *,
+        team_id: str,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """List all of the admins on a given workspace.
+        https://docs.slack.dev/reference/methods/admin.teams.owners.list
+        """
+        kwargs.update({"team_id": team_id, "cursor": cursor, "limit": limit})
+        return self.api_call("admin.teams.owners.list", http_verb="GET", params=kwargs)
+
+    def admin_teams_settings_info(
+        self,
+        *,
+        team_id: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Fetch information about settings in a workspace
+        https://docs.slack.dev/reference/methods/admin.teams.settings.info
+        """
+        kwargs.update({"team_id": team_id})
+        return self.api_call("admin.teams.settings.info", params=kwargs)
+
+    def admin_teams_settings_setDescription(
+        self,
+        *,
+        team_id: str,
+        description: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Set the description of a given workspace.
+        https://docs.slack.dev/reference/methods/admin.teams.settings.setDescription
+        """
+        kwargs.update({"team_id": team_id, "description": description})
+        return self.api_call("admin.teams.settings.setDescription", params=kwargs)
+
+    def admin_teams_settings_setDiscoverability(
+        self,
+        *,
+        team_id: str,
+        discoverability: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Sets the icon of a workspace.
+        https://docs.slack.dev/reference/methods/admin.teams.settings.setDiscoverability
+        """
+        kwargs.update({"team_id": team_id, "discoverability": discoverability})
+        return self.api_call("admin.teams.settings.setDiscoverability", params=kwargs)
+
+    def admin_teams_settings_setIcon(
+        self,
+        *,
+        team_id: str,
+        image_url: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Sets the icon of a workspace.
+        https://docs.slack.dev/reference/methods/admin.teams.settings.setIcon
+        """
+        kwargs.update({"team_id": team_id, "image_url": image_url})
+        return self.api_call("admin.teams.settings.setIcon", http_verb="GET", params=kwargs)
+
+    def admin_teams_settings_setName(
+        self,
+        *,
+        team_id: str,
+        name: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Sets the icon of a workspace.
+        https://docs.slack.dev/reference/methods/admin.teams.settings.setName
+        """
+        kwargs.update({"team_id": team_id, "name": name})
+        return self.api_call("admin.teams.settings.setName", params=kwargs)
+
+    def admin_usergroups_addChannels(
+        self,
+        *,
+        channel_ids: Union[str, Sequence[str]],
+        usergroup_id: str,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Add one or more default channels to an IDP group.
+        https://docs.slack.dev/reference/methods/admin.usergroups.addChannels
+        """
+        kwargs.update({"team_id": team_id, "usergroup_id": usergroup_id})
+        if isinstance(channel_ids, (list, tuple)):
+            kwargs.update({"channel_ids": ",".join(channel_ids)})
+        else:
+            kwargs.update({"channel_ids": channel_ids})
+        return self.api_call("admin.usergroups.addChannels", params=kwargs)
+
+    def admin_usergroups_addTeams(
+        self,
+        *,
+        usergroup_id: str,
+        team_ids: Union[str, Sequence[str]],
+        auto_provision: Optional[bool] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Associate one or more default workspaces with an organization-wide IDP group.
+        https://docs.slack.dev/reference/methods/admin.usergroups.addTeams
+        """
+        kwargs.update({"usergroup_id": usergroup_id, "auto_provision": auto_provision})
+        if isinstance(team_ids, (list, tuple)):
+            kwargs.update({"team_ids": ",".join(team_ids)})
+        else:
+            kwargs.update({"team_ids": team_ids})
+        return self.api_call("admin.usergroups.addTeams", params=kwargs)
+
+    def admin_usergroups_listChannels(
+        self,
+        *,
+        usergroup_id: str,
+        include_num_members: Optional[bool] = None,
+        team_id: Optional[bool] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Add one or more default channels to an IDP group.
+        https://docs.slack.dev/reference/methods/admin.usergroups.listChannels
+        """
+        kwargs.update(
+            {
+                "usergroup_id": usergroup_id,
+                "include_num_members": include_num_members,
+                "team_id": team_id,
+            }
+        )
+        return self.api_call("admin.usergroups.listChannels", params=kwargs)
+
+    def admin_usergroups_removeChannels(
+        self,
+        *,
+        usergroup_id: str,
+        channel_ids: Union[str, Sequence[str]],
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Add one or more default channels to an IDP group.
+        https://docs.slack.dev/reference/methods/admin.usergroups.removeChannels
+        """
+        kwargs.update({"usergroup_id": usergroup_id})
+        if isinstance(channel_ids, (list, tuple)):
+            kwargs.update({"channel_ids": ",".join(channel_ids)})
+        else:
+            kwargs.update({"channel_ids": channel_ids})
+        return self.api_call("admin.usergroups.removeChannels", params=kwargs)
+
+    def admin_users_assign(
+        self,
+        *,
+        team_id: str,
+        user_id: str,
+        channel_ids: Optional[Union[str, Sequence[str]]] = None,
+        is_restricted: Optional[bool] = None,
+        is_ultra_restricted: Optional[bool] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Add an Enterprise user to a workspace.
+        https://docs.slack.dev/reference/methods/admin.users.assign
+        """
+        kwargs.update(
+            {
+                "team_id": team_id,
+                "user_id": user_id,
+                "is_restricted": is_restricted,
+                "is_ultra_restricted": is_ultra_restricted,
+            }
+        )
+        if isinstance(channel_ids, (list, tuple)):
+            kwargs.update({"channel_ids": ",".join(channel_ids)})
+        else:
+            kwargs.update({"channel_ids": channel_ids})
+        return self.api_call("admin.users.assign", params=kwargs)
+
+    def admin_users_invite(
+        self,
+        *,
+        team_id: str,
+        email: str,
+        channel_ids: Union[str, Sequence[str]],
+        custom_message: Optional[str] = None,
+        email_password_policy_enabled: Optional[bool] = None,
+        guest_expiration_ts: Optional[Union[str, float]] = None,
+        is_restricted: Optional[bool] = None,
+        is_ultra_restricted: Optional[bool] = None,
+        real_name: Optional[str] = None,
+        resend: Optional[bool] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Invite a user to a workspace.
+        https://docs.slack.dev/reference/methods/admin.users.invite
+        """
+        kwargs.update(
+            {
+                "team_id": team_id,
+                "email": email,
+                "custom_message": custom_message,
+                "email_password_policy_enabled": email_password_policy_enabled,
+                "guest_expiration_ts": str(guest_expiration_ts) if guest_expiration_ts is not None else None,
+                "is_restricted": is_restricted,
+                "is_ultra_restricted": is_ultra_restricted,
+                "real_name": real_name,
+                "resend": resend,
+            }
+        )
+        if isinstance(channel_ids, (list, tuple)):
+            kwargs.update({"channel_ids": ",".join(channel_ids)})
+        else:
+            kwargs.update({"channel_ids": channel_ids})
+        return self.api_call("admin.users.invite", params=kwargs)
+
+    def admin_users_list(
+        self,
+        *,
+        team_id: Optional[str] = None,
+        include_deactivated_user_workspaces: Optional[bool] = None,
+        is_active: Optional[bool] = None,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """List users on a workspace
+        https://docs.slack.dev/reference/methods/admin.users.list
+        """
+        kwargs.update(
+            {
+                "team_id": team_id,
+                "include_deactivated_user_workspaces": include_deactivated_user_workspaces,
+                "is_active": is_active,
+                "cursor": cursor,
+                "limit": limit,
+            }
+        )
+        return self.api_call("admin.users.list", params=kwargs)
+
+    def admin_users_remove(
+        self,
+        *,
+        team_id: str,
+        user_id: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Remove a user from a workspace.
+        https://docs.slack.dev/reference/methods/admin.users.remove
+        """
+        kwargs.update({"team_id": team_id, "user_id": user_id})
+        return self.api_call("admin.users.remove", params=kwargs)
+
+    def admin_users_setAdmin(
+        self,
+        *,
+        team_id: str,
+        user_id: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Set an existing guest, regular user, or owner to be an admin user.
+        https://docs.slack.dev/reference/methods/admin.users.setAdmin
+        """
+        kwargs.update({"team_id": team_id, "user_id": user_id})
+        return self.api_call("admin.users.setAdmin", params=kwargs)
+
+    def admin_users_setExpiration(
+        self,
+        *,
+        expiration_ts: int,
+        user_id: str,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Set an expiration for a guest user.
+        https://docs.slack.dev/reference/methods/admin.users.setExpiration
+        """
+        kwargs.update({"expiration_ts": expiration_ts, "team_id": team_id, "user_id": user_id})
+        return self.api_call("admin.users.setExpiration", params=kwargs)
+
+    def admin_users_setOwner(
+        self,
+        *,
+        team_id: str,
+        user_id: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Set an existing guest, regular user, or admin user to be a workspace owner.
+        https://docs.slack.dev/reference/methods/admin.users.setOwner
+        """
+        kwargs.update({"team_id": team_id, "user_id": user_id})
+        return self.api_call("admin.users.setOwner", params=kwargs)
+
+    def admin_users_setRegular(
+        self,
+        *,
+        team_id: str,
+        user_id: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Set an existing guest user, admin user, or owner to be a regular user.
+        https://docs.slack.dev/reference/methods/admin.users.setRegular
+        """
+        kwargs.update({"team_id": team_id, "user_id": user_id})
+        return self.api_call("admin.users.setRegular", params=kwargs)
+
+    def admin_workflows_search(
+        self,
+        *,
+        app_id: Optional[str] = None,
+        collaborator_ids: Optional[Union[str, Sequence[str]]] = None,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        no_collaborators: Optional[bool] = None,
+        num_trigger_ids: Optional[int] = None,
+        query: Optional[str] = None,
+        sort: Optional[str] = None,
+        sort_dir: Optional[str] = None,
+        source: Optional[str] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Search workflows within the team or enterprise
+        https://docs.slack.dev/reference/methods/admin.workflows.search
+        """
+        if collaborator_ids is not None:
+            if isinstance(collaborator_ids, (list, tuple)):
+                kwargs.update({"collaborator_ids": ",".join(collaborator_ids)})
+            else:
+                kwargs.update({"collaborator_ids": collaborator_ids})
+        kwargs.update(
+            {
+                "app_id": app_id,
+                "cursor": cursor,
+                "limit": limit,
+                "no_collaborators": no_collaborators,
+                "num_trigger_ids": num_trigger_ids,
+                "query": query,
+                "sort": sort,
+                "sort_dir": sort_dir,
+                "source": source,
+            }
+        )
+        return self.api_call("admin.workflows.search", params=kwargs)
+
+    def admin_workflows_permissions_lookup(
+        self,
+        *,
+        workflow_ids: Union[str, Sequence[str]],
+        max_workflow_triggers: Optional[int] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Look up the permissions for a set of workflows
+        https://docs.slack.dev/reference/methods/admin.workflows.permissions.lookup
+        """
+        if isinstance(workflow_ids, (list, tuple)):
+            kwargs.update({"workflow_ids": ",".join(workflow_ids)})
+        else:
+            kwargs.update({"workflow_ids": workflow_ids})
+        kwargs.update(
+            {
+                "max_workflow_triggers": max_workflow_triggers,
+            }
+        )
+        return self.api_call("admin.workflows.permissions.lookup", params=kwargs)
+
+    def admin_workflows_collaborators_add(
+        self,
+        *,
+        collaborator_ids: Union[str, Sequence[str]],
+        workflow_ids: Union[str, Sequence[str]],
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Add collaborators to workflows within the team or enterprise
+        https://docs.slack.dev/reference/methods/admin.workflows.collaborators.add
+        """
+        if isinstance(collaborator_ids, (list, tuple)):
+            kwargs.update({"collaborator_ids": ",".join(collaborator_ids)})
+        else:
+            kwargs.update({"collaborator_ids": collaborator_ids})
+        if isinstance(workflow_ids, (list, tuple)):
+            kwargs.update({"workflow_ids": ",".join(workflow_ids)})
+        else:
+            kwargs.update({"workflow_ids": workflow_ids})
+        return self.api_call("admin.workflows.collaborators.add", params=kwargs)
+
+    def admin_workflows_collaborators_remove(
+        self,
+        *,
+        collaborator_ids: Union[str, Sequence[str]],
+        workflow_ids: Union[str, Sequence[str]],
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Remove collaborators from workflows within the team or enterprise
+        https://docs.slack.dev/reference/methods/admin.workflows.collaborators.remove
+        """
+        if isinstance(collaborator_ids, (list, tuple)):
+            kwargs.update({"collaborator_ids": ",".join(collaborator_ids)})
+        else:
+            kwargs.update({"collaborator_ids": collaborator_ids})
+        if isinstance(workflow_ids, (list, tuple)):
+            kwargs.update({"workflow_ids": ",".join(workflow_ids)})
+        else:
+            kwargs.update({"workflow_ids": workflow_ids})
+        return self.api_call("admin.workflows.collaborators.remove", params=kwargs)
+
+    def admin_workflows_unpublish(
+        self,
+        *,
+        workflow_ids: Union[str, Sequence[str]],
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Unpublish workflows within the team or enterprise
+        https://docs.slack.dev/reference/methods/admin.workflows.unpublish
+        """
+        if isinstance(workflow_ids, (list, tuple)):
+            kwargs.update({"workflow_ids": ",".join(workflow_ids)})
+        else:
+            kwargs.update({"workflow_ids": workflow_ids})
+        return self.api_call("admin.workflows.unpublish", params=kwargs)
+
+    def api_test(
+        self,
+        *,
+        error: Optional[str] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Checks API calling code.
+        https://docs.slack.dev/reference/methods/api.test
+        """
+        kwargs.update({"error": error})
+        return self.api_call("api.test", params=kwargs)
+
+    def apps_connections_open(
+        self,
+        *,
+        app_token: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Generate a temporary Socket Mode WebSocket URL that your app can connect to
+        in order to receive events and interactive payloads
+        https://docs.slack.dev/reference/methods/apps.connections.open
+        """
+        kwargs.update({"token": app_token})
+        return self.api_call("apps.connections.open", http_verb="POST", params=kwargs)
+
+    def apps_event_authorizations_list(
+        self,
+        *,
+        event_context: str,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Get a list of authorizations for the given event context.
+        Each authorization represents an app installation that the event is visible to.
+        https://docs.slack.dev/reference/methods/apps.event.authorizations.list
+        """
+        kwargs.update({"event_context": event_context, "cursor": cursor, "limit": limit})
+        return self.api_call("apps.event.authorizations.list", params=kwargs)
+
+    def apps_uninstall(
+        self,
+        *,
+        client_id: str,
+        client_secret: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Uninstalls your app from a workspace.
+        https://docs.slack.dev/reference/methods/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://docs.slack.dev/reference/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://docs.slack.dev/reference/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://docs.slack.dev/reference/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://docs.slack.dev/reference/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://docs.slack.dev/reference/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://docs.slack.dev/reference/methods/tooling.tokens.rotate
+        """
+        kwargs.update({"refresh_token": refresh_token})
+        return self.api_call("tooling.tokens.rotate", params=kwargs)
+
+    def assistant_threads_setStatus(
+        self,
+        *,
+        channel_id: str,
+        thread_ts: str,
+        status: str,
+        loading_messages: Optional[List[str]] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Set the status for an AI assistant thread.
+        https://docs.slack.dev/reference/methods/assistant.threads.setStatus
+        """
+        kwargs.update(
+            {"channel_id": channel_id, "thread_ts": thread_ts, "status": status, "loading_messages": loading_messages}
+        )
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("assistant.threads.setStatus", json=kwargs)
+
+    def assistant_threads_setTitle(
+        self,
+        *,
+        channel_id: str,
+        thread_ts: str,
+        title: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Set the title for the given assistant thread.
+        https://docs.slack.dev/reference/methods/assistant.threads.setTitle
+        """
+        kwargs.update({"channel_id": channel_id, "thread_ts": thread_ts, "title": title})
+        return self.api_call("assistant.threads.setTitle", params=kwargs)
+
+    def assistant_threads_setSuggestedPrompts(
+        self,
+        *,
+        channel_id: str,
+        thread_ts: str,
+        title: Optional[str] = None,
+        prompts: List[Dict[str, str]],
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Set suggested prompts for the given assistant thread.
+        https://docs.slack.dev/reference/methods/assistant.threads.setSuggestedPrompts
+        """
+        kwargs.update({"channel_id": channel_id, "thread_ts": thread_ts, "prompts": prompts})
+        if title is not None:
+            kwargs.update({"title": title})
+        return self.api_call("assistant.threads.setSuggestedPrompts", json=kwargs)
+
+    def auth_revoke(
+        self,
+        *,
+        test: Optional[bool] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Revokes a token.
+        https://docs.slack.dev/reference/methods/auth.revoke
+        """
+        kwargs.update({"test": test})
+        return self.api_call("auth.revoke", http_verb="GET", params=kwargs)
+
+    def auth_test(
+        self,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Checks authentication & identity.
+        https://docs.slack.dev/reference/methods/auth.test
+        """
+        return self.api_call("auth.test", params=kwargs)
+
+    def auth_teams_list(
+        self,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        include_icon: Optional[bool] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """List the workspaces a token can access.
+        https://docs.slack.dev/reference/methods/auth.teams.list
+        """
+        kwargs.update({"cursor": cursor, "limit": limit, "include_icon": include_icon})
+        return self.api_call("auth.teams.list", params=kwargs)
+
+    def bookmarks_add(
+        self,
+        *,
+        channel_id: str,
+        title: str,
+        type: str,
+        emoji: Optional[str] = None,
+        entity_id: Optional[str] = None,
+        link: Optional[str] = None,  # include when type is 'link'
+        parent_id: Optional[str] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Add bookmark to a channel.
+        https://docs.slack.dev/reference/methods/bookmarks.add
+        """
+        kwargs.update(
+            {
+                "channel_id": channel_id,
+                "title": title,
+                "type": type,
+                "emoji": emoji,
+                "entity_id": entity_id,
+                "link": link,
+                "parent_id": parent_id,
+            }
+        )
+        return self.api_call("bookmarks.add", http_verb="POST", params=kwargs)
+
+    def bookmarks_edit(
+        self,
+        *,
+        bookmark_id: str,
+        channel_id: str,
+        emoji: Optional[str] = None,
+        link: Optional[str] = None,
+        title: Optional[str] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Edit bookmark.
+        https://docs.slack.dev/reference/methods/bookmarks.edit
+        """
+        kwargs.update(
+            {
+                "bookmark_id": bookmark_id,
+                "channel_id": channel_id,
+                "emoji": emoji,
+                "link": link,
+                "title": title,
+            }
+        )
+        return self.api_call("bookmarks.edit", http_verb="POST", params=kwargs)
+
+    def bookmarks_list(
+        self,
+        *,
+        channel_id: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """List bookmark for the channel.
+        https://docs.slack.dev/reference/methods/bookmarks.list
+        """
+        kwargs.update({"channel_id": channel_id})
+        return self.api_call("bookmarks.list", http_verb="POST", params=kwargs)
+
+    def bookmarks_remove(
+        self,
+        *,
+        bookmark_id: str,
+        channel_id: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Remove bookmark from the channel.
+        https://docs.slack.dev/reference/methods/bookmarks.remove
+        """
+        kwargs.update({"bookmark_id": bookmark_id, "channel_id": channel_id})
+        return self.api_call("bookmarks.remove", http_verb="POST", params=kwargs)
+
+    def bots_info(
+        self,
+        *,
+        bot: Optional[str] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Gets information about a bot user.
+        https://docs.slack.dev/reference/methods/bots.info
+        """
+        kwargs.update({"bot": bot, "team_id": team_id})
+        return self.api_call("bots.info", http_verb="GET", params=kwargs)
+
+    def calls_add(
+        self,
+        *,
+        external_unique_id: str,
+        join_url: str,
+        created_by: Optional[str] = None,
+        date_start: Optional[int] = None,
+        desktop_app_join_url: Optional[str] = None,
+        external_display_id: Optional[str] = None,
+        title: Optional[str] = None,
+        users: Optional[Union[str, Sequence[Dict[str, str]]]] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Registers a new Call.
+        https://docs.slack.dev/reference/methods/calls.add
+        """
+        kwargs.update(
+            {
+                "external_unique_id": external_unique_id,
+                "join_url": join_url,
+                "created_by": created_by,
+                "date_start": date_start,
+                "desktop_app_join_url": desktop_app_join_url,
+                "external_display_id": external_display_id,
+                "title": title,
+            }
+        )
+        _update_call_participants(
+            kwargs,
+            users if users is not None else kwargs.get("users"),  # type: ignore[arg-type]
+        )
+        return self.api_call("calls.add", http_verb="POST", params=kwargs)
+
+    def calls_end(
+        self,
+        *,
+        id: str,
+        duration: Optional[int] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Ends a Call.
+        https://docs.slack.dev/reference/methods/calls.end
+        """
+        kwargs.update({"id": id, "duration": duration})
+        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.
+        https://docs.slack.dev/reference/methods/calls.info
+        """
+        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, Sequence[Dict[str, str]]],
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Registers new participants added to a Call.
+        https://docs.slack.dev/reference/methods/calls.participants.add
+        """
+        kwargs.update({"id": id})
+        _update_call_participants(kwargs, users)
+        return self.api_call("calls.participants.add", http_verb="POST", params=kwargs)
+
+    def calls_participants_remove(
+        self,
+        *,
+        id: str,
+        users: Union[str, Sequence[Dict[str, str]]],
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Registers participants removed from a Call.
+        https://docs.slack.dev/reference/methods/calls.participants.remove
+        """
+        kwargs.update({"id": id})
+        _update_call_participants(kwargs, users)
+        return self.api_call("calls.participants.remove", http_verb="POST", params=kwargs)
+
+    def calls_update(
+        self,
+        *,
+        id: str,
+        desktop_app_join_url: Optional[str] = None,
+        join_url: Optional[str] = None,
+        title: Optional[str] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Updates information about a Call.
+        https://docs.slack.dev/reference/methods/calls.update
+        """
+        kwargs.update(
+            {
+                "id": id,
+                "desktop_app_join_url": desktop_app_join_url,
+                "join_url": join_url,
+                "title": title,
+            }
+        )
+        return self.api_call("calls.update", http_verb="POST", params=kwargs)
+
+    def canvases_create(
+        self,
+        *,
+        title: Optional[str] = None,
+        document_content: Dict[str, str],
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Create Canvas for a user
+        https://docs.slack.dev/reference/methods/canvases.create
+        """
+        kwargs.update({"title": title, "document_content": document_content})
+        return self.api_call("canvases.create", json=kwargs)
+
+    def canvases_edit(
+        self,
+        *,
+        canvas_id: str,
+        changes: Sequence[Dict[str, Any]],
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Update an existing canvas
+        https://docs.slack.dev/reference/methods/canvases.edit
+        """
+        kwargs.update({"canvas_id": canvas_id, "changes": changes})
+        return self.api_call("canvases.edit", json=kwargs)
+
+    def canvases_delete(
+        self,
+        *,
+        canvas_id: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Deletes a canvas
+        https://docs.slack.dev/reference/methods/canvases.delete
+        """
+        kwargs.update({"canvas_id": canvas_id})
+        return self.api_call("canvases.delete", params=kwargs)
+
+    def canvases_access_set(
+        self,
+        *,
+        canvas_id: str,
+        access_level: str,
+        channel_ids: Optional[Union[Sequence[str], str]] = None,
+        user_ids: Optional[Union[Sequence[str], str]] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Sets the access level to a canvas for specified entities
+        https://docs.slack.dev/reference/methods/canvases.access.set
+        """
+        kwargs.update({"canvas_id": canvas_id, "access_level": access_level})
+        if channel_ids is not None:
+            if isinstance(channel_ids, (list, tuple)):
+                kwargs.update({"channel_ids": ",".join(channel_ids)})
+            else:
+                kwargs.update({"channel_ids": channel_ids})
+        if user_ids is not None:
+            if isinstance(user_ids, (list, tuple)):
+                kwargs.update({"user_ids": ",".join(user_ids)})
+            else:
+                kwargs.update({"user_ids": user_ids})
+
+        return self.api_call("canvases.access.set", params=kwargs)
+
+    def canvases_access_delete(
+        self,
+        *,
+        canvas_id: str,
+        channel_ids: Optional[Union[Sequence[str], str]] = None,
+        user_ids: Optional[Union[Sequence[str], str]] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Create a Channel Canvas for a channel
+        https://docs.slack.dev/reference/methods/canvases.access.delete
+        """
+        kwargs.update({"canvas_id": canvas_id})
+        if channel_ids is not None:
+            if isinstance(channel_ids, (list, tuple)):
+                kwargs.update({"channel_ids": ",".join(channel_ids)})
+            else:
+                kwargs.update({"channel_ids": channel_ids})
+        if user_ids is not None:
+            if isinstance(user_ids, (list, tuple)):
+                kwargs.update({"user_ids": ",".join(user_ids)})
+            else:
+                kwargs.update({"user_ids": user_ids})
+        return self.api_call("canvases.access.delete", params=kwargs)
+
+    def canvases_sections_lookup(
+        self,
+        *,
+        canvas_id: str,
+        criteria: Dict[str, Any],
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Find sections matching the provided criteria
+        https://docs.slack.dev/reference/methods/canvases.sections.lookup
+        """
+        kwargs.update({"canvas_id": canvas_id, "criteria": json.dumps(criteria)})
+        return self.api_call("canvases.sections.lookup", params=kwargs)
+
+    # --------------------------
+    # Deprecated: channels.*
+    # You can use conversations.* APIs instead.
+    # https://docs.slack.dev/changelog/2020-01-deprecating-antecedents-to-the-conversations-api/
+    # --------------------------
+
+    def channels_archive(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Archives a channel."""
+        kwargs.update({"channel": channel})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("channels.archive", json=kwargs)
+
+    def channels_create(
+        self,
+        *,
+        name: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Creates a channel."""
+        kwargs.update({"name": name})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("channels.create", json=kwargs)
+
+    def channels_history(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Fetches history of messages and events from a channel."""
+        kwargs.update({"channel": channel})
+        return self.api_call("channels.history", http_verb="GET", params=kwargs)
+
+    def channels_info(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Gets information about a channel."""
+        kwargs.update({"channel": channel})
+        return self.api_call("channels.info", http_verb="GET", params=kwargs)
+
+    def channels_invite(
+        self,
+        *,
+        channel: str,
+        user: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Invites a user to a channel."""
+        kwargs.update({"channel": channel, "user": user})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("channels.invite", json=kwargs)
+
+    def channels_join(
+        self,
+        *,
+        name: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Joins a channel, creating it if needed."""
+        kwargs.update({"name": name})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("channels.join", json=kwargs)
+
+    def channels_kick(
+        self,
+        *,
+        channel: str,
+        user: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Removes a user from a channel."""
+        kwargs.update({"channel": channel, "user": user})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("channels.kick", json=kwargs)
+
+    def channels_leave(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Leaves a channel."""
+        kwargs.update({"channel": channel})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("channels.leave", json=kwargs)
+
+    def channels_list(
+        self,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Lists all channels in a Slack team."""
+        return self.api_call("channels.list", http_verb="GET", params=kwargs)
+
+    def channels_mark(
+        self,
+        *,
+        channel: str,
+        ts: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Sets the read cursor in a channel."""
+        kwargs.update({"channel": channel, "ts": ts})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("channels.mark", json=kwargs)
+
+    def channels_rename(
+        self,
+        *,
+        channel: str,
+        name: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Renames a channel."""
+        kwargs.update({"channel": channel, "name": name})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("channels.rename", json=kwargs)
+
+    def channels_replies(
+        self,
+        *,
+        channel: str,
+        thread_ts: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Retrieve a thread of messages posted to a channel"""
+        kwargs.update({"channel": channel, "thread_ts": thread_ts})
+        return self.api_call("channels.replies", http_verb="GET", params=kwargs)
+
+    def channels_setPurpose(
+        self,
+        *,
+        channel: str,
+        purpose: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Sets the purpose for a channel."""
+        kwargs.update({"channel": channel, "purpose": purpose})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("channels.setPurpose", json=kwargs)
+
+    def channels_setTopic(
+        self,
+        *,
+        channel: str,
+        topic: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Sets the topic for a channel."""
+        kwargs.update({"channel": channel, "topic": topic})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("channels.setTopic", json=kwargs)
+
+    def channels_unarchive(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Unarchives a channel."""
+        kwargs.update({"channel": channel})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("channels.unarchive", json=kwargs)
+
+    # --------------------------
+
+    def chat_appendStream(
+        self,
+        *,
+        channel: str,
+        ts: str,
+        markdown_text: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Appends text to an existing streaming conversation.
+        https://docs.slack.dev/reference/methods/chat.appendStream
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "ts": ts,
+                "markdown_text": markdown_text,
+            }
+        )
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("chat.appendStream", json=kwargs)
+
+    def chat_delete(
+        self,
+        *,
+        channel: str,
+        ts: str,
+        as_user: Optional[bool] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Deletes a message.
+        https://docs.slack.dev/reference/methods/chat.delete
+        """
+        kwargs.update({"channel": channel, "ts": ts, "as_user": as_user})
+        return self.api_call("chat.delete", params=kwargs)
+
+    def chat_deleteScheduledMessage(
+        self,
+        *,
+        channel: str,
+        scheduled_message_id: str,
+        as_user: Optional[bool] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Deletes a scheduled message.
+        https://docs.slack.dev/reference/methods/chat.deleteScheduledMessage
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "scheduled_message_id": scheduled_message_id,
+                "as_user": as_user,
+            }
+        )
+        return self.api_call("chat.deleteScheduledMessage", params=kwargs)
+
+    def chat_getPermalink(
+        self,
+        *,
+        channel: str,
+        message_ts: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Retrieve a permalink URL for a specific extant message
+        https://docs.slack.dev/reference/methods/chat.getPermalink
+        """
+        kwargs.update({"channel": channel, "message_ts": message_ts})
+        return self.api_call("chat.getPermalink", http_verb="GET", params=kwargs)
+
+    def chat_meMessage(
+        self,
+        *,
+        channel: str,
+        text: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Share a me message into a channel.
+        https://docs.slack.dev/reference/methods/chat.meMessage
+        """
+        kwargs.update({"channel": channel, "text": text})
+        return self.api_call("chat.meMessage", params=kwargs)
+
+    def chat_postEphemeral(
+        self,
+        *,
+        channel: str,
+        user: str,
+        text: Optional[str] = None,
+        as_user: Optional[bool] = None,
+        attachments: Optional[Union[str, Sequence[Union[Dict, Attachment]]]] = None,
+        blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None,
+        thread_ts: Optional[str] = None,
+        icon_emoji: Optional[str] = None,
+        icon_url: Optional[str] = None,
+        link_names: Optional[bool] = None,
+        username: Optional[str] = None,
+        parse: Optional[str] = None,
+        markdown_text: Optional[str] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Sends an ephemeral message to a user in a channel.
+        https://docs.slack.dev/reference/methods/chat.postEphemeral
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "user": user,
+                "text": text,
+                "as_user": as_user,
+                "attachments": attachments,
+                "blocks": blocks,
+                "thread_ts": thread_ts,
+                "icon_emoji": icon_emoji,
+                "icon_url": icon_url,
+                "link_names": link_names,
+                "username": username,
+                "parse": parse,
+                "markdown_text": markdown_text,
+            }
+        )
+        _parse_web_class_objects(kwargs)
+        kwargs = _remove_none_values(kwargs)
+        _warn_if_message_text_content_is_missing("chat.postEphemeral", kwargs)
+        # NOTE: intentionally using json over params for the API methods using blocks/attachments
+        return self.api_call("chat.postEphemeral", json=kwargs)
+
+    def chat_postMessage(
+        self,
+        *,
+        channel: str,
+        text: Optional[str] = None,
+        as_user: Optional[bool] = None,
+        attachments: Optional[Union[str, Sequence[Union[Dict, Attachment]]]] = None,
+        blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None,
+        thread_ts: Optional[str] = None,
+        reply_broadcast: Optional[bool] = None,
+        unfurl_links: Optional[bool] = None,
+        unfurl_media: Optional[bool] = None,
+        container_id: Optional[str] = None,
+        icon_emoji: Optional[str] = None,
+        icon_url: Optional[str] = None,
+        mrkdwn: Optional[bool] = None,
+        link_names: Optional[bool] = None,
+        username: Optional[str] = None,
+        parse: Optional[str] = None,  # none, full
+        metadata: Optional[Union[Dict, Metadata, EventAndEntityMetadata]] = None,
+        markdown_text: Optional[str] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Sends a message to a channel.
+        https://docs.slack.dev/reference/methods/chat.postMessage
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "text": text,
+                "as_user": as_user,
+                "attachments": attachments,
+                "blocks": blocks,
+                "thread_ts": thread_ts,
+                "reply_broadcast": reply_broadcast,
+                "unfurl_links": unfurl_links,
+                "unfurl_media": unfurl_media,
+                "container_id": container_id,
+                "icon_emoji": icon_emoji,
+                "icon_url": icon_url,
+                "mrkdwn": mrkdwn,
+                "link_names": link_names,
+                "username": username,
+                "parse": parse,
+                "metadata": metadata,
+                "markdown_text": markdown_text,
+            }
+        )
+        _parse_web_class_objects(kwargs)
+        kwargs = _remove_none_values(kwargs)
+        _warn_if_message_text_content_is_missing("chat.postMessage", kwargs)
+        # NOTE: intentionally using json over params for the API methods using blocks/attachments
+        return self.api_call("chat.postMessage", json=kwargs)
+
+    def chat_scheduleMessage(
+        self,
+        *,
+        channel: str,
+        post_at: Union[str, int],
+        text: Optional[str] = None,
+        as_user: Optional[bool] = None,
+        attachments: Optional[Union[str, Sequence[Union[Dict, Attachment]]]] = None,
+        blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None,
+        thread_ts: Optional[str] = None,
+        parse: Optional[str] = None,
+        reply_broadcast: Optional[bool] = None,
+        unfurl_links: Optional[bool] = None,
+        unfurl_media: Optional[bool] = None,
+        link_names: Optional[bool] = None,
+        metadata: Optional[Union[Dict, Metadata]] = None,
+        markdown_text: Optional[str] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Schedules a message.
+        https://docs.slack.dev/reference/methods/chat.scheduleMessage
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "post_at": post_at,
+                "text": text,
+                "as_user": as_user,
+                "attachments": attachments,
+                "blocks": blocks,
+                "thread_ts": thread_ts,
+                "reply_broadcast": reply_broadcast,
+                "parse": parse,
+                "unfurl_links": unfurl_links,
+                "unfurl_media": unfurl_media,
+                "link_names": link_names,
+                "metadata": metadata,
+                "markdown_text": markdown_text,
+            }
+        )
+        _parse_web_class_objects(kwargs)
+        kwargs = _remove_none_values(kwargs)
+        _warn_if_message_text_content_is_missing("chat.scheduleMessage", kwargs)
+        # NOTE: intentionally using json over params for the API methods using blocks/attachments
+        return self.api_call("chat.scheduleMessage", json=kwargs)
+
+    def chat_scheduledMessages_list(
+        self,
+        *,
+        channel: Optional[str] = None,
+        cursor: Optional[str] = None,
+        latest: Optional[str] = None,
+        limit: Optional[int] = None,
+        oldest: Optional[str] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Lists all scheduled messages.
+        https://docs.slack.dev/reference/methods/chat.scheduledMessages.list
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "cursor": cursor,
+                "latest": latest,
+                "limit": limit,
+                "oldest": oldest,
+                "team_id": team_id,
+            }
+        )
+        return self.api_call("chat.scheduledMessages.list", params=kwargs)
+
+    def chat_startStream(
+        self,
+        *,
+        channel: str,
+        thread_ts: str,
+        markdown_text: Optional[str] = None,
+        recipient_team_id: Optional[str] = None,
+        recipient_user_id: Optional[str] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Starts a new streaming conversation.
+        https://docs.slack.dev/reference/methods/chat.startStream
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "thread_ts": thread_ts,
+                "markdown_text": markdown_text,
+                "recipient_team_id": recipient_team_id,
+                "recipient_user_id": recipient_user_id,
+            }
+        )
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("chat.startStream", json=kwargs)
+
+    def chat_stopStream(
+        self,
+        *,
+        channel: str,
+        ts: str,
+        markdown_text: Optional[str] = None,
+        blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None,
+        metadata: Optional[Union[Dict, Metadata]] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Stops a streaming conversation.
+        https://docs.slack.dev/reference/methods/chat.stopStream
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "ts": ts,
+                "markdown_text": markdown_text,
+                "blocks": blocks,
+                "metadata": metadata,
+            }
+        )
+        _parse_web_class_objects(kwargs)
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("chat.stopStream", json=kwargs)
+
+    def chat_unfurl(
+        self,
+        *,
+        channel: Optional[str] = None,
+        ts: Optional[str] = None,
+        source: Optional[str] = None,
+        unfurl_id: Optional[str] = None,
+        unfurls: Optional[Dict[str, Dict]] = None,  # or user_auth_*
+        metadata: Optional[Union[Dict, EventAndEntityMetadata]] = None,
+        user_auth_blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None,
+        user_auth_message: Optional[str] = None,
+        user_auth_required: Optional[bool] = None,
+        user_auth_url: Optional[str] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Provide custom unfurl behavior for user-posted URLs.
+        https://docs.slack.dev/reference/methods/chat.unfurl
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "ts": ts,
+                "source": source,
+                "unfurl_id": unfurl_id,
+                "unfurls": unfurls,
+                "metadata": metadata,
+                "user_auth_blocks": user_auth_blocks,
+                "user_auth_message": user_auth_message,
+                "user_auth_required": user_auth_required,
+                "user_auth_url": user_auth_url,
+            }
+        )
+        _parse_web_class_objects(kwargs)  # for user_auth_blocks
+        kwargs = _remove_none_values(kwargs)
+        # NOTE: intentionally using json over params for API methods using blocks/attachments
+        return self.api_call("chat.unfurl", json=kwargs)
+
+    def chat_update(
+        self,
+        *,
+        channel: str,
+        ts: str,
+        text: Optional[str] = None,
+        attachments: Optional[Union[str, Sequence[Union[Dict, Attachment]]]] = None,
+        blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None,
+        as_user: Optional[bool] = None,
+        file_ids: Optional[Union[str, Sequence[str]]] = None,
+        link_names: Optional[bool] = None,
+        parse: Optional[str] = None,  # none, full
+        reply_broadcast: Optional[bool] = None,
+        metadata: Optional[Union[Dict, Metadata]] = None,
+        markdown_text: Optional[str] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Updates a message in a channel.
+        https://docs.slack.dev/reference/methods/chat.update
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "ts": ts,
+                "text": text,
+                "attachments": attachments,
+                "blocks": blocks,
+                "as_user": as_user,
+                "link_names": link_names,
+                "parse": parse,
+                "reply_broadcast": reply_broadcast,
+                "metadata": metadata,
+                "markdown_text": markdown_text,
+            }
+        )
+        if isinstance(file_ids, (list, tuple)):
+            kwargs.update({"file_ids": ",".join(file_ids)})
+        else:
+            kwargs.update({"file_ids": file_ids})
+        _parse_web_class_objects(kwargs)
+        kwargs = _remove_none_values(kwargs)
+        _warn_if_message_text_content_is_missing("chat.update", kwargs)
+        # NOTE: intentionally using json over params for API methods using blocks/attachments
+        return self.api_call("chat.update", json=kwargs)
+
+    def conversations_acceptSharedInvite(
+        self,
+        *,
+        channel_name: str,
+        channel_id: Optional[str] = None,
+        invite_id: Optional[str] = None,
+        free_trial_accepted: Optional[bool] = None,
+        is_private: Optional[bool] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Accepts an invitation to a Slack Connect channel.
+        https://docs.slack.dev/reference/methods/conversations.acceptSharedInvite
+        """
+        if channel_id is None and invite_id is None:
+            raise e.SlackRequestError("Either channel_id or invite_id must be provided.")
+        kwargs.update(
+            {
+                "channel_name": channel_name,
+                "channel_id": channel_id,
+                "invite_id": invite_id,
+                "free_trial_accepted": free_trial_accepted,
+                "is_private": is_private,
+                "team_id": team_id,
+            }
+        )
+        return self.api_call("conversations.acceptSharedInvite", http_verb="POST", params=kwargs)
+
+    def conversations_approveSharedInvite(
+        self,
+        *,
+        invite_id: str,
+        target_team: Optional[str] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Approves an invitation to a Slack Connect channel.
+        https://docs.slack.dev/reference/methods/conversations.approveSharedInvite
+        """
+        kwargs.update({"invite_id": invite_id, "target_team": target_team})
+        return self.api_call("conversations.approveSharedInvite", http_verb="POST", params=kwargs)
+
+    def conversations_archive(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Archives a conversation.
+        https://docs.slack.dev/reference/methods/conversations.archive
+        """
+        kwargs.update({"channel": channel})
+        return self.api_call("conversations.archive", params=kwargs)
+
+    def conversations_close(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Closes a direct message or multi-person direct message.
+        https://docs.slack.dev/reference/methods/conversations.close
+        """
+        kwargs.update({"channel": channel})
+        return self.api_call("conversations.close", params=kwargs)
+
+    def conversations_create(
+        self,
+        *,
+        name: str,
+        is_private: Optional[bool] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Initiates a public or private channel-based conversation
+        https://docs.slack.dev/reference/methods/conversations.create
+        """
+        kwargs.update({"name": name, "is_private": is_private, "team_id": team_id})
+        return self.api_call("conversations.create", params=kwargs)
+
+    def conversations_declineSharedInvite(
+        self,
+        *,
+        invite_id: str,
+        target_team: Optional[str] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Declines a Slack Connect channel invite.
+        https://docs.slack.dev/reference/methods/conversations.declineSharedInvite
+        """
+        kwargs.update({"invite_id": invite_id, "target_team": target_team})
+        return self.api_call("conversations.declineSharedInvite", http_verb="GET", params=kwargs)
+
+    def conversations_externalInvitePermissions_set(
+        self, *, action: str, channel: str, target_team: str, **kwargs
+    ) -> Union[Future, SlackResponse]:
+        """Sets a team in a shared External Limited channel to a shared Slack Connect channel or vice versa.
+        https://docs.slack.dev/reference/methods/conversations.externalInvitePermissions.set
+        """
+        kwargs.update(
+            {
+                "action": action,
+                "channel": channel,
+                "target_team": target_team,
+            }
+        )
+        return self.api_call("conversations.externalInvitePermissions.set", params=kwargs)
+
+    def conversations_history(
+        self,
+        *,
+        channel: str,
+        cursor: Optional[str] = None,
+        inclusive: Optional[bool] = None,
+        include_all_metadata: Optional[bool] = None,
+        latest: Optional[str] = None,
+        limit: Optional[int] = None,
+        oldest: Optional[str] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Fetches a conversation's history of messages and events.
+        https://docs.slack.dev/reference/methods/conversations.history
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "cursor": cursor,
+                "inclusive": inclusive,
+                "include_all_metadata": include_all_metadata,
+                "limit": limit,
+                "latest": latest,
+                "oldest": oldest,
+            }
+        )
+        return self.api_call("conversations.history", http_verb="GET", params=kwargs)
+
+    def conversations_info(
+        self,
+        *,
+        channel: str,
+        include_locale: Optional[bool] = None,
+        include_num_members: Optional[bool] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Retrieve information about a conversation.
+        https://docs.slack.dev/reference/methods/conversations.info
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "include_locale": include_locale,
+                "include_num_members": include_num_members,
+            }
+        )
+        return self.api_call("conversations.info", http_verb="GET", params=kwargs)
+
+    def conversations_invite(
+        self,
+        *,
+        channel: str,
+        users: Union[str, Sequence[str]],
+        force: Optional[bool] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Invites users to a channel.
+        https://docs.slack.dev/reference/methods/conversations.invite
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "force": force,
+            }
+        )
+        if isinstance(users, (list, tuple)):
+            kwargs.update({"users": ",".join(users)})
+        else:
+            kwargs.update({"users": users})
+        return self.api_call("conversations.invite", params=kwargs)
+
+    def conversations_inviteShared(
+        self,
+        *,
+        channel: str,
+        emails: Optional[Union[str, Sequence[str]]] = None,
+        user_ids: Optional[Union[str, Sequence[str]]] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Sends an invitation to a Slack Connect channel.
+        https://docs.slack.dev/reference/methods/conversations.inviteShared
+        """
+        if emails is None and user_ids is None:
+            raise e.SlackRequestError("Either emails or user ids must be provided.")
+        kwargs.update({"channel": channel})
+        if isinstance(emails, (list, tuple)):
+            kwargs.update({"emails": ",".join(emails)})
+        else:
+            kwargs.update({"emails": emails})
+        if isinstance(user_ids, (list, tuple)):
+            kwargs.update({"user_ids": ",".join(user_ids)})
+        else:
+            kwargs.update({"user_ids": user_ids})
+        return self.api_call("conversations.inviteShared", http_verb="GET", params=kwargs)
+
+    def conversations_join(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Joins an existing conversation.
+        https://docs.slack.dev/reference/methods/conversations.join
+        """
+        kwargs.update({"channel": channel})
+        return self.api_call("conversations.join", params=kwargs)
+
+    def conversations_kick(
+        self,
+        *,
+        channel: str,
+        user: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Removes a user from a conversation.
+        https://docs.slack.dev/reference/methods/conversations.kick
+        """
+        kwargs.update({"channel": channel, "user": user})
+        return self.api_call("conversations.kick", params=kwargs)
+
+    def conversations_leave(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Leaves a conversation.
+        https://docs.slack.dev/reference/methods/conversations.leave
+        """
+        kwargs.update({"channel": channel})
+        return self.api_call("conversations.leave", params=kwargs)
+
+    def conversations_list(
+        self,
+        *,
+        cursor: Optional[str] = None,
+        exclude_archived: Optional[bool] = None,
+        limit: Optional[int] = None,
+        team_id: Optional[str] = None,
+        types: Optional[Union[str, Sequence[str]]] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Lists all channels in a Slack team.
+        https://docs.slack.dev/reference/methods/conversations.list
+        """
+        kwargs.update(
+            {
+                "cursor": cursor,
+                "exclude_archived": exclude_archived,
+                "limit": limit,
+                "team_id": team_id,
+            }
+        )
+        if isinstance(types, (list, tuple)):
+            kwargs.update({"types": ",".join(types)})
+        else:
+            kwargs.update({"types": types})
+        return self.api_call("conversations.list", http_verb="GET", params=kwargs)
+
+    def conversations_listConnectInvites(
+        self,
+        *,
+        count: Optional[int] = None,
+        cursor: Optional[str] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """List shared channel invites that have been generated
+        or received but have not yet been approved by all parties.
+        https://docs.slack.dev/reference/methods/conversations.listConnectInvites
+        """
+        kwargs.update({"count": count, "cursor": cursor, "team_id": team_id})
+        return self.api_call("conversations.listConnectInvites", params=kwargs)
+
+    def conversations_mark(
+        self,
+        *,
+        channel: str,
+        ts: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Sets the read cursor in a channel.
+        https://docs.slack.dev/reference/methods/conversations.mark
+        """
+        kwargs.update({"channel": channel, "ts": ts})
+        return self.api_call("conversations.mark", params=kwargs)
+
+    def conversations_members(
+        self,
+        *,
+        channel: str,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Retrieve members of a conversation.
+        https://docs.slack.dev/reference/methods/conversations.members
+        """
+        kwargs.update({"channel": channel, "cursor": cursor, "limit": limit})
+        return self.api_call("conversations.members", http_verb="GET", params=kwargs)
+
+    def conversations_open(
+        self,
+        *,
+        channel: Optional[str] = None,
+        return_im: Optional[bool] = None,
+        users: Optional[Union[str, Sequence[str]]] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Opens or resumes a direct message or multi-person direct message.
+        https://docs.slack.dev/reference/methods/conversations.open
+        """
+        if channel is None and users is None:
+            raise e.SlackRequestError("Either channel or users must be provided.")
+        kwargs.update({"channel": channel, "return_im": return_im})
+        if isinstance(users, (list, tuple)):
+            kwargs.update({"users": ",".join(users)})
+        else:
+            kwargs.update({"users": users})
+        return self.api_call("conversations.open", params=kwargs)
+
+    def conversations_rename(
+        self,
+        *,
+        channel: str,
+        name: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Renames a conversation.
+        https://docs.slack.dev/reference/methods/conversations.rename
+        """
+        kwargs.update({"channel": channel, "name": name})
+        return self.api_call("conversations.rename", params=kwargs)
+
+    def conversations_replies(
+        self,
+        *,
+        channel: str,
+        ts: str,
+        cursor: Optional[str] = None,
+        inclusive: Optional[bool] = None,
+        include_all_metadata: Optional[bool] = None,
+        latest: Optional[str] = None,
+        limit: Optional[int] = None,
+        oldest: Optional[str] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Retrieve a thread of messages posted to a conversation
+        https://docs.slack.dev/reference/methods/conversations.replies
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "ts": ts,
+                "cursor": cursor,
+                "inclusive": inclusive,
+                "include_all_metadata": include_all_metadata,
+                "limit": limit,
+                "latest": latest,
+                "oldest": oldest,
+            }
+        )
+        return self.api_call("conversations.replies", http_verb="GET", params=kwargs)
+
+    def conversations_requestSharedInvite_approve(
+        self,
+        *,
+        invite_id: str,
+        channel_id: Optional[str] = None,
+        is_external_limited: Optional[str] = None,
+        message: Optional[Dict[str, Any]] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Approve a request to add an external user to a channel. This also sends them a Slack Connect invite.
+        https://docs.slack.dev/reference/methods/conversations.requestSharedInvite.approve
+        """
+        kwargs.update(
+            {
+                "invite_id": invite_id,
+                "channel_id": channel_id,
+                "is_external_limited": is_external_limited,
+            }
+        )
+        if message is not None:
+            kwargs.update({"message": json.dumps(message)})
+        return self.api_call("conversations.requestSharedInvite.approve", params=kwargs)
+
+    def conversations_requestSharedInvite_deny(
+        self,
+        *,
+        invite_id: str,
+        message: Optional[str] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Deny a request to invite an external user to a channel.
+        https://docs.slack.dev/reference/methods/conversations.requestSharedInvite.deny
+        """
+        kwargs.update({"invite_id": invite_id, "message": message})
+        return self.api_call("conversations.requestSharedInvite.deny", params=kwargs)
+
+    def conversations_requestSharedInvite_list(
+        self,
+        *,
+        cursor: Optional[str] = None,
+        include_approved: Optional[bool] = None,
+        include_denied: Optional[bool] = None,
+        include_expired: Optional[bool] = None,
+        invite_ids: Optional[Union[str, Sequence[str]]] = None,
+        limit: Optional[int] = None,
+        user_id: Optional[str] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Lists requests to add external users to channels with ability to filter.
+        https://docs.slack.dev/reference/methods/conversations.requestSharedInvite.list
+        """
+        kwargs.update(
+            {
+                "cursor": cursor,
+                "include_approved": include_approved,
+                "include_denied": include_denied,
+                "include_expired": include_expired,
+                "limit": limit,
+                "user_id": user_id,
+            }
+        )
+        if invite_ids is not None:
+            if isinstance(invite_ids, (list, tuple)):
+                kwargs.update({"invite_ids": ",".join(invite_ids)})
+            else:
+                kwargs.update({"invite_ids": invite_ids})
+        return self.api_call("conversations.requestSharedInvite.list", params=kwargs)
+
+    def conversations_setPurpose(
+        self,
+        *,
+        channel: str,
+        purpose: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Sets the purpose for a conversation.
+        https://docs.slack.dev/reference/methods/conversations.setPurpose
+        """
+        kwargs.update({"channel": channel, "purpose": purpose})
+        return self.api_call("conversations.setPurpose", params=kwargs)
+
+    def conversations_setTopic(
+        self,
+        *,
+        channel: str,
+        topic: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Sets the topic for a conversation.
+        https://docs.slack.dev/reference/methods/conversations.setTopic
+        """
+        kwargs.update({"channel": channel, "topic": topic})
+        return self.api_call("conversations.setTopic", params=kwargs)
+
+    def conversations_unarchive(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Reverses conversation archival.
+        https://docs.slack.dev/reference/methods/conversations.unarchive
+        """
+        kwargs.update({"channel": channel})
+        return self.api_call("conversations.unarchive", params=kwargs)
+
+    def conversations_canvases_create(
+        self,
+        *,
+        channel_id: str,
+        document_content: Dict[str, str],
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Create a Channel Canvas for a channel
+        https://docs.slack.dev/reference/methods/conversations.canvases.create
+        """
+        kwargs.update({"channel_id": channel_id, "document_content": document_content})
+        return self.api_call("conversations.canvases.create", json=kwargs)
+
+    def dialog_open(
+        self,
+        *,
+        dialog: Dict[str, Any],
+        trigger_id: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Open a dialog with a user.
+        https://docs.slack.dev/reference/methods/dialog.open
+        """
+        kwargs.update({"dialog": dialog, "trigger_id": trigger_id})
+        kwargs = _remove_none_values(kwargs)
+        # NOTE: As the dialog can be a dict, this API call works only with json format.
+        return self.api_call("dialog.open", json=kwargs)
+
+    def dnd_endDnd(
+        self,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Ends the current user's Do Not Disturb session immediately.
+        https://docs.slack.dev/reference/methods/dnd.endDnd
+        """
+        return self.api_call("dnd.endDnd", params=kwargs)
+
+    def dnd_endSnooze(
+        self,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Ends the current user's snooze mode immediately.
+        https://docs.slack.dev/reference/methods/dnd.endSnooze
+        """
+        return self.api_call("dnd.endSnooze", params=kwargs)
+
+    def dnd_info(
+        self,
+        *,
+        team_id: Optional[str] = None,
+        user: Optional[str] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Retrieves a user's current Do Not Disturb status.
+        https://docs.slack.dev/reference/methods/dnd.info
+        """
+        kwargs.update({"team_id": team_id, "user": user})
+        return self.api_call("dnd.info", http_verb="GET", params=kwargs)
+
+    def dnd_setSnooze(
+        self,
+        *,
+        num_minutes: Union[int, str],
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Turns on Do Not Disturb mode for the current user, or changes its duration.
+        https://docs.slack.dev/reference/methods/dnd.setSnooze
+        """
+        kwargs.update({"num_minutes": num_minutes})
+        return self.api_call("dnd.setSnooze", http_verb="GET", params=kwargs)
+
+    def dnd_teamInfo(
+        self,
+        users: Union[str, Sequence[str]],
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Retrieves the Do Not Disturb status for users on a team.
+        https://docs.slack.dev/reference/methods/dnd.teamInfo
+        """
+        if isinstance(users, (list, tuple)):
+            kwargs.update({"users": ",".join(users)})
+        else:
+            kwargs.update({"users": users})
+        kwargs.update({"team_id": team_id})
+        return self.api_call("dnd.teamInfo", http_verb="GET", params=kwargs)
+
+    def emoji_list(
+        self,
+        include_categories: Optional[bool] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Lists custom emoji for a team.
+        https://docs.slack.dev/reference/methods/emoji.list
+        """
+        kwargs.update({"include_categories": include_categories})
+        return self.api_call("emoji.list", http_verb="GET", params=kwargs)
+
+    def entity_presentDetails(
+        self,
+        trigger_id: str,
+        metadata: Optional[Union[Dict, EntityMetadata]] = None,
+        user_auth_required: Optional[bool] = None,
+        user_auth_url: Optional[str] = None,
+        error: Optional[Dict[str, Any]] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Provides entity details for the flexpane.
+        https://docs.slack.dev/reference/methods/entity.presentDetails/
+        """
+        kwargs.update({"trigger_id": trigger_id})
+        if metadata is not None:
+            kwargs.update({"metadata": metadata})
+        if user_auth_required is not None:
+            kwargs.update({"user_auth_required": user_auth_required})
+        if user_auth_url is not None:
+            kwargs.update({"user_auth_url": user_auth_url})
+        if error is not None:
+            kwargs.update({"error": error})
+        _parse_web_class_objects(kwargs)
+        return self.api_call("entity.presentDetails", json=kwargs)
+
+    def files_comments_delete(
+        self,
+        *,
+        file: str,
+        id: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Deletes an existing comment on a file.
+        https://docs.slack.dev/reference/methods/files.comments.delete
+        """
+        kwargs.update({"file": file, "id": id})
+        return self.api_call("files.comments.delete", params=kwargs)
+
+    def files_delete(
+        self,
+        *,
+        file: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Deletes a file.
+        https://docs.slack.dev/reference/methods/files.delete
+        """
+        kwargs.update({"file": file})
+        return self.api_call("files.delete", params=kwargs)
+
+    def files_info(
+        self,
+        *,
+        file: str,
+        count: Optional[int] = None,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        page: Optional[int] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Gets information about a team file.
+        https://docs.slack.dev/reference/methods/files.info
+        """
+        kwargs.update(
+            {
+                "file": file,
+                "count": count,
+                "cursor": cursor,
+                "limit": limit,
+                "page": page,
+            }
+        )
+        return self.api_call("files.info", http_verb="GET", params=kwargs)
+
+    def files_list(
+        self,
+        *,
+        channel: Optional[str] = None,
+        count: Optional[int] = None,
+        page: Optional[int] = None,
+        show_files_hidden_by_limit: Optional[bool] = None,
+        team_id: Optional[str] = None,
+        ts_from: Optional[str] = None,
+        ts_to: Optional[str] = None,
+        types: Optional[Union[str, Sequence[str]]] = None,
+        user: Optional[str] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Lists & filters team files.
+        https://docs.slack.dev/reference/methods/files.list
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "count": count,
+                "page": page,
+                "show_files_hidden_by_limit": show_files_hidden_by_limit,
+                "team_id": team_id,
+                "ts_from": ts_from,
+                "ts_to": ts_to,
+                "user": user,
+            }
+        )
+        if isinstance(types, (list, tuple)):
+            kwargs.update({"types": ",".join(types)})
+        else:
+            kwargs.update({"types": types})
+        return self.api_call("files.list", http_verb="GET", params=kwargs)
+
+    def files_remote_info(
+        self,
+        *,
+        external_id: Optional[str] = None,
+        file: Optional[str] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Retrieve information about a remote file added to Slack.
+        https://docs.slack.dev/reference/methods/files.remote.info
+        """
+        kwargs.update({"external_id": external_id, "file": file})
+        return self.api_call("files.remote.info", http_verb="GET", params=kwargs)
+
+    def files_remote_list(
+        self,
+        *,
+        channel: Optional[str] = None,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        ts_from: Optional[str] = None,
+        ts_to: Optional[str] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Retrieve information about a remote file added to Slack.
+        https://docs.slack.dev/reference/methods/files.remote.list
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "cursor": cursor,
+                "limit": limit,
+                "ts_from": ts_from,
+                "ts_to": ts_to,
+            }
+        )
+        return self.api_call("files.remote.list", http_verb="GET", params=kwargs)
+
+    def files_remote_add(
+        self,
+        *,
+        external_id: str,
+        external_url: str,
+        title: str,
+        filetype: Optional[str] = None,
+        indexable_file_contents: Optional[Union[str, bytes, IOBase]] = None,
+        preview_image: Optional[Union[str, bytes, IOBase]] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Adds a file from a remote service.
+        https://docs.slack.dev/reference/methods/files.remote.add
+        """
+        kwargs.update(
+            {
+                "external_id": external_id,
+                "external_url": external_url,
+                "title": title,
+                "filetype": filetype,
+            }
+        )
+        files = None
+        # preview_image (file): Preview of the document via multipart/form-data.
+        if preview_image is not None or indexable_file_contents is not None:
+            files = {
+                "preview_image": preview_image,
+                "indexable_file_contents": indexable_file_contents,
+            }
+
+        return self.api_call(
+            # Intentionally using "POST" method over "GET" here
+            "files.remote.add",
+            http_verb="POST",
+            data=kwargs,
+            files=files,
+        )
+
+    def files_remote_update(
+        self,
+        *,
+        external_id: Optional[str] = None,
+        external_url: Optional[str] = None,
+        file: Optional[str] = None,
+        title: Optional[str] = None,
+        filetype: Optional[str] = None,
+        indexable_file_contents: Optional[str] = None,
+        preview_image: Optional[str] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Updates an existing remote file.
+        https://docs.slack.dev/reference/methods/files.remote.update
+        """
+        kwargs.update(
+            {
+                "external_id": external_id,
+                "external_url": external_url,
+                "file": file,
+                "title": title,
+                "filetype": filetype,
+            }
+        )
+        files = None
+        # preview_image (file): Preview of the document via multipart/form-data.
+        if preview_image is not None or indexable_file_contents is not None:
+            files = {
+                "preview_image": preview_image,
+                "indexable_file_contents": indexable_file_contents,
+            }
+
+        return self.api_call(
+            # Intentionally using "POST" method over "GET" here
+            "files.remote.update",
+            http_verb="POST",
+            data=kwargs,
+            files=files,
+        )
+
+    def files_remote_remove(
+        self,
+        *,
+        external_id: Optional[str] = None,
+        file: Optional[str] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Remove a remote file.
+        https://docs.slack.dev/reference/methods/files.remote.remove
+        """
+        kwargs.update({"external_id": external_id, "file": file})
+        return self.api_call("files.remote.remove", http_verb="POST", params=kwargs)
+
+    def files_remote_share(
+        self,
+        *,
+        channels: Union[str, Sequence[str]],
+        external_id: Optional[str] = None,
+        file: Optional[str] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Share a remote file into a channel.
+        https://docs.slack.dev/reference/methods/files.remote.share
+        """
+        if external_id is None and file is None:
+            raise e.SlackRequestError("Either external_id or file must be provided.")
+        if isinstance(channels, (list, tuple)):
+            kwargs.update({"channels": ",".join(channels)})
+        else:
+            kwargs.update({"channels": channels})
+        kwargs.update({"external_id": external_id, "file": file})
+        return self.api_call("files.remote.share", http_verb="GET", params=kwargs)
+
+    def files_revokePublicURL(
+        self,
+        *,
+        file: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Revokes public/external sharing access for a file
+        https://docs.slack.dev/reference/methods/files.revokePublicURL
+        """
+        kwargs.update({"file": file})
+        return self.api_call("files.revokePublicURL", params=kwargs)
+
+    def files_sharedPublicURL(
+        self,
+        *,
+        file: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Enables a file for public/external sharing.
+        https://docs.slack.dev/reference/methods/files.sharedPublicURL
+        """
+        kwargs.update({"file": file})
+        return self.api_call("files.sharedPublicURL", params=kwargs)
+
+    def files_upload(
+        self,
+        *,
+        file: Optional[Union[str, bytes, IOBase]] = None,
+        content: Optional[Union[str, bytes]] = None,
+        filename: Optional[str] = None,
+        filetype: Optional[str] = None,
+        initial_comment: Optional[str] = None,
+        thread_ts: Optional[str] = None,
+        title: Optional[str] = None,
+        channels: Optional[Union[str, Sequence[str]]] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Uploads or creates a file.
+        https://docs.slack.dev/reference/methods/files.upload
+        """
+        _print_files_upload_v2_suggestion()
+
+        if file is None and content is None:
+            raise e.SlackRequestError("The file or content argument must be specified.")
+        if file is not None and content is not None:
+            raise e.SlackRequestError("You cannot specify both the file and the content argument.")
+
+        if isinstance(channels, (list, tuple)):
+            kwargs.update({"channels": ",".join(channels)})
+        else:
+            kwargs.update({"channels": channels})
+        kwargs.update(
+            {
+                "filename": filename,
+                "filetype": filetype,
+                "initial_comment": initial_comment,
+                "thread_ts": thread_ts,
+                "title": title,
+            }
+        )
+        if file:
+            if kwargs.get("filename") is None and isinstance(file, str):
+                # use the local filename if filename is missing
+                if kwargs.get("filename") is None:
+                    kwargs["filename"] = file.split(os.path.sep)[-1]
+            return self.api_call("files.upload", files={"file": file}, data=kwargs)
+        else:
+            kwargs["content"] = content
+            return self.api_call("files.upload", data=kwargs)
+
+    def files_upload_v2(
+        self,
+        *,
+        # for sending a single file
+        filename: Optional[str] = None,  # you can skip this only when sending along with content parameter
+        file: Optional[Union[str, bytes, IOBase, os.PathLike]] = None,
+        content: Optional[Union[str, bytes]] = None,
+        title: Optional[str] = None,
+        alt_txt: Optional[str] = None,
+        snippet_type: Optional[str] = None,
+        # To upload multiple files at a time
+        file_uploads: Optional[List[Dict[str, Any]]] = None,
+        channel: Optional[str] = None,
+        channels: Optional[List[str]] = None,
+        initial_comment: Optional[str] = None,
+        thread_ts: Optional[str] = None,
+        request_file_info: bool = True,  # since v3.23, this flag is no longer necessary
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """This wrapper method provides an easy way to upload files using the following endpoints:
+
+        - step1: https://docs.slack.dev/reference/methods/files.getUploadURLExternal
+
+        - step2: "https://files.slack.com/upload/v1/..." URLs returned from files.getUploadURLExternal API
+
+        - step3: https://docs.slack.dev/reference/methods/files.completeUploadExternal
+            and https://docs.slack.dev/reference/methods/files.info
+
+        """
+        if file is None and content is None and file_uploads is None:
+            raise e.SlackRequestError("Any of file, content, and file_uploads must be specified.")
+        if file is not None and content is not None:
+            raise e.SlackRequestError("You cannot specify both the file and the content argument.")
+
+        # deprecated arguments:
+        filetype = kwargs.get("filetype")
+
+        if filetype is not None:
+            warnings.warn("The filetype parameter is no longer supported. Please remove it from the arguments.")
+
+        # step1: files.getUploadURLExternal per file
+        files: List[Dict[str, Any]] = []
+        if file_uploads is not None:
+            for f in file_uploads:
+                files.append(_to_v2_file_upload_item(f))
+        else:
+            f = _to_v2_file_upload_item(
+                {
+                    "filename": filename,
+                    "file": file,
+                    "content": content,
+                    "title": title,
+                    "alt_txt": alt_txt,
+                    "snippet_type": snippet_type,
+                }
+            )
+            files.append(f)
+
+        for f in files:
+            url_response = self.files_getUploadURLExternal(
+                filename=f.get("filename"),  # type: ignore[arg-type]
+                length=f.get("length"),  # type: ignore[arg-type]
+                alt_txt=f.get("alt_txt"),
+                snippet_type=f.get("snippet_type"),
+                token=kwargs.get("token"),
+            )
+            _validate_for_legacy_client(url_response)
+            f["file_id"] = url_response.get("file_id")  # type: ignore[union-attr, unused-ignore]
+            f["upload_url"] = url_response.get("upload_url")  # type: ignore[union-attr, unused-ignore]
+
+        # step2: "https://files.slack.com/upload/v1/..." per file
+        for f in files:
+            upload_result = self._upload_file(
+                url=f["upload_url"],
+                data=f["data"],
+                logger=self._logger,
+                timeout=self.timeout,
+                proxy=self.proxy,
+                ssl=self.ssl,
+            )
+            if upload_result.status != 200:
+                status = upload_result.status
+                body = upload_result.body
+                message = (
+                    "Failed to upload a file "
+                    f"(status: {status}, body: {body}, filename: {f.get('filename')}, title: {f.get('title')})"
+                )
+                raise e.SlackRequestError(message)
+
+        # step3: files.completeUploadExternal with all the sets of (file_id + title)
+        completion = self.files_completeUploadExternal(
+            files=[{"id": f["file_id"], "title": f["title"]} for f in files],
+            channel_id=channel,
+            channels=channels,
+            initial_comment=initial_comment,
+            thread_ts=thread_ts,
+            **kwargs,
+        )
+        if len(completion.get("files")) == 1:  # type: ignore[arg-type, union-attr, unused-ignore]
+            completion.data["file"] = completion.get("files")[0]  # type: ignore[index, union-attr, unused-ignore]
+        return completion
+
+    def files_getUploadURLExternal(
+        self,
+        *,
+        filename: str,
+        length: int,
+        alt_txt: Optional[str] = None,
+        snippet_type: Optional[str] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Gets a URL for an edge external upload.
+        https://docs.slack.dev/reference/methods/files.getUploadURLExternal
+        """
+        kwargs.update(
+            {
+                "filename": filename,
+                "length": length,
+                "alt_txt": alt_txt,
+                "snippet_type": snippet_type,
+            }
+        )
+        return self.api_call("files.getUploadURLExternal", params=kwargs)
+
+    def files_completeUploadExternal(
+        self,
+        *,
+        files: List[Dict[str, str]],
+        channel_id: Optional[str] = None,
+        channels: Optional[List[str]] = None,
+        initial_comment: Optional[str] = None,
+        thread_ts: Optional[str] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Finishes an upload started with files.getUploadURLExternal.
+        https://docs.slack.dev/reference/methods/files.completeUploadExternal
+        """
+        _files = [{k: v for k, v in f.items() if v is not None} for f in files]
+        kwargs.update(
+            {
+                "files": json.dumps(_files),
+                "channel_id": channel_id,
+                "initial_comment": initial_comment,
+                "thread_ts": thread_ts,
+            }
+        )
+        if channels:
+            kwargs["channels"] = ",".join(channels)
+        return self.api_call("files.completeUploadExternal", params=kwargs)
+
+    def functions_completeSuccess(
+        self,
+        *,
+        function_execution_id: str,
+        outputs: Dict[str, Any],
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Signal the successful completion of a function
+        https://docs.slack.dev/reference/methods/functions.completeSuccess
+        """
+        kwargs.update({"function_execution_id": function_execution_id, "outputs": json.dumps(outputs)})
+        return self.api_call("functions.completeSuccess", params=kwargs)
+
+    def functions_completeError(
+        self,
+        *,
+        function_execution_id: str,
+        error: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Signal the failure to execute a function
+        https://docs.slack.dev/reference/methods/functions.completeError
+        """
+        kwargs.update({"function_execution_id": function_execution_id, "error": error})
+        return self.api_call("functions.completeError", params=kwargs)
+
+    # --------------------------
+    # Deprecated: groups.*
+    # You can use conversations.* APIs instead.
+    # https://docs.slack.dev/changelog/2020-01-deprecating-antecedents-to-the-conversations-api/
+    # --------------------------
+
+    def groups_archive(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Archives a private channel."""
+        kwargs.update({"channel": channel})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("groups.archive", json=kwargs)
+
+    def groups_create(
+        self,
+        *,
+        name: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Creates a private channel."""
+        kwargs.update({"name": name})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("groups.create", json=kwargs)
+
+    def groups_createChild(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Clones and archives a private channel."""
+        kwargs.update({"channel": channel})
+        return self.api_call("groups.createChild", http_verb="GET", params=kwargs)
+
+    def groups_history(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Fetches history of messages and events from a private channel."""
+        kwargs.update({"channel": channel})
+        return self.api_call("groups.history", http_verb="GET", params=kwargs)
+
+    def groups_info(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Gets information about a private channel."""
+        kwargs.update({"channel": channel})
+        return self.api_call("groups.info", http_verb="GET", params=kwargs)
+
+    def groups_invite(
+        self,
+        *,
+        channel: str,
+        user: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Invites a user to a private channel."""
+        kwargs.update({"channel": channel, "user": user})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("groups.invite", json=kwargs)
+
+    def groups_kick(
+        self,
+        *,
+        channel: str,
+        user: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Removes a user from a private channel."""
+        kwargs.update({"channel": channel, "user": user})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("groups.kick", json=kwargs)
+
+    def groups_leave(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Leaves a private channel."""
+        kwargs.update({"channel": channel})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("groups.leave", json=kwargs)
+
+    def groups_list(
+        self,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Lists private channels that the calling user has access to."""
+        return self.api_call("groups.list", http_verb="GET", params=kwargs)
+
+    def groups_mark(
+        self,
+        *,
+        channel: str,
+        ts: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Sets the read cursor in a private channel."""
+        kwargs.update({"channel": channel, "ts": ts})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("groups.mark", json=kwargs)
+
+    def groups_open(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Opens a private channel."""
+        kwargs.update({"channel": channel})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("groups.open", json=kwargs)
+
+    def groups_rename(
+        self,
+        *,
+        channel: str,
+        name: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Renames a private channel."""
+        kwargs.update({"channel": channel, "name": name})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("groups.rename", json=kwargs)
+
+    def groups_replies(
+        self,
+        *,
+        channel: str,
+        thread_ts: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Retrieve a thread of messages posted to a private channel"""
+        kwargs.update({"channel": channel, "thread_ts": thread_ts})
+        return self.api_call("groups.replies", http_verb="GET", params=kwargs)
+
+    def groups_setPurpose(
+        self,
+        *,
+        channel: str,
+        purpose: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Sets the purpose for a private channel."""
+        kwargs.update({"channel": channel, "purpose": purpose})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("groups.setPurpose", json=kwargs)
+
+    def groups_setTopic(
+        self,
+        *,
+        channel: str,
+        topic: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Sets the topic for a private channel."""
+        kwargs.update({"channel": channel, "topic": topic})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("groups.setTopic", json=kwargs)
+
+    def groups_unarchive(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Unarchives a private channel."""
+        kwargs.update({"channel": channel})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("groups.unarchive", json=kwargs)
+
+    # --------------------------
+    # Deprecated: im.*
+    # You can use conversations.* APIs instead.
+    # https://docs.slack.dev/changelog/2020-01-deprecating-antecedents-to-the-conversations-api/
+    # --------------------------
+
+    def im_close(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Close a direct message channel."""
+        kwargs.update({"channel": channel})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("im.close", json=kwargs)
+
+    def im_history(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Fetches history of messages and events from direct message channel."""
+        kwargs.update({"channel": channel})
+        return self.api_call("im.history", http_verb="GET", params=kwargs)
+
+    def im_list(
+        self,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Lists direct message channels for the calling user."""
+        return self.api_call("im.list", http_verb="GET", params=kwargs)
+
+    def im_mark(
+        self,
+        *,
+        channel: str,
+        ts: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Sets the read cursor in a direct message channel."""
+        kwargs.update({"channel": channel, "ts": ts})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("im.mark", json=kwargs)
+
+    def im_open(
+        self,
+        *,
+        user: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Opens a direct message channel."""
+        kwargs.update({"user": user})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("im.open", json=kwargs)
+
+    def im_replies(
+        self,
+        *,
+        channel: str,
+        thread_ts: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Retrieve a thread of messages posted to a direct message conversation"""
+        kwargs.update({"channel": channel, "thread_ts": thread_ts})
+        return self.api_call("im.replies", http_verb="GET", params=kwargs)
+
+    # --------------------------
+
+    def migration_exchange(
+        self,
+        *,
+        users: Union[str, Sequence[str]],
+        team_id: Optional[str] = None,
+        to_old: Optional[bool] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """For Enterprise Grid workspaces, map local user IDs to global user IDs
+        https://docs.slack.dev/reference/methods/migration.exchange
+        """
+        if isinstance(users, (list, tuple)):
+            kwargs.update({"users": ",".join(users)})
+        else:
+            kwargs.update({"users": users})
+        kwargs.update({"team_id": team_id, "to_old": to_old})
+        return self.api_call("migration.exchange", http_verb="GET", params=kwargs)
+
+    # --------------------------
+    # Deprecated: mpim.*
+    # You can use conversations.* APIs instead.
+    # https://docs.slack.dev/changelog/2020-01-deprecating-antecedents-to-the-conversations-api/
+    # --------------------------
+
+    def mpim_close(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Closes a multiparty direct message channel."""
+        kwargs.update({"channel": channel})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("mpim.close", json=kwargs)
+
+    def mpim_history(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Fetches history of messages and events from a multiparty direct message."""
+        kwargs.update({"channel": channel})
+        return self.api_call("mpim.history", http_verb="GET", params=kwargs)
+
+    def mpim_list(
+        self,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Lists multiparty direct message channels for the calling user."""
+        return self.api_call("mpim.list", http_verb="GET", params=kwargs)
+
+    def mpim_mark(
+        self,
+        *,
+        channel: str,
+        ts: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Sets the read cursor in a multiparty direct message channel."""
+        kwargs.update({"channel": channel, "ts": ts})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("mpim.mark", json=kwargs)
+
+    def mpim_open(
+        self,
+        *,
+        users: Union[str, Sequence[str]],
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """This method opens a multiparty direct message."""
+        if isinstance(users, (list, tuple)):
+            kwargs.update({"users": ",".join(users)})
+        else:
+            kwargs.update({"users": users})
+        return self.api_call("mpim.open", params=kwargs)
+
+    def mpim_replies(
+        self,
+        *,
+        channel: str,
+        thread_ts: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Retrieve a thread of messages posted to a direct message conversation from a
+        multiparty direct message.
+        """
+        kwargs.update({"channel": channel, "thread_ts": thread_ts})
+        return self.api_call("mpim.replies", http_verb="GET", params=kwargs)
+
+    # --------------------------
+
+    def oauth_v2_access(
+        self,
+        *,
+        client_id: str,
+        client_secret: str,
+        # This field is required when processing the OAuth redirect URL requests
+        # while it's absent for token rotation
+        code: Optional[str] = None,
+        redirect_uri: Optional[str] = None,
+        # This field is required for token rotation
+        grant_type: Optional[str] = None,
+        # This field is required for token rotation
+        refresh_token: Optional[str] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Exchanges a temporary OAuth verifier code for an access token.
+        https://docs.slack.dev/reference/methods/oauth.v2.access
+        """
+        if redirect_uri is not None:
+            kwargs.update({"redirect_uri": redirect_uri})
+        if code is not None:
+            kwargs.update({"code": code})
+        if grant_type is not None:
+            kwargs.update({"grant_type": grant_type})
+        if refresh_token is not None:
+            kwargs.update({"refresh_token": refresh_token})
+        return self.api_call(
+            "oauth.v2.access",
+            data=kwargs,
+            auth={"client_id": client_id, "client_secret": client_secret},
+        )
+
+    def oauth_access(
+        self,
+        *,
+        client_id: str,
+        client_secret: str,
+        code: str,
+        redirect_uri: Optional[str] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Exchanges a temporary OAuth verifier code for an access token.
+        https://docs.slack.dev/reference/methods/oauth.access
+        """
+        if redirect_uri is not None:
+            kwargs.update({"redirect_uri": redirect_uri})
+        kwargs.update({"code": code})
+        return self.api_call(
+            "oauth.access",
+            data=kwargs,
+            auth={"client_id": client_id, "client_secret": client_secret},
+        )
+
+    def oauth_v2_exchange(
+        self,
+        *,
+        token: str,
+        client_id: str,
+        client_secret: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Exchanges a legacy access token for a new expiring access token and refresh token
+        https://docs.slack.dev/reference/methods/oauth.v2.exchange
+        """
+        kwargs.update({"client_id": client_id, "client_secret": client_secret, "token": token})
+        return self.api_call("oauth.v2.exchange", params=kwargs)
+
+    def openid_connect_token(
+        self,
+        client_id: str,
+        client_secret: str,
+        code: Optional[str] = None,
+        redirect_uri: Optional[str] = None,
+        grant_type: Optional[str] = None,
+        refresh_token: Optional[str] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Exchanges a temporary OAuth verifier code for an access token for Sign in with Slack.
+        https://docs.slack.dev/reference/methods/openid.connect.token
+        """
+        if redirect_uri is not None:
+            kwargs.update({"redirect_uri": redirect_uri})
+        if code is not None:
+            kwargs.update({"code": code})
+        if grant_type is not None:
+            kwargs.update({"grant_type": grant_type})
+        if refresh_token is not None:
+            kwargs.update({"refresh_token": refresh_token})
+        return self.api_call(
+            "openid.connect.token",
+            data=kwargs,
+            auth={"client_id": client_id, "client_secret": client_secret},
+        )
+
+    def openid_connect_userInfo(
+        self,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Get the identity of a user who has authorized Sign in with Slack.
+        https://docs.slack.dev/reference/methods/openid.connect.userInfo
+        """
+        return self.api_call("openid.connect.userInfo", params=kwargs)
+
+    def pins_add(
+        self,
+        *,
+        channel: str,
+        timestamp: Optional[str] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Pins an item to a channel.
+        https://docs.slack.dev/reference/methods/pins.add
+        """
+        kwargs.update({"channel": channel, "timestamp": timestamp})
+        return self.api_call("pins.add", params=kwargs)
+
+    def pins_list(
+        self,
+        *,
+        channel: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Lists items pinned to a channel.
+        https://docs.slack.dev/reference/methods/pins.list
+        """
+        kwargs.update({"channel": channel})
+        return self.api_call("pins.list", http_verb="GET", params=kwargs)
+
+    def pins_remove(
+        self,
+        *,
+        channel: str,
+        timestamp: Optional[str] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Un-pins an item from a channel.
+        https://docs.slack.dev/reference/methods/pins.remove
+        """
+        kwargs.update({"channel": channel, "timestamp": timestamp})
+        return self.api_call("pins.remove", params=kwargs)
+
+    def reactions_add(
+        self,
+        *,
+        channel: str,
+        name: str,
+        timestamp: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Adds a reaction to an item.
+        https://docs.slack.dev/reference/methods/reactions.add
+        """
+        kwargs.update({"channel": channel, "name": name, "timestamp": timestamp})
+        return self.api_call("reactions.add", params=kwargs)
+
+    def reactions_get(
+        self,
+        *,
+        channel: Optional[str] = None,
+        file: Optional[str] = None,
+        file_comment: Optional[str] = None,
+        full: Optional[bool] = None,
+        timestamp: Optional[str] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Gets reactions for an item.
+        https://docs.slack.dev/reference/methods/reactions.get
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "file": file,
+                "file_comment": file_comment,
+                "full": full,
+                "timestamp": timestamp,
+            }
+        )
+        return self.api_call("reactions.get", http_verb="GET", params=kwargs)
+
+    def reactions_list(
+        self,
+        *,
+        count: Optional[int] = None,
+        cursor: Optional[str] = None,
+        full: Optional[bool] = None,
+        limit: Optional[int] = None,
+        page: Optional[int] = None,
+        team_id: Optional[str] = None,
+        user: Optional[str] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Lists reactions made by a user.
+        https://docs.slack.dev/reference/methods/reactions.list
+        """
+        kwargs.update(
+            {
+                "count": count,
+                "cursor": cursor,
+                "full": full,
+                "limit": limit,
+                "page": page,
+                "team_id": team_id,
+                "user": user,
+            }
+        )
+        return self.api_call("reactions.list", http_verb="GET", params=kwargs)
+
+    def reactions_remove(
+        self,
+        *,
+        name: str,
+        channel: Optional[str] = None,
+        file: Optional[str] = None,
+        file_comment: Optional[str] = None,
+        timestamp: Optional[str] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Removes a reaction from an item.
+        https://docs.slack.dev/reference/methods/reactions.remove
+        """
+        kwargs.update(
+            {
+                "name": name,
+                "channel": channel,
+                "file": file,
+                "file_comment": file_comment,
+                "timestamp": timestamp,
+            }
+        )
+        return self.api_call("reactions.remove", params=kwargs)
+
+    def reminders_add(
+        self,
+        *,
+        text: str,
+        time: str,
+        team_id: Optional[str] = None,
+        user: Optional[str] = None,
+        recurrence: Optional[str] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Creates a reminder.
+        https://docs.slack.dev/reference/methods/reminders.add
+        """
+        kwargs.update(
+            {
+                "text": text,
+                "time": time,
+                "team_id": team_id,
+                "user": user,
+                "recurrence": recurrence,
+            }
+        )
+        return self.api_call("reminders.add", params=kwargs)
+
+    def reminders_complete(
+        self,
+        *,
+        reminder: str,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Marks a reminder as complete.
+        https://docs.slack.dev/reference/methods/reminders.complete
+        """
+        kwargs.update({"reminder": reminder, "team_id": team_id})
+        return self.api_call("reminders.complete", params=kwargs)
+
+    def reminders_delete(
+        self,
+        *,
+        reminder: str,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Deletes a reminder.
+        https://docs.slack.dev/reference/methods/reminders.delete
+        """
+        kwargs.update({"reminder": reminder, "team_id": team_id})
+        return self.api_call("reminders.delete", params=kwargs)
+
+    def reminders_info(
+        self,
+        *,
+        reminder: str,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Gets information about a reminder.
+        https://docs.slack.dev/reference/methods/reminders.info
+        """
+        kwargs.update({"reminder": reminder, "team_id": team_id})
+        return self.api_call("reminders.info", http_verb="GET", params=kwargs)
+
+    def reminders_list(
+        self,
+        *,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Lists all reminders created by or for a given user.
+        https://docs.slack.dev/reference/methods/reminders.list
+        """
+        kwargs.update({"team_id": team_id})
+        return self.api_call("reminders.list", http_verb="GET", params=kwargs)
+
+    def rtm_connect(
+        self,
+        *,
+        batch_presence_aware: Optional[bool] = None,
+        presence_sub: Optional[bool] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Starts a Real Time Messaging session.
+        https://docs.slack.dev/reference/methods/rtm.connect
+        """
+        kwargs.update({"batch_presence_aware": batch_presence_aware, "presence_sub": presence_sub})
+        return self.api_call("rtm.connect", http_verb="GET", params=kwargs)
+
+    def rtm_start(
+        self,
+        *,
+        batch_presence_aware: Optional[bool] = None,
+        include_locale: Optional[bool] = None,
+        mpim_aware: Optional[bool] = None,
+        no_latest: Optional[bool] = None,
+        no_unreads: Optional[bool] = None,
+        presence_sub: Optional[bool] = None,
+        simple_latest: Optional[bool] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Starts a Real Time Messaging session.
+        https://docs.slack.dev/reference/methods/rtm.start
+        """
+        kwargs.update(
+            {
+                "batch_presence_aware": batch_presence_aware,
+                "include_locale": include_locale,
+                "mpim_aware": mpim_aware,
+                "no_latest": no_latest,
+                "no_unreads": no_unreads,
+                "presence_sub": presence_sub,
+                "simple_latest": simple_latest,
+            }
+        )
+        return self.api_call("rtm.start", http_verb="GET", params=kwargs)
+
+    def search_all(
+        self,
+        *,
+        query: str,
+        count: Optional[int] = None,
+        highlight: Optional[bool] = None,
+        page: Optional[int] = None,
+        sort: Optional[str] = None,
+        sort_dir: Optional[str] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Searches for messages and files matching a query.
+        https://docs.slack.dev/reference/methods/search.all
+        """
+        kwargs.update(
+            {
+                "query": query,
+                "count": count,
+                "highlight": highlight,
+                "page": page,
+                "sort": sort,
+                "sort_dir": sort_dir,
+                "team_id": team_id,
+            }
+        )
+        return self.api_call("search.all", http_verb="GET", params=kwargs)
+
+    def search_files(
+        self,
+        *,
+        query: str,
+        count: Optional[int] = None,
+        highlight: Optional[bool] = None,
+        page: Optional[int] = None,
+        sort: Optional[str] = None,
+        sort_dir: Optional[str] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Searches for files matching a query.
+        https://docs.slack.dev/reference/methods/search.files
+        """
+        kwargs.update(
+            {
+                "query": query,
+                "count": count,
+                "highlight": highlight,
+                "page": page,
+                "sort": sort,
+                "sort_dir": sort_dir,
+                "team_id": team_id,
+            }
+        )
+        return self.api_call("search.files", http_verb="GET", params=kwargs)
+
+    def search_messages(
+        self,
+        *,
+        query: str,
+        count: Optional[int] = None,
+        cursor: Optional[str] = None,
+        highlight: Optional[bool] = None,
+        page: Optional[int] = None,
+        sort: Optional[str] = None,
+        sort_dir: Optional[str] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Searches for messages matching a query.
+        https://docs.slack.dev/reference/methods/search.messages
+        """
+        kwargs.update(
+            {
+                "query": query,
+                "count": count,
+                "cursor": cursor,
+                "highlight": highlight,
+                "page": page,
+                "sort": sort,
+                "sort_dir": sort_dir,
+                "team_id": team_id,
+            }
+        )
+        return self.api_call("search.messages", http_verb="GET", params=kwargs)
+
+    def slackLists_access_delete(
+        self,
+        *,
+        list_id: str,
+        channel_ids: Optional[List[str]] = None,
+        user_ids: Optional[List[str]] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Revoke access to a List for specified entities.
+        https://docs.slack.dev/reference/methods/slackLists.access.delete
+        """
+        kwargs.update({"list_id": list_id, "channel_ids": channel_ids, "user_ids": user_ids})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("slackLists.access.delete", json=kwargs)
+
+    def slackLists_access_set(
+        self,
+        *,
+        list_id: str,
+        access_level: str,
+        channel_ids: Optional[List[str]] = None,
+        user_ids: Optional[List[str]] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Set the access level to a List for specified entities.
+        https://docs.slack.dev/reference/methods/slackLists.access.set
+        """
+        kwargs.update({"list_id": list_id, "access_level": access_level, "channel_ids": channel_ids, "user_ids": user_ids})
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("slackLists.access.set", json=kwargs)
+
+    def slackLists_create(
+        self,
+        *,
+        name: str,
+        description_blocks: Optional[Union[str, Sequence[Union[Dict, RichTextBlock]]]] = None,
+        schema: Optional[List[Dict[str, Any]]] = None,
+        copy_from_list_id: Optional[str] = None,
+        include_copied_list_records: Optional[bool] = None,
+        todo_mode: Optional[bool] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Creates a List.
+        https://docs.slack.dev/reference/methods/slackLists.create
+        """
+        kwargs.update(
+            {
+                "name": name,
+                "description_blocks": description_blocks,
+                "schema": schema,
+                "copy_from_list_id": copy_from_list_id,
+                "include_copied_list_records": include_copied_list_records,
+                "todo_mode": todo_mode,
+            }
+        )
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("slackLists.create", json=kwargs)
+
+    def slackLists_download_get(
+        self,
+        *,
+        list_id: str,
+        job_id: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Retrieve List download URL from an export job to download List contents.
+        https://docs.slack.dev/reference/methods/slackLists.download.get
+        """
+        kwargs.update(
+            {
+                "list_id": list_id,
+                "job_id": job_id,
+            }
+        )
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("slackLists.download.get", json=kwargs)
+
+    def slackLists_download_start(
+        self,
+        *,
+        list_id: str,
+        include_archived: Optional[bool] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Initiate a job to export List contents.
+        https://docs.slack.dev/reference/methods/slackLists.download.start
+        """
+        kwargs.update(
+            {
+                "list_id": list_id,
+                "include_archived": include_archived,
+            }
+        )
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("slackLists.download.start", json=kwargs)
+
+    def slackLists_items_create(
+        self,
+        *,
+        list_id: str,
+        duplicated_item_id: Optional[str] = None,
+        parent_item_id: Optional[str] = None,
+        initial_fields: Optional[List[Dict[str, Any]]] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Add a new item to an existing List.
+        https://docs.slack.dev/reference/methods/slackLists.items.create
+        """
+        kwargs.update(
+            {
+                "list_id": list_id,
+                "duplicated_item_id": duplicated_item_id,
+                "parent_item_id": parent_item_id,
+                "initial_fields": initial_fields,
+            }
+        )
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("slackLists.items.create", json=kwargs)
+
+    def slackLists_items_delete(
+        self,
+        *,
+        list_id: str,
+        id: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Deletes an item from an existing List.
+        https://docs.slack.dev/reference/methods/slackLists.items.delete
+        """
+        kwargs.update(
+            {
+                "list_id": list_id,
+                "id": id,
+            }
+        )
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("slackLists.items.delete", json=kwargs)
+
+    def slackLists_items_deleteMultiple(
+        self,
+        *,
+        list_id: str,
+        ids: List[str],
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Deletes multiple items from an existing List.
+        https://docs.slack.dev/reference/methods/slackLists.items.deleteMultiple
+        """
+        kwargs.update(
+            {
+                "list_id": list_id,
+                "ids": ids,
+            }
+        )
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("slackLists.items.deleteMultiple", json=kwargs)
+
+    def slackLists_items_info(
+        self,
+        *,
+        list_id: str,
+        id: str,
+        include_is_subscribed: Optional[bool] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Get a row from a List.
+        https://docs.slack.dev/reference/methods/slackLists.items.info
+        """
+        kwargs.update(
+            {
+                "list_id": list_id,
+                "id": id,
+                "include_is_subscribed": include_is_subscribed,
+            }
+        )
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("slackLists.items.info", json=kwargs)
+
+    def slackLists_items_list(
+        self,
+        *,
+        list_id: str,
+        limit: Optional[int] = None,
+        cursor: Optional[str] = None,
+        archived: Optional[bool] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Get records from a List.
+        https://docs.slack.dev/reference/methods/slackLists.items.list
+        """
+        kwargs.update(
+            {
+                "list_id": list_id,
+                "limit": limit,
+                "cursor": cursor,
+                "archived": archived,
+            }
+        )
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("slackLists.items.list", json=kwargs)
+
+    def slackLists_items_update(
+        self,
+        *,
+        list_id: str,
+        cells: List[Dict[str, Any]],
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Updates cells in a List.
+        https://docs.slack.dev/reference/methods/slackLists.items.update
+        """
+        kwargs.update(
+            {
+                "list_id": list_id,
+                "cells": cells,
+            }
+        )
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("slackLists.items.update", json=kwargs)
+
+    def slackLists_update(
+        self,
+        *,
+        id: str,
+        name: Optional[str] = None,
+        description_blocks: Optional[Union[str, Sequence[Union[Dict, RichTextBlock]]]] = None,
+        todo_mode: Optional[bool] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Update a List.
+        https://docs.slack.dev/reference/methods/slackLists.update
+        """
+        kwargs.update(
+            {
+                "id": id,
+                "name": name,
+                "description_blocks": description_blocks,
+                "todo_mode": todo_mode,
+            }
+        )
+        kwargs = _remove_none_values(kwargs)
+        return self.api_call("slackLists.update", json=kwargs)
+
+    def stars_add(
+        self,
+        *,
+        channel: Optional[str] = None,
+        file: Optional[str] = None,
+        file_comment: Optional[str] = None,
+        timestamp: Optional[str] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Adds a star to an item.
+        https://docs.slack.dev/reference/methods/stars.add
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "file": file,
+                "file_comment": file_comment,
+                "timestamp": timestamp,
+            }
+        )
+        return self.api_call("stars.add", params=kwargs)
+
+    def stars_list(
+        self,
+        *,
+        count: Optional[int] = None,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        page: Optional[int] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Lists stars for a user.
+        https://docs.slack.dev/reference/methods/stars.list
+        """
+        kwargs.update(
+            {
+                "count": count,
+                "cursor": cursor,
+                "limit": limit,
+                "page": page,
+                "team_id": team_id,
+            }
+        )
+        return self.api_call("stars.list", http_verb="GET", params=kwargs)
+
+    def stars_remove(
+        self,
+        *,
+        channel: Optional[str] = None,
+        file: Optional[str] = None,
+        file_comment: Optional[str] = None,
+        timestamp: Optional[str] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Removes a star from an item.
+        https://docs.slack.dev/reference/methods/stars.remove
+        """
+        kwargs.update(
+            {
+                "channel": channel,
+                "file": file,
+                "file_comment": file_comment,
+                "timestamp": timestamp,
+            }
+        )
+        return self.api_call("stars.remove", params=kwargs)
+
+    def team_accessLogs(
+        self,
+        *,
+        before: Optional[Union[int, str]] = None,
+        count: Optional[Union[int, str]] = None,
+        page: Optional[Union[int, str]] = None,
+        team_id: Optional[str] = None,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Gets the access logs for the current team.
+        https://docs.slack.dev/reference/methods/team.accessLogs
+        """
+        kwargs.update(
+            {
+                "before": before,
+                "count": count,
+                "page": page,
+                "team_id": team_id,
+                "cursor": cursor,
+                "limit": limit,
+            }
+        )
+        return self.api_call("team.accessLogs", http_verb="GET", params=kwargs)
+
+    def team_billableInfo(
+        self,
+        *,
+        team_id: Optional[str] = None,
+        user: Optional[str] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Gets billable users information for the current team.
+        https://docs.slack.dev/reference/methods/team.billableInfo
+        """
+        kwargs.update({"team_id": team_id, "user": user})
+        return self.api_call("team.billableInfo", http_verb="GET", params=kwargs)
+
+    def team_billing_info(
+        self,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Reads a workspace's billing plan information.
+        https://docs.slack.dev/reference/methods/team.billing.info
+        """
+        return self.api_call("team.billing.info", params=kwargs)
+
+    def team_externalTeams_disconnect(
+        self,
+        *,
+        target_team: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Disconnects an external organization.
+        https://docs.slack.dev/reference/methods/team.externalTeams.disconnect
+        """
+        kwargs.update(
+            {
+                "target_team": target_team,
+            }
+        )
+        return self.api_call("team.externalTeams.disconnect", params=kwargs)
+
+    def team_externalTeams_list(
+        self,
+        *,
+        connection_status_filter: Optional[str] = None,
+        slack_connect_pref_filter: Optional[Sequence[str]] = None,
+        sort_direction: Optional[str] = None,
+        sort_field: Optional[str] = None,
+        workspace_filter: Optional[Sequence[str]] = None,
+        cursor: Optional[str] = None,
+        limit: Optional[int] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Returns a list of all the external teams connected and details about the connection.
+        https://docs.slack.dev/reference/methods/team.externalTeams.list
+        """
+        kwargs.update(
+            {
+                "connection_status_filter": connection_status_filter,
+                "sort_direction": sort_direction,
+                "sort_field": sort_field,
+                "cursor": cursor,
+                "limit": limit,
+            }
+        )
+        if slack_connect_pref_filter is not None:
+            if isinstance(slack_connect_pref_filter, (list, tuple)):
+                kwargs.update({"slack_connect_pref_filter": ",".join(slack_connect_pref_filter)})
+            else:
+                kwargs.update({"slack_connect_pref_filter": slack_connect_pref_filter})
+        if workspace_filter is not None:
+            if isinstance(workspace_filter, (list, tuple)):
+                kwargs.update({"workspace_filter": ",".join(workspace_filter)})
+            else:
+                kwargs.update({"workspace_filter": workspace_filter})
+        return self.api_call("team.externalTeams.list", http_verb="GET", params=kwargs)
+
+    def team_info(
+        self,
+        *,
+        team: Optional[str] = None,
+        domain: Optional[str] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Gets information about the current team.
+        https://docs.slack.dev/reference/methods/team.info
+        """
+        kwargs.update({"team": team, "domain": domain})
+        return self.api_call("team.info", http_verb="GET", params=kwargs)
+
+    def team_integrationLogs(
+        self,
+        *,
+        app_id: Optional[str] = None,
+        change_type: Optional[str] = None,
+        count: Optional[Union[int, str]] = None,
+        page: Optional[Union[int, str]] = None,
+        service_id: Optional[str] = None,
+        team_id: Optional[str] = None,
+        user: Optional[str] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Gets the integration logs for the current team.
+        https://docs.slack.dev/reference/methods/team.integrationLogs
+        """
+        kwargs.update(
+            {
+                "app_id": app_id,
+                "change_type": change_type,
+                "count": count,
+                "page": page,
+                "service_id": service_id,
+                "team_id": team_id,
+                "user": user,
+            }
+        )
+        return self.api_call("team.integrationLogs", http_verb="GET", params=kwargs)
+
+    def team_profile_get(
+        self,
+        *,
+        visibility: Optional[str] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Retrieve a team's profile.
+        https://docs.slack.dev/reference/methods/team.profile.get
+        """
+        kwargs.update({"visibility": visibility})
+        return self.api_call("team.profile.get", http_verb="GET", params=kwargs)
+
+    def team_preferences_list(
+        self,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Retrieve a list of a workspace's team preferences.
+        https://docs.slack.dev/reference/methods/team.preferences.list
+        """
+        return self.api_call("team.preferences.list", params=kwargs)
+
+    def usergroups_create(
+        self,
+        *,
+        name: str,
+        channels: Optional[Union[str, Sequence[str]]] = None,
+        description: Optional[str] = None,
+        handle: Optional[str] = None,
+        include_count: Optional[bool] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Create a User Group
+        https://docs.slack.dev/reference/methods/usergroups.create
+        """
+        kwargs.update(
+            {
+                "name": name,
+                "description": description,
+                "handle": handle,
+                "include_count": include_count,
+                "team_id": team_id,
+            }
+        )
+        if isinstance(channels, (list, tuple)):
+            kwargs.update({"channels": ",".join(channels)})
+        else:
+            kwargs.update({"channels": channels})
+        return self.api_call("usergroups.create", params=kwargs)
+
+    def usergroups_disable(
+        self,
+        *,
+        usergroup: str,
+        include_count: Optional[bool] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Disable an existing User Group
+        https://docs.slack.dev/reference/methods/usergroups.disable
+        """
+        kwargs.update({"usergroup": usergroup, "include_count": include_count, "team_id": team_id})
+        return self.api_call("usergroups.disable", params=kwargs)
+
+    def usergroups_enable(
+        self,
+        *,
+        usergroup: str,
+        include_count: Optional[bool] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Enable a User Group
+        https://docs.slack.dev/reference/methods/usergroups.enable
+        """
+        kwargs.update({"usergroup": usergroup, "include_count": include_count, "team_id": team_id})
+        return self.api_call("usergroups.enable", params=kwargs)
+
+    def usergroups_list(
+        self,
+        *,
+        include_count: Optional[bool] = None,
+        include_disabled: Optional[bool] = None,
+        include_users: Optional[bool] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """List all User Groups for a team
+        https://docs.slack.dev/reference/methods/usergroups.list
+        """
+        kwargs.update(
+            {
+                "include_count": include_count,
+                "include_disabled": include_disabled,
+                "include_users": include_users,
+                "team_id": team_id,
+            }
+        )
+        return self.api_call("usergroups.list", http_verb="GET", params=kwargs)
+
+    def usergroups_update(
+        self,
+        *,
+        usergroup: str,
+        channels: Optional[Union[str, Sequence[str]]] = None,
+        description: Optional[str] = None,
+        handle: Optional[str] = None,
+        include_count: Optional[bool] = None,
+        name: Optional[str] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Update an existing User Group
+        https://docs.slack.dev/reference/methods/usergroups.update
+        """
+        kwargs.update(
+            {
+                "usergroup": usergroup,
+                "description": description,
+                "handle": handle,
+                "include_count": include_count,
+                "name": name,
+                "team_id": team_id,
+            }
+        )
+        if isinstance(channels, (list, tuple)):
+            kwargs.update({"channels": ",".join(channels)})
+        else:
+            kwargs.update({"channels": channels})
+        return self.api_call("usergroups.update", params=kwargs)
+
+    def usergroups_users_list(
+        self,
+        *,
+        usergroup: str,
+        include_disabled: Optional[bool] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """List all users in a User Group
+        https://docs.slack.dev/reference/methods/usergroups.users.list
+        """
+        kwargs.update(
+            {
+                "usergroup": usergroup,
+                "include_disabled": include_disabled,
+                "team_id": team_id,
+            }
+        )
+        return self.api_call("usergroups.users.list", http_verb="GET", params=kwargs)
+
+    def usergroups_users_update(
+        self,
+        *,
+        usergroup: str,
+        users: Union[str, Sequence[str]],
+        include_count: Optional[bool] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Update the list of users for a User Group
+        https://docs.slack.dev/reference/methods/usergroups.users.update
+        """
+        kwargs.update(
+            {
+                "usergroup": usergroup,
+                "include_count": include_count,
+                "team_id": team_id,
+            }
+        )
+        if isinstance(users, (list, tuple)):
+            kwargs.update({"users": ",".join(users)})
+        else:
+            kwargs.update({"users": users})
+        return self.api_call("usergroups.users.update", params=kwargs)
+
+    def users_conversations(
+        self,
+        *,
+        cursor: Optional[str] = None,
+        exclude_archived: Optional[bool] = None,
+        limit: Optional[int] = None,
+        team_id: Optional[str] = None,
+        types: Optional[Union[str, Sequence[str]]] = None,
+        user: Optional[str] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """List conversations the calling user may access.
+        https://docs.slack.dev/reference/methods/users.conversations
+        """
+        kwargs.update(
+            {
+                "cursor": cursor,
+                "exclude_archived": exclude_archived,
+                "limit": limit,
+                "team_id": team_id,
+                "user": user,
+            }
+        )
+        if isinstance(types, (list, tuple)):
+            kwargs.update({"types": ",".join(types)})
+        else:
+            kwargs.update({"types": types})
+        return self.api_call("users.conversations", http_verb="GET", params=kwargs)
+
+    def users_deletePhoto(
+        self,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Delete the user profile photo
+        https://docs.slack.dev/reference/methods/users.deletePhoto
+        """
+        return self.api_call("users.deletePhoto", http_verb="GET", params=kwargs)
+
+    def users_getPresence(
+        self,
+        *,
+        user: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Gets user presence information.
+        https://docs.slack.dev/reference/methods/users.getPresence
+        """
+        kwargs.update({"user": user})
+        return self.api_call("users.getPresence", http_verb="GET", params=kwargs)
+
+    def users_identity(
+        self,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Get a user's identity.
+        https://docs.slack.dev/reference/methods/users.identity
+        """
+        return self.api_call("users.identity", http_verb="GET", params=kwargs)
+
+    def users_info(
+        self,
+        *,
+        user: str,
+        include_locale: Optional[bool] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Gets information about a user.
+        https://docs.slack.dev/reference/methods/users.info
+        """
+        kwargs.update({"user": user, "include_locale": include_locale})
+        return self.api_call("users.info", http_verb="GET", params=kwargs)
+
+    def users_list(
+        self,
+        *,
+        cursor: Optional[str] = None,
+        include_locale: Optional[bool] = None,
+        limit: Optional[int] = None,
+        team_id: Optional[str] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Lists all users in a Slack team.
+        https://docs.slack.dev/reference/methods/users.list
+        """
+        kwargs.update(
+            {
+                "cursor": cursor,
+                "include_locale": include_locale,
+                "limit": limit,
+                "team_id": team_id,
+            }
+        )
+        return self.api_call("users.list", http_verb="GET", params=kwargs)
+
+    def users_lookupByEmail(
+        self,
+        *,
+        email: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Find a user with an email address.
+        https://docs.slack.dev/reference/methods/users.lookupByEmail
+        """
+        kwargs.update({"email": email})
+        return self.api_call("users.lookupByEmail", http_verb="GET", params=kwargs)
+
+    def users_setPhoto(
+        self,
+        *,
+        image: Union[str, IOBase],
+        crop_w: Optional[Union[int, str]] = None,
+        crop_x: Optional[Union[int, str]] = None,
+        crop_y: Optional[Union[int, str]] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Set the user profile photo
+        https://docs.slack.dev/reference/methods/users.setPhoto
+        """
+        kwargs.update({"crop_w": crop_w, "crop_x": crop_x, "crop_y": crop_y})
+        return self.api_call("users.setPhoto", files={"image": image}, data=kwargs)
+
+    def users_setPresence(
+        self,
+        *,
+        presence: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Manually sets user presence.
+        https://docs.slack.dev/reference/methods/users.setPresence
+        """
+        kwargs.update({"presence": presence})
+        return self.api_call("users.setPresence", params=kwargs)
+
+    def users_discoverableContacts_lookup(
+        self,
+        email: str,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Lookup an email address to see if someone is on Slack
+        https://docs.slack.dev/reference/methods/users.discoverableContacts.lookup
+        """
+        kwargs.update({"email": email})
+        return self.api_call("users.discoverableContacts.lookup", params=kwargs)
+
+    def users_profile_get(
+        self,
+        *,
+        user: Optional[str] = None,
+        include_labels: Optional[bool] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Retrieves a user's profile information.
+        https://docs.slack.dev/reference/methods/users.profile.get
+        """
+        kwargs.update({"user": user, "include_labels": include_labels})
+        return self.api_call("users.profile.get", http_verb="GET", params=kwargs)
+
+    def users_profile_set(
+        self,
+        *,
+        name: Optional[str] = None,
+        value: Optional[str] = None,
+        user: Optional[str] = None,
+        profile: Optional[Dict] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Set the profile information for a user.
+        https://docs.slack.dev/reference/methods/users.profile.set
+        """
+        kwargs.update(
+            {
+                "name": name,
+                "profile": profile,
+                "user": user,
+                "value": value,
+            }
+        )
+        kwargs = _remove_none_values(kwargs)
+        # NOTE: Intentionally using json for the "profile" parameter
+        return self.api_call("users.profile.set", json=kwargs)
+
+    def views_open(
+        self,
+        *,
+        trigger_id: Optional[str] = None,
+        interactivity_pointer: Optional[str] = None,
+        view: Union[dict, View],
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Open a view for a user.
+        https://docs.slack.dev/reference/methods/views.open
+        See https://docs.slack.dev/surfaces/modals/ for details.
+        """
+        kwargs.update({"trigger_id": trigger_id, "interactivity_pointer": interactivity_pointer})
+        if isinstance(view, View):
+            kwargs.update({"view": view.to_dict()})
+        else:
+            kwargs.update({"view": view})
+        kwargs = _remove_none_values(kwargs)
+        # NOTE: Intentionally using json for the "view" parameter
+        return self.api_call("views.open", json=kwargs)
+
+    def views_push(
+        self,
+        *,
+        trigger_id: Optional[str] = None,
+        interactivity_pointer: Optional[str] = None,
+        view: Union[dict, View],
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Push a view onto the stack of a root view.
+        Push a new view onto the existing view stack by passing a view
+        payload and a valid trigger_id generated from an interaction
+        within the existing modal.
+        Read the modals documentation (https://docs.slack.dev/surfaces/modals/)
+        to learn more about the lifecycle and intricacies of views.
+        https://docs.slack.dev/reference/methods/views.push
+        """
+        kwargs.update({"trigger_id": trigger_id, "interactivity_pointer": interactivity_pointer})
+        if isinstance(view, View):
+            kwargs.update({"view": view.to_dict()})
+        else:
+            kwargs.update({"view": view})
+        kwargs = _remove_none_values(kwargs)
+        # NOTE: Intentionally using json for the "view" parameter
+        return self.api_call("views.push", json=kwargs)
+
+    def views_update(
+        self,
+        *,
+        view: Union[dict, View],
+        external_id: Optional[str] = None,
+        view_id: Optional[str] = None,
+        hash: Optional[str] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Update an existing view.
+        Update a view by passing a new view definition along with the
+        view_id returned in views.open or the external_id.
+        See the modals documentation (https://docs.slack.dev/surfaces/modals/#updating_views)
+        to learn more about updating views and avoiding race conditions with the hash argument.
+        https://docs.slack.dev/reference/methods/views.update
+        """
+        if isinstance(view, View):
+            kwargs.update({"view": view.to_dict()})
+        else:
+            kwargs.update({"view": view})
+        if external_id:
+            kwargs.update({"external_id": external_id})
+        elif view_id:
+            kwargs.update({"view_id": view_id})
+        else:
+            raise e.SlackRequestError("Either view_id or external_id is required.")
+        kwargs.update({"hash": hash})
+        kwargs = _remove_none_values(kwargs)
+        # NOTE: Intentionally using json for the "view" parameter
+        return self.api_call("views.update", json=kwargs)
+
+    def views_publish(
+        self,
+        *,
+        user_id: str,
+        view: Union[dict, View],
+        hash: Optional[str] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Publish a static view for a User.
+        Create or update the view that comprises an
+        app's Home tab (https://docs.slack.dev/surfaces/app-home/)
+        https://docs.slack.dev/reference/methods/views.publish
+        """
+        kwargs.update({"user_id": user_id, "hash": hash})
+        if isinstance(view, View):
+            kwargs.update({"view": view.to_dict()})
+        else:
+            kwargs.update({"view": view})
+        kwargs = _remove_none_values(kwargs)
+        # NOTE: Intentionally using json for the "view" parameter
+        return self.api_call("views.publish", json=kwargs)
+
+    def workflows_featured_add(
+        self,
+        *,
+        channel_id: str,
+        trigger_ids: Union[str, Sequence[str]],
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Add featured workflows to a channel.
+        https://docs.slack.dev/reference/methods/workflows.featured.add
+        """
+        kwargs.update({"channel_id": channel_id})
+        if isinstance(trigger_ids, (list, tuple)):
+            kwargs.update({"trigger_ids": ",".join(trigger_ids)})
+        else:
+            kwargs.update({"trigger_ids": trigger_ids})
+        return self.api_call("workflows.featured.add", params=kwargs)
+
+    def workflows_featured_list(
+        self,
+        *,
+        channel_ids: Union[str, Sequence[str]],
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """List the featured workflows for specified channels.
+        https://docs.slack.dev/reference/methods/workflows.featured.list
+        """
+        if isinstance(channel_ids, (list, tuple)):
+            kwargs.update({"channel_ids": ",".join(channel_ids)})
+        else:
+            kwargs.update({"channel_ids": channel_ids})
+        return self.api_call("workflows.featured.list", params=kwargs)
+
+    def workflows_featured_remove(
+        self,
+        *,
+        channel_id: str,
+        trigger_ids: Union[str, Sequence[str]],
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Remove featured workflows from a channel.
+        https://docs.slack.dev/reference/methods/workflows.featured.remove
+        """
+        kwargs.update({"channel_id": channel_id})
+        if isinstance(trigger_ids, (list, tuple)):
+            kwargs.update({"trigger_ids": ",".join(trigger_ids)})
+        else:
+            kwargs.update({"trigger_ids": trigger_ids})
+        return self.api_call("workflows.featured.remove", params=kwargs)
+
+    def workflows_featured_set(
+        self,
+        *,
+        channel_id: str,
+        trigger_ids: Union[str, Sequence[str]],
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Set featured workflows for a channel.
+        https://docs.slack.dev/reference/methods/workflows.featured.set
+        """
+        kwargs.update({"channel_id": channel_id})
+        if isinstance(trigger_ids, (list, tuple)):
+            kwargs.update({"trigger_ids": ",".join(trigger_ids)})
+        else:
+            kwargs.update({"trigger_ids": trigger_ids})
+        return self.api_call("workflows.featured.set", params=kwargs)
+
+    def workflows_stepCompleted(
+        self,
+        *,
+        workflow_step_execute_id: str,
+        outputs: Optional[dict] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Indicate a successful outcome of a workflow step's execution.
+        https://docs.slack.dev/reference/methods/workflows.stepCompleted
+        """
+        kwargs.update({"workflow_step_execute_id": workflow_step_execute_id})
+        if outputs is not None:
+            kwargs.update({"outputs": outputs})
+        kwargs = _remove_none_values(kwargs)
+        # NOTE: Intentionally using json for the "outputs" parameter
+        return self.api_call("workflows.stepCompleted", json=kwargs)
+
+    def workflows_stepFailed(
+        self,
+        *,
+        workflow_step_execute_id: str,
+        error: Dict[str, str],
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Indicate an unsuccessful outcome of a workflow step's execution.
+        https://docs.slack.dev/reference/methods/workflows.stepFailed
+        """
+        kwargs.update(
+            {
+                "workflow_step_execute_id": workflow_step_execute_id,
+                "error": error,
+            }
+        )
+        kwargs = _remove_none_values(kwargs)
+        # NOTE: Intentionally using json for the "error" parameter
+        return self.api_call("workflows.stepFailed", json=kwargs)
+
+    def workflows_updateStep(
+        self,
+        *,
+        workflow_step_edit_id: str,
+        inputs: Optional[Dict[str, Any]] = None,
+        outputs: Optional[List[Dict[str, str]]] = None,
+        **kwargs,
+    ) -> Union[Future, SlackResponse]:
+        """Update the configuration for a workflow extension step.
+        https://docs.slack.dev/reference/methods/workflows.updateStep
+        """
+        kwargs.update({"workflow_step_edit_id": workflow_step_edit_id})
+        if inputs is not None:
+            kwargs.update({"inputs": inputs})
+        if outputs is not None:
+            kwargs.update({"outputs": outputs})
+        kwargs = _remove_none_values(kwargs)
+        # NOTE: Intentionally using json for the "inputs" / "outputs" parameters
+        return self.api_call("workflows.updateStep", json=kwargs)
+
+

A WebClient allows apps to communicate with the Slack Platform's Web API.

+

https://docs.slack.dev/reference/methods

+

The Slack Web API is an interface for querying information from +and enacting change in a Slack workspace.

+

This client handles constructing and sending HTTP requests to Slack +as well as parsing any responses received into a SlackResponse.

+

Attributes

+
+
token : str
+
A string specifying an xoxp-* or xoxb-* token.
+
base_url : str
+
A string representing the Slack API base URL. +Default is 'https://slack.com/api/'
+
timeout : int
+
The maximum number of seconds the client will wait +to connect and receive a response from Slack. +Default is 30 seconds.
+
ssl : SSLContext
+
An ssl.SSLContext instance, helpful for specifying +your own custom certificate chain.
+
proxy : str
+
String representing a fully-qualified URL to a proxy through +which to route all requests to the Slack API. Even if this parameter +is not specified, if any of the following environment variables are +present, they will be loaded into this parameter: HTTPS_PROXY, +https_proxy, HTTP_PROXY or http_proxy.
+
headers : dict
+
Additional request headers to attach to all requests.
+
+

Methods

+

api_call: Constructs a request and executes the API call to Slack.

+

Example of recommended usage:

+
    import os
+    from slack_sdk.web.legacy_client import LegacyWebClient
+
+    client = LegacyWebClient(token=os.environ['SLACK_API_TOKEN'])
+    response = client.chat_postMessage(
+        channel='#random',
+        text="Hello world!")
+    assert response["ok"]
+    assert response["message"]["text"] == "Hello world!"
+
+

Example manually creating an API request:

+
    import os
+    from slack_sdk.web.legacy_client import LegacyWebClient
+
+    client = LegacyWebClient(token=os.environ['SLACK_API_TOKEN'])
+    response = client.api_call(
+        api_method='chat.postMessage',
+        json={'channel': '#random','text': "Hello world!"}
+    )
+    assert response["ok"]
+    assert response["message"]["text"] == "Hello world!"
+
+

Note

+

Any attributes or methods prefixed with _underscores are +intended to be "private" internal use only. They may be changed or +removed at anytime.

+

Ancestors

+ +

Methods

+
+
+def admin_analytics_getFile(self,
*,
type: str,
date: str | None = None,
metadata_only: bool | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def admin_analytics_getFile(
+    self,
+    *,
+    type: str,
+    date: Optional[str] = None,
+    metadata_only: Optional[bool] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Retrieve analytics data for a given date, presented as a compressed JSON file
+    https://docs.slack.dev/reference/methods/admin.analytics.getFile
+    """
+    kwargs.update({"type": type})
+    if date is not None:
+        kwargs.update({"date": date})
+    if metadata_only is not None:
+        kwargs.update({"metadata_only": metadata_only})
+    return self.api_call("admin.analytics.getFile", params=kwargs)
+
+

Retrieve analytics data for a given date, presented as a compressed JSON file +https://docs.slack.dev/reference/methods/admin.analytics.getFile

+
+
+def admin_apps_activities_list(self,
*,
app_id: str | None = None,
component_id: str | None = None,
component_type: str | None = None,
log_event_type: str | None = None,
max_date_created: int | None = None,
min_date_created: int | None = None,
min_log_level: str | None = None,
sort_direction: str | None = None,
source: str | None = None,
team_id: str | None = None,
trace_id: str | None = None,
cursor: str | None = None,
limit: int | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def admin_apps_activities_list(
+    self,
+    *,
+    app_id: Optional[str] = None,
+    component_id: Optional[str] = None,
+    component_type: Optional[str] = None,
+    log_event_type: Optional[str] = None,
+    max_date_created: Optional[int] = None,
+    min_date_created: Optional[int] = None,
+    min_log_level: Optional[str] = None,
+    sort_direction: Optional[str] = None,
+    source: Optional[str] = None,
+    team_id: Optional[str] = None,
+    trace_id: Optional[str] = None,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Get logs for a specified team/org
+    https://docs.slack.dev/reference/methods/admin.apps.activities.list
+    """
+    kwargs.update(
+        {
+            "app_id": app_id,
+            "component_id": component_id,
+            "component_type": component_type,
+            "log_event_type": log_event_type,
+            "max_date_created": max_date_created,
+            "min_date_created": min_date_created,
+            "min_log_level": min_log_level,
+            "sort_direction": sort_direction,
+            "source": source,
+            "team_id": team_id,
+            "trace_id": trace_id,
+            "cursor": cursor,
+            "limit": limit,
+        }
+    )
+    return self.api_call("admin.apps.activities.list", params=kwargs)
+
+ +
+
+def admin_apps_approve(self,
*,
app_id: str | None = None,
request_id: str | None = None,
enterprise_id: str | None = None,
team_id: str | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def admin_apps_approve(
+    self,
+    *,
+    app_id: Optional[str] = None,
+    request_id: Optional[str] = None,
+    enterprise_id: Optional[str] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Approve an app for installation on a workspace.
+    Either app_id or request_id is required.
+    These IDs can be obtained either directly via the app_requested event,
+    or by the admin.apps.requests.list method.
+    https://docs.slack.dev/reference/methods/admin.apps.approve
+    """
+    if app_id:
+        kwargs.update({"app_id": app_id})
+    elif request_id:
+        kwargs.update({"request_id": request_id})
+    else:
+        raise e.SlackRequestError("The app_id or request_id argument must be specified.")
+
+    kwargs.update(
+        {
+            "enterprise_id": enterprise_id,
+            "team_id": team_id,
+        }
+    )
+    return self.api_call("admin.apps.approve", params=kwargs)
+
+

Approve an app for installation on a workspace. +Either app_id or request_id is required. +These IDs can be obtained either directly via the app_requested event, +or by the admin.apps.requests.list method. +https://docs.slack.dev/reference/methods/admin.apps.approve

+
+
+def admin_apps_approved_list(self,
*,
cursor: str | None = None,
limit: int | None = None,
enterprise_id: str | None = None,
team_id: str | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def admin_apps_approved_list(
+    self,
+    *,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    enterprise_id: Optional[str] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """List approved apps for an org or workspace.
+    https://docs.slack.dev/reference/methods/admin.apps.approved.list
+    """
+    kwargs.update(
+        {
+            "cursor": cursor,
+            "limit": limit,
+            "enterprise_id": enterprise_id,
+            "team_id": team_id,
+        }
+    )
+    return self.api_call("admin.apps.approved.list", http_verb="GET", params=kwargs)
+
+

List approved apps for an org or workspace. +https://docs.slack.dev/reference/methods/admin.apps.approved.list

+
+
+def admin_apps_clearResolution(self,
*,
app_id: str,
enterprise_id: str | None = None,
team_id: str | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def admin_apps_clearResolution(
+    self,
+    *,
+    app_id: str,
+    enterprise_id: Optional[str] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Clear an app resolution
+    https://docs.slack.dev/reference/methods/admin.apps.clearResolution
+    """
+    kwargs.update(
+        {
+            "app_id": app_id,
+            "enterprise_id": enterprise_id,
+            "team_id": team_id,
+        }
+    )
+    return self.api_call("admin.apps.clearResolution", http_verb="POST", params=kwargs)
+
+ +
+
+def admin_apps_config_lookup(self, *, app_ids: str | Sequence[str], **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def admin_apps_config_lookup(
+    self,
+    *,
+    app_ids: Union[str, Sequence[str]],
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Look up the app config for connectors by their IDs
+    https://docs.slack.dev/reference/methods/admin.apps.config.lookup
+    """
+    if isinstance(app_ids, (list, tuple)):
+        kwargs.update({"app_ids": ",".join(app_ids)})
+    else:
+        kwargs.update({"app_ids": app_ids})
+    return self.api_call("admin.apps.config.lookup", params=kwargs)
+
+

Look up the app config for connectors by their IDs +https://docs.slack.dev/reference/methods/admin.apps.config.lookup

+
+
+def admin_apps_config_set(self,
*,
app_id: str,
domain_restrictions: Dict[str, Any] | None = None,
workflow_auth_strategy: str | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def admin_apps_config_set(
+    self,
+    *,
+    app_id: str,
+    domain_restrictions: Optional[Dict[str, Any]] = None,
+    workflow_auth_strategy: Optional[str] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Set the app config for a connector
+    https://docs.slack.dev/reference/methods/admin.apps.config.set
+    """
+    kwargs.update(
+        {
+            "app_id": app_id,
+            "workflow_auth_strategy": workflow_auth_strategy,
+        }
+    )
+    if domain_restrictions is not None:
+        kwargs.update({"domain_restrictions": json.dumps(domain_restrictions)})
+    return self.api_call("admin.apps.config.set", params=kwargs)
+
+ +
+
+def admin_apps_requests_cancel(self,
*,
request_id: str,
enterprise_id: str | None = None,
team_id: str | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def admin_apps_requests_cancel(
+    self,
+    *,
+    request_id: str,
+    enterprise_id: Optional[str] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """List app requests for a team/workspace.
+    https://docs.slack.dev/reference/methods/admin.apps.requests.cancel
+    """
+    kwargs.update(
+        {
+            "request_id": request_id,
+            "enterprise_id": enterprise_id,
+            "team_id": team_id,
+        }
+    )
+    return self.api_call("admin.apps.requests.cancel", http_verb="POST", params=kwargs)
+
+ +
+
+def admin_apps_requests_list(self,
*,
cursor: str | None = None,
limit: int | None = None,
team_id: str | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def admin_apps_requests_list(
+    self,
+    *,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """List app requests for a team/workspace.
+    https://docs.slack.dev/reference/methods/admin.apps.requests.list
+    """
+    kwargs.update(
+        {
+            "cursor": cursor,
+            "limit": limit,
+            "team_id": team_id,
+        }
+    )
+    return self.api_call("admin.apps.requests.list", http_verb="GET", params=kwargs)
+
+ +
+
+def admin_apps_restrict(self,
*,
app_id: str | None = None,
request_id: str | None = None,
enterprise_id: str | None = None,
team_id: str | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def admin_apps_restrict(
+    self,
+    *,
+    app_id: Optional[str] = None,
+    request_id: Optional[str] = None,
+    enterprise_id: Optional[str] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Restrict an app for installation on a workspace.
+    Exactly one of the team_id or enterprise_id arguments is required, not both.
+    Either app_id or request_id is required. These IDs can be obtained either directly
+    via the app_requested event, or by the admin.apps.requests.list method.
+    https://docs.slack.dev/reference/methods/admin.apps.restrict
+    """
+    if app_id:
+        kwargs.update({"app_id": app_id})
+    elif request_id:
+        kwargs.update({"request_id": request_id})
+    else:
+        raise e.SlackRequestError("The app_id or request_id argument must be specified.")
+
+    kwargs.update(
+        {
+            "enterprise_id": enterprise_id,
+            "team_id": team_id,
+        }
+    )
+    return self.api_call("admin.apps.restrict", params=kwargs)
+
+

Restrict an app for installation on a workspace. +Exactly one of the team_id or enterprise_id arguments is required, not both. +Either app_id or request_id is required. These IDs can be obtained either directly +via the app_requested event, or by the admin.apps.requests.list method. +https://docs.slack.dev/reference/methods/admin.apps.restrict

+
+
+def admin_apps_restricted_list(self,
*,
cursor: str | None = None,
limit: int | None = None,
enterprise_id: str | None = None,
team_id: str | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def admin_apps_restricted_list(
+    self,
+    *,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    enterprise_id: Optional[str] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """List restricted apps for an org or workspace.
+    https://docs.slack.dev/reference/methods/admin.apps.restricted.list
+    """
+    kwargs.update(
+        {
+            "cursor": cursor,
+            "limit": limit,
+            "enterprise_id": enterprise_id,
+            "team_id": team_id,
+        }
+    )
+    return self.api_call("admin.apps.restricted.list", http_verb="GET", params=kwargs)
+
+

List restricted apps for an org or workspace. +https://docs.slack.dev/reference/methods/admin.apps.restricted.list

+
+
+def admin_apps_uninstall(self,
*,
app_id: str,
enterprise_id: str | None = None,
team_ids: str | Sequence[str] | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def admin_apps_uninstall(
+    self,
+    *,
+    app_id: str,
+    enterprise_id: Optional[str] = None,
+    team_ids: Optional[Union[str, Sequence[str]]] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Uninstall an app from one or many workspaces, or an entire enterprise organization.
+    With an org-level token, enterprise_id or team_ids is required.
+    https://docs.slack.dev/reference/methods/admin.apps.uninstall
+    """
+    kwargs.update({"app_id": app_id})
+    if enterprise_id is not None:
+        kwargs.update({"enterprise_id": enterprise_id})
+    if team_ids is not None:
+        if isinstance(team_ids, (list, tuple)):
+            kwargs.update({"team_ids": ",".join(team_ids)})
+        else:
+            kwargs.update({"team_ids": team_ids})
+    return self.api_call("admin.apps.uninstall", http_verb="POST", params=kwargs)
+
+

Uninstall an app from one or many workspaces, or an entire enterprise organization. +With an org-level token, enterprise_id or team_ids is required. +https://docs.slack.dev/reference/methods/admin.apps.uninstall

+
+
+def admin_auth_policy_assignEntities(self,
*,
entity_ids: str | Sequence[str],
policy_name: str,
entity_type: str,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def admin_auth_policy_assignEntities(
+    self,
+    *,
+    entity_ids: Union[str, Sequence[str]],
+    policy_name: str,
+    entity_type: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Assign entities to a particular authentication policy.
+    https://docs.slack.dev/reference/methods/admin.auth.policy.assignEntities
+    """
+    if isinstance(entity_ids, (list, tuple)):
+        kwargs.update({"entity_ids": ",".join(entity_ids)})
+    else:
+        kwargs.update({"entity_ids": entity_ids})
+    kwargs.update({"policy_name": policy_name})
+    kwargs.update({"entity_type": entity_type})
+    return self.api_call("admin.auth.policy.assignEntities", http_verb="POST", params=kwargs)
+
+

Assign entities to a particular authentication policy. +https://docs.slack.dev/reference/methods/admin.auth.policy.assignEntities

+
+
+def admin_auth_policy_getEntities(self,
*,
policy_name: str,
cursor: str | None = None,
entity_type: str | None = None,
limit: int | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def admin_auth_policy_getEntities(
+    self,
+    *,
+    policy_name: str,
+    cursor: Optional[str] = None,
+    entity_type: Optional[str] = None,
+    limit: Optional[int] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Fetch all the entities assigned to a particular authentication policy by name.
+    https://docs.slack.dev/reference/methods/admin.auth.policy.getEntities
+    """
+    kwargs.update({"policy_name": policy_name})
+    if cursor is not None:
+        kwargs.update({"cursor": cursor})
+    if entity_type is not None:
+        kwargs.update({"entity_type": entity_type})
+    if limit is not None:
+        kwargs.update({"limit": limit})
+    return self.api_call("admin.auth.policy.getEntities", http_verb="POST", params=kwargs)
+
+

Fetch all the entities assigned to a particular authentication policy by name. +https://docs.slack.dev/reference/methods/admin.auth.policy.getEntities

+
+
+def admin_auth_policy_removeEntities(self,
*,
entity_ids: str | Sequence[str],
policy_name: str,
entity_type: str,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def admin_auth_policy_removeEntities(
+    self,
+    *,
+    entity_ids: Union[str, Sequence[str]],
+    policy_name: str,
+    entity_type: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Remove specified entities from a specified authentication policy.
+    https://docs.slack.dev/reference/methods/admin.auth.policy.removeEntities
+    """
+    if isinstance(entity_ids, (list, tuple)):
+        kwargs.update({"entity_ids": ",".join(entity_ids)})
+    else:
+        kwargs.update({"entity_ids": entity_ids})
+    kwargs.update({"policy_name": policy_name})
+    kwargs.update({"entity_type": entity_type})
+    return self.api_call("admin.auth.policy.removeEntities", http_verb="POST", params=kwargs)
+
+

Remove specified entities from a specified authentication policy. +https://docs.slack.dev/reference/methods/admin.auth.policy.removeEntities

+
+
+def admin_barriers_create(self,
*,
barriered_from_usergroup_ids: str | Sequence[str],
primary_usergroup_id: str,
restricted_subjects: str | Sequence[str],
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def admin_barriers_create(
+    self,
+    *,
+    barriered_from_usergroup_ids: Union[str, Sequence[str]],
+    primary_usergroup_id: str,
+    restricted_subjects: Union[str, Sequence[str]],
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Create an Information Barrier
+    https://docs.slack.dev/reference/methods/admin.barriers.create
+    """
+    kwargs.update({"primary_usergroup_id": primary_usergroup_id})
+    if isinstance(barriered_from_usergroup_ids, (list, tuple)):
+        kwargs.update({"barriered_from_usergroup_ids": ",".join(barriered_from_usergroup_ids)})
+    else:
+        kwargs.update({"barriered_from_usergroup_ids": barriered_from_usergroup_ids})
+    if isinstance(restricted_subjects, (list, tuple)):
+        kwargs.update({"restricted_subjects": ",".join(restricted_subjects)})
+    else:
+        kwargs.update({"restricted_subjects": restricted_subjects})
+    return self.api_call("admin.barriers.create", http_verb="POST", params=kwargs)
+
+ +
+
+def admin_barriers_delete(self, *, barrier_id: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def admin_barriers_delete(
+    self,
+    *,
+    barrier_id: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Delete an existing Information Barrier
+    https://docs.slack.dev/reference/methods/admin.barriers.delete
+    """
+    kwargs.update({"barrier_id": barrier_id})
+    return self.api_call("admin.barriers.delete", http_verb="POST", params=kwargs)
+
+ +
+
+def admin_barriers_list(self, *, cursor: str | None = None, limit: int | None = None, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def admin_barriers_list(
+    self,
+    *,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Get all Information Barriers for your organization
+    https://docs.slack.dev/reference/methods/admin.barriers.list"""
+    kwargs.update(
+        {
+            "cursor": cursor,
+            "limit": limit,
+        }
+    )
+    return self.api_call("admin.barriers.list", http_verb="GET", params=kwargs)
+
+

Get all Information Barriers for your organization +https://docs.slack.dev/reference/methods/admin.barriers.list

+
+
+def admin_barriers_update(self,
*,
barrier_id: str,
barriered_from_usergroup_ids: str | Sequence[str],
primary_usergroup_id: str,
restricted_subjects: str | Sequence[str],
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def admin_barriers_update(
+    self,
+    *,
+    barrier_id: str,
+    barriered_from_usergroup_ids: Union[str, Sequence[str]],
+    primary_usergroup_id: str,
+    restricted_subjects: Union[str, Sequence[str]],
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Update an existing Information Barrier
+    https://docs.slack.dev/reference/methods/admin.barriers.update
+    """
+    kwargs.update({"barrier_id": barrier_id, "primary_usergroup_id": primary_usergroup_id})
+    if isinstance(barriered_from_usergroup_ids, (list, tuple)):
+        kwargs.update({"barriered_from_usergroup_ids": ",".join(barriered_from_usergroup_ids)})
+    else:
+        kwargs.update({"barriered_from_usergroup_ids": barriered_from_usergroup_ids})
+    if isinstance(restricted_subjects, (list, tuple)):
+        kwargs.update({"restricted_subjects": ",".join(restricted_subjects)})
+    else:
+        kwargs.update({"restricted_subjects": restricted_subjects})
+    return self.api_call("admin.barriers.update", http_verb="POST", params=kwargs)
+
+ +
+
+def admin_conversations_archive(self, *, channel_id: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def admin_conversations_archive(
+    self,
+    *,
+    channel_id: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Archive a public or private channel.
+    https://docs.slack.dev/reference/methods/admin.conversations.archive
+    """
+    kwargs.update({"channel_id": channel_id})
+    return self.api_call("admin.conversations.archive", params=kwargs)
+
+ +
+
+def admin_conversations_bulkArchive(self, *, channel_ids: str | Sequence[str], **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def admin_conversations_bulkArchive(
+    self,
+    *,
+    channel_ids: Union[Sequence[str], str],
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Archive public or private channels in bulk.
+    https://docs.slack.dev/reference/methods/admin.conversations.bulkArchive
+    """
+    kwargs.update({"channel_ids": ",".join(channel_ids) if isinstance(channel_ids, (list, tuple)) else channel_ids})
+    return self.api_call("admin.conversations.bulkArchive", params=kwargs)
+
+ +
+
+def admin_conversations_bulkDelete(self, *, channel_ids: str | Sequence[str], **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def admin_conversations_bulkDelete(
+    self,
+    *,
+    channel_ids: Union[Sequence[str], str],
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Delete public or private channels in bulk.
+    https://slack.com/api/admin.conversations.bulkDelete
+    """
+    kwargs.update({"channel_ids": ",".join(channel_ids) if isinstance(channel_ids, (list, tuple)) else channel_ids})
+    return self.api_call("admin.conversations.bulkDelete", params=kwargs)
+
+

Delete public or private channels in bulk. +https://slack.com/api/admin.conversations.bulkDelete

+
+
+def admin_conversations_bulkMove(self, *, channel_ids: str | Sequence[str], target_team_id: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def admin_conversations_bulkMove(
+    self,
+    *,
+    channel_ids: Union[Sequence[str], str],
+    target_team_id: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Move public or private channels in bulk.
+    https://docs.slack.dev/reference/methods/admin.conversations.bulkMove
+    """
+    kwargs.update(
+        {
+            "target_team_id": target_team_id,
+            "channel_ids": ",".join(channel_ids) if isinstance(channel_ids, (list, tuple)) else channel_ids,
+        }
+    )
+    return self.api_call("admin.conversations.bulkMove", params=kwargs)
+
+ +
+
+def admin_conversations_convertToPrivate(self, *, channel_id: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def admin_conversations_convertToPrivate(
+    self,
+    *,
+    channel_id: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Convert a public channel to a private channel.
+    https://docs.slack.dev/reference/methods/admin.conversations.convertToPrivate
+    """
+    kwargs.update({"channel_id": channel_id})
+    return self.api_call("admin.conversations.convertToPrivate", params=kwargs)
+
+ +
+
+def admin_conversations_convertToPublic(self, *, channel_id: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def admin_conversations_convertToPublic(
+    self,
+    *,
+    channel_id: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Convert a privte channel to a public channel.
+    https://docs.slack.dev/reference/methods/admin.conversations.convertToPublic
+    """
+    kwargs.update({"channel_id": channel_id})
+    return self.api_call("admin.conversations.convertToPublic", params=kwargs)
+
+ +
+
+def admin_conversations_create(self,
*,
is_private: bool,
name: str,
description: str | None = None,
org_wide: bool | None = None,
team_id: str | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def admin_conversations_create(
+    self,
+    *,
+    is_private: bool,
+    name: str,
+    description: Optional[str] = None,
+    org_wide: Optional[bool] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Create a public or private channel-based conversation.
+    https://docs.slack.dev/reference/methods/admin.conversations.create
+    """
+    kwargs.update(
+        {
+            "is_private": is_private,
+            "name": name,
+            "description": description,
+            "org_wide": org_wide,
+            "team_id": team_id,
+        }
+    )
+    return self.api_call("admin.conversations.create", params=kwargs)
+
+

Create a public or private channel-based conversation. +https://docs.slack.dev/reference/methods/admin.conversations.create

+
+
+def admin_conversations_createForObjects(self,
*,
object_id: str,
salesforce_org_id: str,
invite_object_team: bool | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def admin_conversations_createForObjects(
+    self,
+    *,
+    object_id: str,
+    salesforce_org_id: str,
+    invite_object_team: Optional[bool] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Create a Salesforce channel for the corresponding object provided.
+    https://docs.slack.dev/reference/methods/admin.conversations.createForObjects
+    """
+    kwargs.update(
+        {"object_id": object_id, "salesforce_org_id": salesforce_org_id, "invite_object_team": invite_object_team}
+    )
+    return self.api_call("admin.conversations.createForObjects", params=kwargs)
+
+

Create a Salesforce channel for the corresponding object provided. +https://docs.slack.dev/reference/methods/admin.conversations.createForObjects

+
+
+def admin_conversations_delete(self, *, channel_id: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def admin_conversations_delete(
+    self,
+    *,
+    channel_id: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Delete a public or private channel.
+    https://docs.slack.dev/reference/methods/admin.conversations.delete
+    """
+    kwargs.update({"channel_id": channel_id})
+    return self.api_call("admin.conversations.delete", params=kwargs)
+
+ +
+
+def admin_conversations_disconnectShared(self,
*,
channel_id: str,
leaving_team_ids: str | Sequence[str] | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def admin_conversations_disconnectShared(
+    self,
+    *,
+    channel_id: str,
+    leaving_team_ids: Optional[Union[str, Sequence[str]]] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Disconnect a connected channel from one or more workspaces.
+    https://docs.slack.dev/reference/methods/admin.conversations.disconnectShared
+    """
+    kwargs.update({"channel_id": channel_id})
+    if isinstance(leaving_team_ids, (list, tuple)):
+        kwargs.update({"leaving_team_ids": ",".join(leaving_team_ids)})
+    else:
+        kwargs.update({"leaving_team_ids": leaving_team_ids})
+    return self.api_call("admin.conversations.disconnectShared", params=kwargs)
+
+

Disconnect a connected channel from one or more workspaces. +https://docs.slack.dev/reference/methods/admin.conversations.disconnectShared

+
+
+def admin_conversations_ekm_listOriginalConnectedChannelInfo(self,
*,
channel_ids: str | Sequence[str] | None = None,
cursor: str | None = None,
limit: int | None = None,
team_ids: str | Sequence[str] | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def admin_conversations_ekm_listOriginalConnectedChannelInfo(
+    self,
+    *,
+    channel_ids: Optional[Union[str, Sequence[str]]] = None,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    team_ids: Optional[Union[str, Sequence[str]]] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """List all disconnected channels—i.e.,
+    channels that were once connected to other workspaces and then disconnected—and
+    the corresponding original channel IDs for key revocation with EKM.
+    https://docs.slack.dev/reference/methods/admin.conversations.ekm.listOriginalConnectedChannelInfo
+    """
+    kwargs.update(
+        {
+            "cursor": cursor,
+            "limit": limit,
+        }
+    )
+    if isinstance(channel_ids, (list, tuple)):
+        kwargs.update({"channel_ids": ",".join(channel_ids)})
+    else:
+        kwargs.update({"channel_ids": channel_ids})
+    if isinstance(team_ids, (list, tuple)):
+        kwargs.update({"team_ids": ",".join(team_ids)})
+    else:
+        kwargs.update({"team_ids": team_ids})
+    return self.api_call("admin.conversations.ekm.listOriginalConnectedChannelInfo", params=kwargs)
+
+

List all disconnected channels—i.e., +channels that were once connected to other workspaces and then disconnected—and +the corresponding original channel IDs for key revocation with EKM. +https://docs.slack.dev/reference/methods/admin.conversations.ekm.listOriginalConnectedChannelInfo

+
+
+def admin_conversations_getConversationPrefs(self, *, channel_id: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def admin_conversations_getConversationPrefs(
+    self,
+    *,
+    channel_id: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Get conversation preferences for a public or private channel.
+    https://docs.slack.dev/reference/methods/admin.conversations.getConversationPrefs
+    """
+    kwargs.update({"channel_id": channel_id})
+    return self.api_call("admin.conversations.getConversationPrefs", params=kwargs)
+
+

Get conversation preferences for a public or private channel. +https://docs.slack.dev/reference/methods/admin.conversations.getConversationPrefs

+
+
+def admin_conversations_getCustomRetention(self, *, channel_id: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def admin_conversations_getCustomRetention(
+    self,
+    *,
+    channel_id: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Get a channel's retention policy
+    https://docs.slack.dev/reference/methods/admin.conversations.getCustomRetention
+    """
+    kwargs.update({"channel_id": channel_id})
+    return self.api_call("admin.conversations.getCustomRetention", params=kwargs)
+
+ +
+
+def admin_conversations_getTeams(self,
*,
channel_id: str,
cursor: str | None = None,
limit: int | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def admin_conversations_getTeams(
+    self,
+    *,
+    channel_id: str,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Set the workspaces in an Enterprise grid org that connect to a channel.
+    https://docs.slack.dev/reference/methods/admin.conversations.getTeams
+    """
+    kwargs.update(
+        {
+            "channel_id": channel_id,
+            "cursor": cursor,
+            "limit": limit,
+        }
+    )
+    return self.api_call("admin.conversations.getTeams", params=kwargs)
+
+

Set the workspaces in an Enterprise grid org that connect to a channel. +https://docs.slack.dev/reference/methods/admin.conversations.getTeams

+
+
+def admin_conversations_invite(self, *, channel_id: str, user_ids: str | Sequence[str], **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def admin_conversations_invite(
+    self,
+    *,
+    channel_id: str,
+    user_ids: Union[str, Sequence[str]],
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Invite a user to a public or private channel.
+    https://docs.slack.dev/reference/methods/admin.conversations.invite
+    """
+    kwargs.update({"channel_id": channel_id})
+    if isinstance(user_ids, (list, tuple)):
+        kwargs.update({"user_ids": ",".join(user_ids)})
+    else:
+        kwargs.update({"user_ids": user_ids})
+    # NOTE: the endpoint is unable to handle Content-Type: application/json as of Sep 3, 2020.
+    return self.api_call("admin.conversations.invite", params=kwargs)
+
+

Invite a user to a public or private channel. +https://docs.slack.dev/reference/methods/admin.conversations.invite

+
+
+def admin_conversations_linkObjects(self, *, channel: str, record_id: str, salesforce_org_id: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def admin_conversations_linkObjects(
+    self,
+    *,
+    channel: str,
+    record_id: str,
+    salesforce_org_id: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Link a Salesforce record to a channel.
+    https://docs.slack.dev/reference/methods/admin.conversations.linkObjects
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "record_id": record_id,
+            "salesforce_org_id": salesforce_org_id,
+        }
+    )
+    return self.api_call("admin.conversations.linkObjects", params=kwargs)
+
+ +
+
+def admin_conversations_lookup(self,
*,
last_message_activity_before: int,
team_ids: str | Sequence[str],
cursor: str | None = None,
limit: int | None = None,
max_member_count: int | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def admin_conversations_lookup(
+    self,
+    *,
+    last_message_activity_before: int,
+    team_ids: Union[str, Sequence[str]],
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    max_member_count: Optional[int] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Returns channels on the given team using the filters.
+    https://docs.slack.dev/reference/methods/admin.conversations.lookup
+    """
+    kwargs.update(
+        {
+            "last_message_activity_before": last_message_activity_before,
+            "cursor": cursor,
+            "limit": limit,
+            "max_member_count": max_member_count,
+        }
+    )
+    if isinstance(team_ids, (list, tuple)):
+        kwargs.update({"team_ids": ",".join(team_ids)})
+    else:
+        kwargs.update({"team_ids": team_ids})
+    return self.api_call("admin.conversations.lookup", params=kwargs)
+
+

Returns channels on the given team using the filters. +https://docs.slack.dev/reference/methods/admin.conversations.lookup

+
+
+def admin_conversations_removeCustomRetention(self, *, channel_id: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def admin_conversations_removeCustomRetention(
+    self,
+    *,
+    channel_id: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Remove a channel's retention policy
+    https://docs.slack.dev/reference/methods/admin.conversations.removeCustomRetention
+    """
+    kwargs.update({"channel_id": channel_id})
+    return self.api_call("admin.conversations.removeCustomRetention", params=kwargs)
+
+ +
+
+def admin_conversations_rename(self, *, channel_id: str, name: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def admin_conversations_rename(
+    self,
+    *,
+    channel_id: str,
+    name: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Rename a public or private channel.
+    https://docs.slack.dev/reference/methods/admin.conversations.rename
+    """
+    kwargs.update({"channel_id": channel_id, "name": name})
+    return self.api_call("admin.conversations.rename", params=kwargs)
+
+ +
+
+def admin_conversations_restrictAccess_addGroup(self, *, channel_id: str, group_id: str, team_id: str | None = None, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def admin_conversations_restrictAccess_addGroup(
+    self,
+    *,
+    channel_id: str,
+    group_id: str,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Add an allowlist of IDP groups for accessing a channel.
+    https://docs.slack.dev/reference/methods/admin.conversations.restrictAccess.addGroup
+    """
+    kwargs.update(
+        {
+            "channel_id": channel_id,
+            "group_id": group_id,
+            "team_id": team_id,
+        }
+    )
+    return self.api_call(
+        "admin.conversations.restrictAccess.addGroup",
+        http_verb="GET",
+        params=kwargs,
+    )
+
+

Add an allowlist of IDP groups for accessing a channel. +https://docs.slack.dev/reference/methods/admin.conversations.restrictAccess.addGroup

+
+
+def admin_conversations_restrictAccess_listGroups(self, *, channel_id: str, team_id: str | None = None, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def admin_conversations_restrictAccess_listGroups(
+    self,
+    *,
+    channel_id: str,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """List all IDP Groups linked to a channel.
+    https://docs.slack.dev/reference/methods/admin.conversations.restrictAccess.listGroups
+    """
+    kwargs.update(
+        {
+            "channel_id": channel_id,
+            "team_id": team_id,
+        }
+    )
+    return self.api_call(
+        "admin.conversations.restrictAccess.listGroups",
+        http_verb="GET",
+        params=kwargs,
+    )
+
+ +
+
+def admin_conversations_restrictAccess_removeGroup(self, *, channel_id: str, group_id: str, team_id: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def admin_conversations_restrictAccess_removeGroup(
+    self,
+    *,
+    channel_id: str,
+    group_id: str,
+    team_id: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Remove a linked IDP group linked from a private channel.
+    https://docs.slack.dev/reference/methods/admin.conversations.restrictAccess.removeGroup
+    """
+    kwargs.update(
+        {
+            "channel_id": channel_id,
+            "group_id": group_id,
+            "team_id": team_id,
+        }
+    )
+    return self.api_call(
+        "admin.conversations.restrictAccess.removeGroup",
+        http_verb="GET",
+        params=kwargs,
+    )
+
+

Remove a linked IDP group linked from a private channel. +https://docs.slack.dev/reference/methods/admin.conversations.restrictAccess.removeGroup

+
+ +
+
+ +Expand source code + +
def admin_conversations_search(
+    self,
+    *,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    query: Optional[str] = None,
+    search_channel_types: Optional[Union[str, Sequence[str]]] = None,
+    sort: Optional[str] = None,
+    sort_dir: Optional[str] = None,
+    team_ids: Optional[Union[str, Sequence[str]]] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Search for public or private channels in an Enterprise organization.
+    https://docs.slack.dev/reference/methods/admin.conversations.search
+    """
+    kwargs.update(
+        {
+            "cursor": cursor,
+            "limit": limit,
+            "query": query,
+            "sort": sort,
+            "sort_dir": sort_dir,
+        }
+    )
+
+    if isinstance(search_channel_types, (list, tuple)):
+        kwargs.update({"search_channel_types": ",".join(search_channel_types)})
+    else:
+        kwargs.update({"search_channel_types": search_channel_types})
+
+    if isinstance(team_ids, (list, tuple)):
+        kwargs.update({"team_ids": ",".join(team_ids)})
+    else:
+        kwargs.update({"team_ids": team_ids})
+
+    return self.api_call("admin.conversations.search", params=kwargs)
+
+

Search for public or private channels in an Enterprise organization. +https://docs.slack.dev/reference/methods/admin.conversations.search

+
+
+def admin_conversations_setConversationPrefs(self, *, channel_id: str, prefs: str | Dict[str, str], **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def admin_conversations_setConversationPrefs(
+    self,
+    *,
+    channel_id: str,
+    prefs: Union[str, Dict[str, str]],
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Set the posting permissions for a public or private channel.
+    https://docs.slack.dev/reference/methods/admin.conversations.setConversationPrefs
+    """
+    kwargs.update({"channel_id": channel_id})
+    if isinstance(prefs, dict):
+        kwargs.update({"prefs": json.dumps(prefs)})
+    else:
+        kwargs.update({"prefs": prefs})
+    return self.api_call("admin.conversations.setConversationPrefs", params=kwargs)
+
+

Set the posting permissions for a public or private channel. +https://docs.slack.dev/reference/methods/admin.conversations.setConversationPrefs

+
+
+def admin_conversations_setCustomRetention(self, *, channel_id: str, duration_days: int, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def admin_conversations_setCustomRetention(
+    self,
+    *,
+    channel_id: str,
+    duration_days: int,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Set a channel's retention policy
+    https://docs.slack.dev/reference/methods/admin.conversations.setCustomRetention
+    """
+    kwargs.update({"channel_id": channel_id, "duration_days": duration_days})
+    return self.api_call("admin.conversations.setCustomRetention", params=kwargs)
+
+ +
+
+def admin_conversations_setTeams(self,
*,
channel_id: str,
org_channel: bool | None = None,
target_team_ids: str | Sequence[str] | None = None,
team_id: str | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def admin_conversations_setTeams(
+    self,
+    *,
+    channel_id: str,
+    org_channel: Optional[bool] = None,
+    target_team_ids: Optional[Union[str, Sequence[str]]] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Set the workspaces in an Enterprise grid org that connect to a public or private channel.
+    https://docs.slack.dev/reference/methods/admin.conversations.setTeams
+    """
+    kwargs.update(
+        {
+            "channel_id": channel_id,
+            "org_channel": org_channel,
+            "team_id": team_id,
+        }
+    )
+    if isinstance(target_team_ids, (list, tuple)):
+        kwargs.update({"target_team_ids": ",".join(target_team_ids)})
+    else:
+        kwargs.update({"target_team_ids": target_team_ids})
+    return self.api_call("admin.conversations.setTeams", params=kwargs)
+
+

Set the workspaces in an Enterprise grid org that connect to a public or private channel. +https://docs.slack.dev/reference/methods/admin.conversations.setTeams

+
+
+def admin_conversations_unarchive(self, *, channel_id: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def admin_conversations_unarchive(
+    self,
+    *,
+    channel_id: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Unarchive a public or private channel.
+    https://docs.slack.dev/reference/methods/admin.conversations.archive
+    """
+    kwargs.update({"channel_id": channel_id})
+    return self.api_call("admin.conversations.unarchive", params=kwargs)
+
+ +
+
+def admin_conversations_unlinkObjects(self, *, channel: str, new_name: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def admin_conversations_unlinkObjects(
+    self,
+    *,
+    channel: str,
+    new_name: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Unlink a Salesforce record from a channel.
+    https://docs.slack.dev/reference/methods/admin.conversations.unlinkObjects
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "new_name": new_name,
+        }
+    )
+    return self.api_call("admin.conversations.unlinkObjects", params=kwargs)
+
+ +
+
+def admin_emoji_add(self, *, name: str, url: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def admin_emoji_add(
+    self,
+    *,
+    name: str,
+    url: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Add an emoji.
+    https://docs.slack.dev/reference/methods/admin.emoji.add
+    """
+    kwargs.update({"name": name, "url": url})
+    return self.api_call("admin.emoji.add", http_verb="GET", params=kwargs)
+
+ +
+
+def admin_emoji_addAlias(self, *, alias_for: str, name: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def admin_emoji_addAlias(
+    self,
+    *,
+    alias_for: str,
+    name: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Add an emoji alias.
+    https://docs.slack.dev/reference/methods/admin.emoji.addAlias
+    """
+    kwargs.update({"alias_for": alias_for, "name": name})
+    return self.api_call("admin.emoji.addAlias", http_verb="GET", params=kwargs)
+
+ +
+
+def admin_emoji_list(self, *, cursor: str | None = None, limit: int | None = None, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def admin_emoji_list(
+    self,
+    *,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """List emoji for an Enterprise Grid organization.
+    https://docs.slack.dev/reference/methods/admin.emoji.list
+    """
+    kwargs.update({"cursor": cursor, "limit": limit})
+    return self.api_call("admin.emoji.list", http_verb="GET", params=kwargs)
+
+

List emoji for an Enterprise Grid organization. +https://docs.slack.dev/reference/methods/admin.emoji.list

+
+
+def admin_emoji_remove(self, *, name: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def admin_emoji_remove(
+    self,
+    *,
+    name: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Remove an emoji across an Enterprise Grid organization.
+    https://docs.slack.dev/reference/methods/admin.emoji.remove
+    """
+    kwargs.update({"name": name})
+    return self.api_call("admin.emoji.remove", http_verb="GET", params=kwargs)
+
+

Remove an emoji across an Enterprise Grid organization. +https://docs.slack.dev/reference/methods/admin.emoji.remove

+
+
+def admin_emoji_rename(self, *, name: str, new_name: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def admin_emoji_rename(
+    self,
+    *,
+    name: str,
+    new_name: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Rename an emoji.
+    https://docs.slack.dev/reference/methods/admin.emoji.rename
+    """
+    kwargs.update({"name": name, "new_name": new_name})
+    return self.api_call("admin.emoji.rename", http_verb="GET", params=kwargs)
+
+ +
+
+def admin_functions_list(self,
*,
app_ids: str | Sequence[str],
team_id: str | None = None,
cursor: str | None = None,
limit: int | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def admin_functions_list(
+    self,
+    *,
+    app_ids: Union[str, Sequence[str]],
+    team_id: Optional[str] = None,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Look up functions by a set of apps
+    https://docs.slack.dev/reference/methods/admin.functions.list
+    """
+    if isinstance(app_ids, (list, tuple)):
+        kwargs.update({"app_ids": ",".join(app_ids)})
+    else:
+        kwargs.update({"app_ids": app_ids})
+    kwargs.update(
+        {
+            "team_id": team_id,
+            "cursor": cursor,
+            "limit": limit,
+        }
+    )
+    return self.api_call("admin.functions.list", params=kwargs)
+
+ +
+
+def admin_functions_permissions_lookup(self, *, function_ids: str | Sequence[str], **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def admin_functions_permissions_lookup(
+    self,
+    *,
+    function_ids: Union[str, Sequence[str]],
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Lookup the visibility of multiple Slack functions
+    and include the users if it is limited to particular named entities.
+    https://docs.slack.dev/reference/methods/admin.functions.permissions.lookup
+    """
+    if isinstance(function_ids, (list, tuple)):
+        kwargs.update({"function_ids": ",".join(function_ids)})
+    else:
+        kwargs.update({"function_ids": function_ids})
+    return self.api_call("admin.functions.permissions.lookup", params=kwargs)
+
+

Lookup the visibility of multiple Slack functions +and include the users if it is limited to particular named entities. +https://docs.slack.dev/reference/methods/admin.functions.permissions.lookup

+
+
+def admin_functions_permissions_set(self,
*,
function_id: str,
visibility: str,
user_ids: str | Sequence[str] | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def admin_functions_permissions_set(
+    self,
+    *,
+    function_id: str,
+    visibility: str,
+    user_ids: Optional[Union[str, Sequence[str]]] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Set the visibility of a Slack function
+    and define the users or workspaces if it is set to named_entities
+    https://docs.slack.dev/reference/methods/admin.functions.permissions.set
+    """
+    kwargs.update(
+        {
+            "function_id": function_id,
+            "visibility": visibility,
+        }
+    )
+    if user_ids is not None:
+        if isinstance(user_ids, (list, tuple)):
+            kwargs.update({"user_ids": ",".join(user_ids)})
+        else:
+            kwargs.update({"user_ids": user_ids})
+    return self.api_call("admin.functions.permissions.set", params=kwargs)
+
+

Set the visibility of a Slack function +and define the users or workspaces if it is set to named_entities +https://docs.slack.dev/reference/methods/admin.functions.permissions.set

+
+
+def admin_inviteRequests_approve(self, *, invite_request_id: str, team_id: str | None = None, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def admin_inviteRequests_approve(
+    self,
+    *,
+    invite_request_id: str,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Approve a workspace invite request.
+    https://docs.slack.dev/reference/methods/admin.inviteRequests.approve
+    """
+    kwargs.update({"invite_request_id": invite_request_id, "team_id": team_id})
+    return self.api_call("admin.inviteRequests.approve", params=kwargs)
+
+ +
+
+def admin_inviteRequests_approved_list(self,
*,
cursor: str | None = None,
limit: int | None = None,
team_id: str | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def admin_inviteRequests_approved_list(
+    self,
+    *,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """List all approved workspace invite requests.
+    https://docs.slack.dev/reference/methods/admin.inviteRequests.approved.list
+    """
+    kwargs.update(
+        {
+            "cursor": cursor,
+            "limit": limit,
+            "team_id": team_id,
+        }
+    )
+    return self.api_call("admin.inviteRequests.approved.list", params=kwargs)
+
+ +
+
+def admin_inviteRequests_denied_list(self,
*,
cursor: str | None = None,
limit: int | None = None,
team_id: str | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def admin_inviteRequests_denied_list(
+    self,
+    *,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """List all denied workspace invite requests.
+    https://docs.slack.dev/reference/methods/admin.inviteRequests.denied.list
+    """
+    kwargs.update(
+        {
+            "cursor": cursor,
+            "limit": limit,
+            "team_id": team_id,
+        }
+    )
+    return self.api_call("admin.inviteRequests.denied.list", params=kwargs)
+
+ +
+
+def admin_inviteRequests_deny(self, *, invite_request_id: str, team_id: str | None = None, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def admin_inviteRequests_deny(
+    self,
+    *,
+    invite_request_id: str,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Deny a workspace invite request.
+    https://docs.slack.dev/reference/methods/admin.inviteRequests.deny
+    """
+    kwargs.update({"invite_request_id": invite_request_id, "team_id": team_id})
+    return self.api_call("admin.inviteRequests.deny", params=kwargs)
+
+ +
+
+def admin_inviteRequests_list(self, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def admin_inviteRequests_list(
+    self,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """List all pending workspace invite requests."""
+    return self.api_call("admin.inviteRequests.list", params=kwargs)
+
+

List all pending workspace invite requests.

+
+
+def admin_roles_addAssignments(self,
*,
role_id: str,
entity_ids: str | Sequence[str],
user_ids: str | Sequence[str],
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def admin_roles_addAssignments(
+    self,
+    *,
+    role_id: str,
+    entity_ids: Union[str, Sequence[str]],
+    user_ids: Union[str, Sequence[str]],
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Adds members to the specified role with the specified scopes
+    https://docs.slack.dev/reference/methods/admin.roles.addAssignments
+    """
+    kwargs.update({"role_id": role_id})
+    if isinstance(entity_ids, (list, tuple)):
+        kwargs.update({"entity_ids": ",".join(entity_ids)})
+    else:
+        kwargs.update({"entity_ids": entity_ids})
+    if isinstance(user_ids, (list, tuple)):
+        kwargs.update({"user_ids": ",".join(user_ids)})
+    else:
+        kwargs.update({"user_ids": user_ids})
+    return self.api_call("admin.roles.addAssignments", params=kwargs)
+
+

Adds members to the specified role with the specified scopes +https://docs.slack.dev/reference/methods/admin.roles.addAssignments

+
+
+def admin_roles_listAssignments(self,
*,
role_ids: str | Sequence[str] | None = None,
entity_ids: str | Sequence[str] | None = None,
cursor: str | None = None,
limit: str | int | None = None,
sort_dir: str | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def admin_roles_listAssignments(
+    self,
+    *,
+    role_ids: Optional[Union[str, Sequence[str]]] = None,
+    entity_ids: Optional[Union[str, Sequence[str]]] = None,
+    cursor: Optional[str] = None,
+    limit: Optional[Union[str, int]] = None,
+    sort_dir: Optional[str] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Lists assignments for all roles across entities.
+        Options to scope results by any combination of roles or entities
+    https://docs.slack.dev/reference/methods/admin.roles.listAssignments
+    """
+    kwargs.update({"cursor": cursor, "limit": limit, "sort_dir": sort_dir})
+    if isinstance(entity_ids, (list, tuple)):
+        kwargs.update({"entity_ids": ",".join(entity_ids)})
+    else:
+        kwargs.update({"entity_ids": entity_ids})
+    if isinstance(role_ids, (list, tuple)):
+        kwargs.update({"role_ids": ",".join(role_ids)})
+    else:
+        kwargs.update({"role_ids": role_ids})
+    return self.api_call("admin.roles.listAssignments", params=kwargs)
+
+

Lists assignments for all roles across entities. +Options to scope results by any combination of roles or entities +https://docs.slack.dev/reference/methods/admin.roles.listAssignments

+
+
+def admin_roles_removeAssignments(self,
*,
role_id: str,
entity_ids: str | Sequence[str],
user_ids: str | Sequence[str],
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def admin_roles_removeAssignments(
+    self,
+    *,
+    role_id: str,
+    entity_ids: Union[str, Sequence[str]],
+    user_ids: Union[str, Sequence[str]],
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Removes a set of users from a role for the given scopes and entities
+    https://docs.slack.dev/reference/methods/admin.roles.removeAssignments
+    """
+    kwargs.update({"role_id": role_id})
+    if isinstance(entity_ids, (list, tuple)):
+        kwargs.update({"entity_ids": ",".join(entity_ids)})
+    else:
+        kwargs.update({"entity_ids": entity_ids})
+    if isinstance(user_ids, (list, tuple)):
+        kwargs.update({"user_ids": ",".join(user_ids)})
+    else:
+        kwargs.update({"user_ids": user_ids})
+    return self.api_call("admin.roles.removeAssignments", params=kwargs)
+
+

Removes a set of users from a role for the given scopes and entities +https://docs.slack.dev/reference/methods/admin.roles.removeAssignments

+
+
+def admin_teams_admins_list(self, *, team_id: str, cursor: str | None = None, limit: int | None = None, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def admin_teams_admins_list(
+    self,
+    *,
+    team_id: str,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """List all of the admins on a given workspace.
+    https://docs.slack.dev/reference/methods/admin.inviteRequests.list
+    """
+    kwargs.update(
+        {
+            "cursor": cursor,
+            "limit": limit,
+            "team_id": team_id,
+        }
+    )
+    return self.api_call("admin.teams.admins.list", http_verb="GET", params=kwargs)
+
+

List all of the admins on a given workspace. +https://docs.slack.dev/reference/methods/admin.inviteRequests.list

+
+
+def admin_teams_create(self,
*,
team_domain: str,
team_name: str,
team_description: str | None = None,
team_discoverability: str | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def admin_teams_create(
+    self,
+    *,
+    team_domain: str,
+    team_name: str,
+    team_description: Optional[str] = None,
+    team_discoverability: Optional[str] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Create an Enterprise team.
+    https://docs.slack.dev/reference/methods/admin.teams.create
+    """
+    kwargs.update(
+        {
+            "team_domain": team_domain,
+            "team_name": team_name,
+            "team_description": team_description,
+            "team_discoverability": team_discoverability,
+        }
+    )
+    return self.api_call("admin.teams.create", params=kwargs)
+
+ +
+
+def admin_teams_list(self, *, cursor: str | None = None, limit: int | None = None, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def admin_teams_list(
+    self,
+    *,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """List all teams on an Enterprise organization.
+    https://docs.slack.dev/reference/methods/admin.teams.list
+    """
+    kwargs.update({"cursor": cursor, "limit": limit})
+    return self.api_call("admin.teams.list", params=kwargs)
+
+

List all teams on an Enterprise organization. +https://docs.slack.dev/reference/methods/admin.teams.list

+
+
+def admin_teams_owners_list(self, *, team_id: str, cursor: str | None = None, limit: int | None = None, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def admin_teams_owners_list(
+    self,
+    *,
+    team_id: str,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """List all of the admins on a given workspace.
+    https://docs.slack.dev/reference/methods/admin.teams.owners.list
+    """
+    kwargs.update({"team_id": team_id, "cursor": cursor, "limit": limit})
+    return self.api_call("admin.teams.owners.list", http_verb="GET", params=kwargs)
+
+

List all of the admins on a given workspace. +https://docs.slack.dev/reference/methods/admin.teams.owners.list

+
+
+def admin_teams_settings_info(self, *, team_id: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def admin_teams_settings_info(
+    self,
+    *,
+    team_id: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Fetch information about settings in a workspace
+    https://docs.slack.dev/reference/methods/admin.teams.settings.info
+    """
+    kwargs.update({"team_id": team_id})
+    return self.api_call("admin.teams.settings.info", params=kwargs)
+
+

Fetch information about settings in a workspace +https://docs.slack.dev/reference/methods/admin.teams.settings.info

+
+
+def admin_teams_settings_setDefaultChannels(self, *, team_id: str, channel_ids: str | Sequence[str], **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def admin_teams_settings_setDefaultChannels(
+    self,
+    *,
+    team_id: str,
+    channel_ids: Union[str, Sequence[str]],
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Set the default channels of a workspace.
+    https://docs.slack.dev/reference/methods/admin.teams.settings.setDefaultChannels
+    """
+    kwargs.update({"team_id": team_id})
+    if isinstance(channel_ids, (list, tuple)):
+        kwargs.update({"channel_ids": ",".join(channel_ids)})
+    else:
+        kwargs.update({"channel_ids": channel_ids})
+    return self.api_call("admin.teams.settings.setDefaultChannels", http_verb="GET", params=kwargs)
+
+ +
+
+def admin_teams_settings_setDescription(self, *, team_id: str, description: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def admin_teams_settings_setDescription(
+    self,
+    *,
+    team_id: str,
+    description: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Set the description of a given workspace.
+    https://docs.slack.dev/reference/methods/admin.teams.settings.setDescription
+    """
+    kwargs.update({"team_id": team_id, "description": description})
+    return self.api_call("admin.teams.settings.setDescription", params=kwargs)
+
+ +
+
+def admin_teams_settings_setDiscoverability(self, *, team_id: str, discoverability: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def admin_teams_settings_setDiscoverability(
+    self,
+    *,
+    team_id: str,
+    discoverability: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Sets the icon of a workspace.
+    https://docs.slack.dev/reference/methods/admin.teams.settings.setDiscoverability
+    """
+    kwargs.update({"team_id": team_id, "discoverability": discoverability})
+    return self.api_call("admin.teams.settings.setDiscoverability", params=kwargs)
+
+ +
+
+def admin_teams_settings_setIcon(self, *, team_id: str, image_url: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def admin_teams_settings_setIcon(
+    self,
+    *,
+    team_id: str,
+    image_url: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Sets the icon of a workspace.
+    https://docs.slack.dev/reference/methods/admin.teams.settings.setIcon
+    """
+    kwargs.update({"team_id": team_id, "image_url": image_url})
+    return self.api_call("admin.teams.settings.setIcon", http_verb="GET", params=kwargs)
+
+ +
+
+def admin_teams_settings_setName(self, *, team_id: str, name: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def admin_teams_settings_setName(
+    self,
+    *,
+    team_id: str,
+    name: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Sets the icon of a workspace.
+    https://docs.slack.dev/reference/methods/admin.teams.settings.setName
+    """
+    kwargs.update({"team_id": team_id, "name": name})
+    return self.api_call("admin.teams.settings.setName", params=kwargs)
+
+ +
+
+def admin_usergroups_addChannels(self,
*,
channel_ids: str | Sequence[str],
usergroup_id: str,
team_id: str | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def admin_usergroups_addChannels(
+    self,
+    *,
+    channel_ids: Union[str, Sequence[str]],
+    usergroup_id: str,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Add one or more default channels to an IDP group.
+    https://docs.slack.dev/reference/methods/admin.usergroups.addChannels
+    """
+    kwargs.update({"team_id": team_id, "usergroup_id": usergroup_id})
+    if isinstance(channel_ids, (list, tuple)):
+        kwargs.update({"channel_ids": ",".join(channel_ids)})
+    else:
+        kwargs.update({"channel_ids": channel_ids})
+    return self.api_call("admin.usergroups.addChannels", params=kwargs)
+
+

Add one or more default channels to an IDP group. +https://docs.slack.dev/reference/methods/admin.usergroups.addChannels

+
+
+def admin_usergroups_addTeams(self,
*,
usergroup_id: str,
team_ids: str | Sequence[str],
auto_provision: bool | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def admin_usergroups_addTeams(
+    self,
+    *,
+    usergroup_id: str,
+    team_ids: Union[str, Sequence[str]],
+    auto_provision: Optional[bool] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Associate one or more default workspaces with an organization-wide IDP group.
+    https://docs.slack.dev/reference/methods/admin.usergroups.addTeams
+    """
+    kwargs.update({"usergroup_id": usergroup_id, "auto_provision": auto_provision})
+    if isinstance(team_ids, (list, tuple)):
+        kwargs.update({"team_ids": ",".join(team_ids)})
+    else:
+        kwargs.update({"team_ids": team_ids})
+    return self.api_call("admin.usergroups.addTeams", params=kwargs)
+
+

Associate one or more default workspaces with an organization-wide IDP group. +https://docs.slack.dev/reference/methods/admin.usergroups.addTeams

+
+
+def admin_usergroups_listChannels(self,
*,
usergroup_id: str,
include_num_members: bool | None = None,
team_id: bool | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def admin_usergroups_listChannels(
+    self,
+    *,
+    usergroup_id: str,
+    include_num_members: Optional[bool] = None,
+    team_id: Optional[bool] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Add one or more default channels to an IDP group.
+    https://docs.slack.dev/reference/methods/admin.usergroups.listChannels
+    """
+    kwargs.update(
+        {
+            "usergroup_id": usergroup_id,
+            "include_num_members": include_num_members,
+            "team_id": team_id,
+        }
+    )
+    return self.api_call("admin.usergroups.listChannels", params=kwargs)
+
+

Add one or more default channels to an IDP group. +https://docs.slack.dev/reference/methods/admin.usergroups.listChannels

+
+
+def admin_usergroups_removeChannels(self, *, usergroup_id: str, channel_ids: str | Sequence[str], **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def admin_usergroups_removeChannels(
+    self,
+    *,
+    usergroup_id: str,
+    channel_ids: Union[str, Sequence[str]],
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Add one or more default channels to an IDP group.
+    https://docs.slack.dev/reference/methods/admin.usergroups.removeChannels
+    """
+    kwargs.update({"usergroup_id": usergroup_id})
+    if isinstance(channel_ids, (list, tuple)):
+        kwargs.update({"channel_ids": ",".join(channel_ids)})
+    else:
+        kwargs.update({"channel_ids": channel_ids})
+    return self.api_call("admin.usergroups.removeChannels", params=kwargs)
+
+

Add one or more default channels to an IDP group. +https://docs.slack.dev/reference/methods/admin.usergroups.removeChannels

+
+
+def admin_users_assign(self,
*,
team_id: str,
user_id: str,
channel_ids: str | Sequence[str] | None = None,
is_restricted: bool | None = None,
is_ultra_restricted: bool | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def admin_users_assign(
+    self,
+    *,
+    team_id: str,
+    user_id: str,
+    channel_ids: Optional[Union[str, Sequence[str]]] = None,
+    is_restricted: Optional[bool] = None,
+    is_ultra_restricted: Optional[bool] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Add an Enterprise user to a workspace.
+    https://docs.slack.dev/reference/methods/admin.users.assign
+    """
+    kwargs.update(
+        {
+            "team_id": team_id,
+            "user_id": user_id,
+            "is_restricted": is_restricted,
+            "is_ultra_restricted": is_ultra_restricted,
+        }
+    )
+    if isinstance(channel_ids, (list, tuple)):
+        kwargs.update({"channel_ids": ",".join(channel_ids)})
+    else:
+        kwargs.update({"channel_ids": channel_ids})
+    return self.api_call("admin.users.assign", params=kwargs)
+
+

Add an Enterprise user to a workspace. +https://docs.slack.dev/reference/methods/admin.users.assign

+
+
+def admin_users_invite(self,
*,
team_id: str,
email: str,
channel_ids: str | Sequence[str],
custom_message: str | None = None,
email_password_policy_enabled: bool | None = None,
guest_expiration_ts: str | float | None = None,
is_restricted: bool | None = None,
is_ultra_restricted: bool | None = None,
real_name: str | None = None,
resend: bool | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def admin_users_invite(
+    self,
+    *,
+    team_id: str,
+    email: str,
+    channel_ids: Union[str, Sequence[str]],
+    custom_message: Optional[str] = None,
+    email_password_policy_enabled: Optional[bool] = None,
+    guest_expiration_ts: Optional[Union[str, float]] = None,
+    is_restricted: Optional[bool] = None,
+    is_ultra_restricted: Optional[bool] = None,
+    real_name: Optional[str] = None,
+    resend: Optional[bool] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Invite a user to a workspace.
+    https://docs.slack.dev/reference/methods/admin.users.invite
+    """
+    kwargs.update(
+        {
+            "team_id": team_id,
+            "email": email,
+            "custom_message": custom_message,
+            "email_password_policy_enabled": email_password_policy_enabled,
+            "guest_expiration_ts": str(guest_expiration_ts) if guest_expiration_ts is not None else None,
+            "is_restricted": is_restricted,
+            "is_ultra_restricted": is_ultra_restricted,
+            "real_name": real_name,
+            "resend": resend,
+        }
+    )
+    if isinstance(channel_ids, (list, tuple)):
+        kwargs.update({"channel_ids": ",".join(channel_ids)})
+    else:
+        kwargs.update({"channel_ids": channel_ids})
+    return self.api_call("admin.users.invite", params=kwargs)
+
+ +
+
+def admin_users_list(self,
*,
team_id: str | None = None,
include_deactivated_user_workspaces: bool | None = None,
is_active: bool | None = None,
cursor: str | None = None,
limit: int | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def admin_users_list(
+    self,
+    *,
+    team_id: Optional[str] = None,
+    include_deactivated_user_workspaces: Optional[bool] = None,
+    is_active: Optional[bool] = None,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """List users on a workspace
+    https://docs.slack.dev/reference/methods/admin.users.list
+    """
+    kwargs.update(
+        {
+            "team_id": team_id,
+            "include_deactivated_user_workspaces": include_deactivated_user_workspaces,
+            "is_active": is_active,
+            "cursor": cursor,
+            "limit": limit,
+        }
+    )
+    return self.api_call("admin.users.list", params=kwargs)
+
+ +
+
+def admin_users_remove(self, *, team_id: str, user_id: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def admin_users_remove(
+    self,
+    *,
+    team_id: str,
+    user_id: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Remove a user from a workspace.
+    https://docs.slack.dev/reference/methods/admin.users.remove
+    """
+    kwargs.update({"team_id": team_id, "user_id": user_id})
+    return self.api_call("admin.users.remove", params=kwargs)
+
+ +
+
+def admin_users_session_clearSettings(self, *, user_ids: str | Sequence[str], **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def admin_users_session_clearSettings(
+    self,
+    *,
+    user_ids: Union[str, Sequence[str]],
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Clear user-specific session settings—the session duration
+    and what happens when the client closes—for a list of users.
+    https://docs.slack.dev/reference/methods/admin.users.session.clearSettings
+    """
+    if isinstance(user_ids, (list, tuple)):
+        kwargs.update({"user_ids": ",".join(user_ids)})
+    else:
+        kwargs.update({"user_ids": user_ids})
+    return self.api_call("admin.users.session.clearSettings", params=kwargs)
+
+

Clear user-specific session settings—the session duration +and what happens when the client closes—for a list of users. +https://docs.slack.dev/reference/methods/admin.users.session.clearSettings

+
+
+def admin_users_session_getSettings(self, *, user_ids: str | Sequence[str], **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def admin_users_session_getSettings(
+    self,
+    *,
+    user_ids: Union[str, Sequence[str]],
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Get user-specific session settings—the session duration
+    and what happens when the client closes—given a list of users.
+    https://docs.slack.dev/reference/methods/admin.users.session.getSettings
+    """
+    if isinstance(user_ids, (list, tuple)):
+        kwargs.update({"user_ids": ",".join(user_ids)})
+    else:
+        kwargs.update({"user_ids": user_ids})
+    return self.api_call("admin.users.session.getSettings", params=kwargs)
+
+

Get user-specific session settings—the session duration +and what happens when the client closes—given a list of users. +https://docs.slack.dev/reference/methods/admin.users.session.getSettings

+
+
+def admin_users_session_invalidate(self, *, session_id: str, team_id: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def admin_users_session_invalidate(
+    self,
+    *,
+    session_id: str,
+    team_id: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Invalidate a single session for a user by session_id.
+    https://docs.slack.dev/reference/methods/admin.users.session.invalidate
+    """
+    kwargs.update({"session_id": session_id, "team_id": team_id})
+    return self.api_call("admin.users.session.invalidate", params=kwargs)
+
+

Invalidate a single session for a user by session_id. +https://docs.slack.dev/reference/methods/admin.users.session.invalidate

+
+
+def admin_users_session_list(self,
*,
cursor: str | None = None,
limit: int | None = None,
team_id: str | None = None,
user_id: str | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def admin_users_session_list(
+    self,
+    *,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    team_id: Optional[str] = None,
+    user_id: Optional[str] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Lists all active user sessions for an organization
+    https://docs.slack.dev/reference/methods/admin.users.session.list
+    """
+    kwargs.update(
+        {
+            "cursor": cursor,
+            "limit": limit,
+            "team_id": team_id,
+            "user_id": user_id,
+        }
+    )
+    return self.api_call("admin.users.session.list", params=kwargs)
+
+

Lists all active user sessions for an organization +https://docs.slack.dev/reference/methods/admin.users.session.list

+
+
+def admin_users_session_reset(self,
*,
user_id: str,
mobile_only: bool | None = None,
web_only: bool | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def admin_users_session_reset(
+    self,
+    *,
+    user_id: str,
+    mobile_only: Optional[bool] = None,
+    web_only: Optional[bool] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Wipes all valid sessions on all devices for a given user.
+    https://docs.slack.dev/reference/methods/admin.users.session.reset
+    """
+    kwargs.update(
+        {
+            "user_id": user_id,
+            "mobile_only": mobile_only,
+            "web_only": web_only,
+        }
+    )
+    return self.api_call("admin.users.session.reset", params=kwargs)
+
+

Wipes all valid sessions on all devices for a given user. +https://docs.slack.dev/reference/methods/admin.users.session.reset

+
+
+def admin_users_session_resetBulk(self,
*,
user_ids: str | Sequence[str],
mobile_only: bool | None = None,
web_only: bool | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def admin_users_session_resetBulk(
+    self,
+    *,
+    user_ids: Union[str, Sequence[str]],
+    mobile_only: Optional[bool] = None,
+    web_only: Optional[bool] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Enqueues an asynchronous job to wipe all valid sessions on all devices for a given list of users
+    https://docs.slack.dev/reference/methods/admin.users.session.resetBulk
+    """
+    if isinstance(user_ids, (list, tuple)):
+        kwargs.update({"user_ids": ",".join(user_ids)})
+    else:
+        kwargs.update({"user_ids": user_ids})
+    kwargs.update(
+        {
+            "mobile_only": mobile_only,
+            "web_only": web_only,
+        }
+    )
+    return self.api_call("admin.users.session.resetBulk", params=kwargs)
+
+

Enqueues an asynchronous job to wipe all valid sessions on all devices for a given list of users +https://docs.slack.dev/reference/methods/admin.users.session.resetBulk

+
+
+def admin_users_session_setSettings(self,
*,
user_ids: str | Sequence[str],
desktop_app_browser_quit: bool | None = None,
duration: int | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def admin_users_session_setSettings(
+    self,
+    *,
+    user_ids: Union[str, Sequence[str]],
+    desktop_app_browser_quit: Optional[bool] = None,
+    duration: Optional[int] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Configure the user-level session settings—the session duration
+    and what happens when the client closes—for one or more users.
+    https://docs.slack.dev/reference/methods/admin.users.session.setSettings
+    """
+    if isinstance(user_ids, (list, tuple)):
+        kwargs.update({"user_ids": ",".join(user_ids)})
+    else:
+        kwargs.update({"user_ids": user_ids})
+    kwargs.update(
+        {
+            "desktop_app_browser_quit": desktop_app_browser_quit,
+            "duration": duration,
+        }
+    )
+    return self.api_call("admin.users.session.setSettings", params=kwargs)
+
+

Configure the user-level session settings—the session duration +and what happens when the client closes—for one or more users. +https://docs.slack.dev/reference/methods/admin.users.session.setSettings

+
+
+def admin_users_setAdmin(self, *, team_id: str, user_id: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def admin_users_setAdmin(
+    self,
+    *,
+    team_id: str,
+    user_id: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Set an existing guest, regular user, or owner to be an admin user.
+    https://docs.slack.dev/reference/methods/admin.users.setAdmin
+    """
+    kwargs.update({"team_id": team_id, "user_id": user_id})
+    return self.api_call("admin.users.setAdmin", params=kwargs)
+
+

Set an existing guest, regular user, or owner to be an admin user. +https://docs.slack.dev/reference/methods/admin.users.setAdmin

+
+
+def admin_users_setExpiration(self, *, expiration_ts: int, user_id: str, team_id: str | None = None, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def admin_users_setExpiration(
+    self,
+    *,
+    expiration_ts: int,
+    user_id: str,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Set an expiration for a guest user.
+    https://docs.slack.dev/reference/methods/admin.users.setExpiration
+    """
+    kwargs.update({"expiration_ts": expiration_ts, "team_id": team_id, "user_id": user_id})
+    return self.api_call("admin.users.setExpiration", params=kwargs)
+
+ +
+
+def admin_users_setOwner(self, *, team_id: str, user_id: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def admin_users_setOwner(
+    self,
+    *,
+    team_id: str,
+    user_id: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Set an existing guest, regular user, or admin user to be a workspace owner.
+    https://docs.slack.dev/reference/methods/admin.users.setOwner
+    """
+    kwargs.update({"team_id": team_id, "user_id": user_id})
+    return self.api_call("admin.users.setOwner", params=kwargs)
+
+

Set an existing guest, regular user, or admin user to be a workspace owner. +https://docs.slack.dev/reference/methods/admin.users.setOwner

+
+
+def admin_users_setRegular(self, *, team_id: str, user_id: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def admin_users_setRegular(
+    self,
+    *,
+    team_id: str,
+    user_id: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Set an existing guest user, admin user, or owner to be a regular user.
+    https://docs.slack.dev/reference/methods/admin.users.setRegular
+    """
+    kwargs.update({"team_id": team_id, "user_id": user_id})
+    return self.api_call("admin.users.setRegular", params=kwargs)
+
+

Set an existing guest user, admin user, or owner to be a regular user. +https://docs.slack.dev/reference/methods/admin.users.setRegular

+
+
+def admin_users_unsupportedVersions_export(self,
*,
date_end_of_support: str | int | None = None,
date_sessions_started: str | int | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def admin_users_unsupportedVersions_export(
+    self,
+    *,
+    date_end_of_support: Optional[Union[str, int]] = None,
+    date_sessions_started: Optional[Union[str, int]] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Ask Slackbot to send you an export listing all workspace members using unsupported software,
+    presented as a zipped CSV file.
+    https://docs.slack.dev/reference/methods/admin.users.unsupportedVersions.export
+    """
+    kwargs.update(
+        {
+            "date_end_of_support": date_end_of_support,
+            "date_sessions_started": date_sessions_started,
+        }
+    )
+    return self.api_call("admin.users.unsupportedVersions.export", params=kwargs)
+
+

Ask Slackbot to send you an export listing all workspace members using unsupported software, +presented as a zipped CSV file. +https://docs.slack.dev/reference/methods/admin.users.unsupportedVersions.export

+
+
+def admin_workflows_collaborators_add(self,
*,
collaborator_ids: str | Sequence[str],
workflow_ids: str | Sequence[str],
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def admin_workflows_collaborators_add(
+    self,
+    *,
+    collaborator_ids: Union[str, Sequence[str]],
+    workflow_ids: Union[str, Sequence[str]],
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Add collaborators to workflows within the team or enterprise
+    https://docs.slack.dev/reference/methods/admin.workflows.collaborators.add
+    """
+    if isinstance(collaborator_ids, (list, tuple)):
+        kwargs.update({"collaborator_ids": ",".join(collaborator_ids)})
+    else:
+        kwargs.update({"collaborator_ids": collaborator_ids})
+    if isinstance(workflow_ids, (list, tuple)):
+        kwargs.update({"workflow_ids": ",".join(workflow_ids)})
+    else:
+        kwargs.update({"workflow_ids": workflow_ids})
+    return self.api_call("admin.workflows.collaborators.add", params=kwargs)
+
+

Add collaborators to workflows within the team or enterprise +https://docs.slack.dev/reference/methods/admin.workflows.collaborators.add

+
+
+def admin_workflows_collaborators_remove(self,
*,
collaborator_ids: str | Sequence[str],
workflow_ids: str | Sequence[str],
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def admin_workflows_collaborators_remove(
+    self,
+    *,
+    collaborator_ids: Union[str, Sequence[str]],
+    workflow_ids: Union[str, Sequence[str]],
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Remove collaborators from workflows within the team or enterprise
+    https://docs.slack.dev/reference/methods/admin.workflows.collaborators.remove
+    """
+    if isinstance(collaborator_ids, (list, tuple)):
+        kwargs.update({"collaborator_ids": ",".join(collaborator_ids)})
+    else:
+        kwargs.update({"collaborator_ids": collaborator_ids})
+    if isinstance(workflow_ids, (list, tuple)):
+        kwargs.update({"workflow_ids": ",".join(workflow_ids)})
+    else:
+        kwargs.update({"workflow_ids": workflow_ids})
+    return self.api_call("admin.workflows.collaborators.remove", params=kwargs)
+
+

Remove collaborators from workflows within the team or enterprise +https://docs.slack.dev/reference/methods/admin.workflows.collaborators.remove

+
+
+def admin_workflows_permissions_lookup(self,
*,
workflow_ids: str | Sequence[str],
max_workflow_triggers: int | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def admin_workflows_permissions_lookup(
+    self,
+    *,
+    workflow_ids: Union[str, Sequence[str]],
+    max_workflow_triggers: Optional[int] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Look up the permissions for a set of workflows
+    https://docs.slack.dev/reference/methods/admin.workflows.permissions.lookup
+    """
+    if isinstance(workflow_ids, (list, tuple)):
+        kwargs.update({"workflow_ids": ",".join(workflow_ids)})
+    else:
+        kwargs.update({"workflow_ids": workflow_ids})
+    kwargs.update(
+        {
+            "max_workflow_triggers": max_workflow_triggers,
+        }
+    )
+    return self.api_call("admin.workflows.permissions.lookup", params=kwargs)
+
+ +
+ +
+
+ +Expand source code + +
def admin_workflows_search(
+    self,
+    *,
+    app_id: Optional[str] = None,
+    collaborator_ids: Optional[Union[str, Sequence[str]]] = None,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    no_collaborators: Optional[bool] = None,
+    num_trigger_ids: Optional[int] = None,
+    query: Optional[str] = None,
+    sort: Optional[str] = None,
+    sort_dir: Optional[str] = None,
+    source: Optional[str] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Search workflows within the team or enterprise
+    https://docs.slack.dev/reference/methods/admin.workflows.search
+    """
+    if collaborator_ids is not None:
+        if isinstance(collaborator_ids, (list, tuple)):
+            kwargs.update({"collaborator_ids": ",".join(collaborator_ids)})
+        else:
+            kwargs.update({"collaborator_ids": collaborator_ids})
+    kwargs.update(
+        {
+            "app_id": app_id,
+            "cursor": cursor,
+            "limit": limit,
+            "no_collaborators": no_collaborators,
+            "num_trigger_ids": num_trigger_ids,
+            "query": query,
+            "sort": sort,
+            "sort_dir": sort_dir,
+            "source": source,
+        }
+    )
+    return self.api_call("admin.workflows.search", params=kwargs)
+
+

Search workflows within the team or enterprise +https://docs.slack.dev/reference/methods/admin.workflows.search

+
+
+def admin_workflows_unpublish(self, *, workflow_ids: str | Sequence[str], **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def admin_workflows_unpublish(
+    self,
+    *,
+    workflow_ids: Union[str, Sequence[str]],
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Unpublish workflows within the team or enterprise
+    https://docs.slack.dev/reference/methods/admin.workflows.unpublish
+    """
+    if isinstance(workflow_ids, (list, tuple)):
+        kwargs.update({"workflow_ids": ",".join(workflow_ids)})
+    else:
+        kwargs.update({"workflow_ids": workflow_ids})
+    return self.api_call("admin.workflows.unpublish", params=kwargs)
+
+

Unpublish workflows within the team or enterprise +https://docs.slack.dev/reference/methods/admin.workflows.unpublish

+
+
+def api_test(self, *, error: str | None = None, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def api_test(
+    self,
+    *,
+    error: Optional[str] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Checks API calling code.
+    https://docs.slack.dev/reference/methods/api.test
+    """
+    kwargs.update({"error": error})
+    return self.api_call("api.test", params=kwargs)
+
+ +
+
+def apps_connections_open(self, *, app_token: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def apps_connections_open(
+    self,
+    *,
+    app_token: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Generate a temporary Socket Mode WebSocket URL that your app can connect to
+    in order to receive events and interactive payloads
+    https://docs.slack.dev/reference/methods/apps.connections.open
+    """
+    kwargs.update({"token": app_token})
+    return self.api_call("apps.connections.open", http_verb="POST", params=kwargs)
+
+

Generate a temporary Socket Mode WebSocket URL that your app can connect to +in order to receive events and interactive payloads +https://docs.slack.dev/reference/methods/apps.connections.open

+
+
+def apps_event_authorizations_list(self,
*,
event_context: str,
cursor: str | None = None,
limit: int | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def apps_event_authorizations_list(
+    self,
+    *,
+    event_context: str,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Get a list of authorizations for the given event context.
+    Each authorization represents an app installation that the event is visible to.
+    https://docs.slack.dev/reference/methods/apps.event.authorizations.list
+    """
+    kwargs.update({"event_context": event_context, "cursor": cursor, "limit": limit})
+    return self.api_call("apps.event.authorizations.list", params=kwargs)
+
+

Get a list of authorizations for the given event context. +Each authorization represents an app installation that the event is visible to. +https://docs.slack.dev/reference/methods/apps.event.authorizations.list

+
+
+def apps_manifest_create(self, *, manifest: str | Dict[str, Any], **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def apps_manifest_create(
+    self,
+    *,
+    manifest: Union[str, Dict[str, Any]],
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Create an app from an app manifest
+    https://docs.slack.dev/reference/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) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def apps_manifest_delete(
+    self,
+    *,
+    app_id: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Permanently deletes an app created through app manifests
+    https://docs.slack.dev/reference/methods/apps.manifest.delete
+    """
+    kwargs.update({"app_id": app_id})
+    return self.api_call("apps.manifest.delete", params=kwargs)
+
+

Permanently deletes an app created through app manifests +https://docs.slack.dev/reference/methods/apps.manifest.delete

+
+
+def apps_manifest_export(self, *, app_id: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def apps_manifest_export(
+    self,
+    *,
+    app_id: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Export an app manifest from an existing app
+    https://docs.slack.dev/reference/methods/apps.manifest.export
+    """
+    kwargs.update({"app_id": app_id})
+    return self.api_call("apps.manifest.export", params=kwargs)
+
+

Export an app manifest from an existing app +https://docs.slack.dev/reference/methods/apps.manifest.export

+
+
+def apps_manifest_update(self, *, app_id: str, manifest: str | Dict[str, Any], **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
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://docs.slack.dev/reference/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: str | Dict[str, Any], app_id: str | None = None, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
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://docs.slack.dev/reference/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 apps_uninstall(self, *, client_id: str, client_secret: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def apps_uninstall(
+    self,
+    *,
+    client_id: str,
+    client_secret: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Uninstalls your app from a workspace.
+    https://docs.slack.dev/reference/methods/apps.uninstall
+    """
+    kwargs.update({"client_id": client_id, "client_secret": client_secret})
+    return self.api_call("apps.uninstall", params=kwargs)
+
+

Uninstalls your app from a workspace. +https://docs.slack.dev/reference/methods/apps.uninstall

+
+
+def assistant_threads_setStatus(self,
*,
channel_id: str,
thread_ts: str,
status: str,
loading_messages: List[str] | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def assistant_threads_setStatus(
+    self,
+    *,
+    channel_id: str,
+    thread_ts: str,
+    status: str,
+    loading_messages: Optional[List[str]] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Set the status for an AI assistant thread.
+    https://docs.slack.dev/reference/methods/assistant.threads.setStatus
+    """
+    kwargs.update(
+        {"channel_id": channel_id, "thread_ts": thread_ts, "status": status, "loading_messages": loading_messages}
+    )
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("assistant.threads.setStatus", json=kwargs)
+
+ +
+
+def assistant_threads_setSuggestedPrompts(self,
*,
channel_id: str,
thread_ts: str,
title: str | None = None,
prompts: List[Dict[str, str]],
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def assistant_threads_setSuggestedPrompts(
+    self,
+    *,
+    channel_id: str,
+    thread_ts: str,
+    title: Optional[str] = None,
+    prompts: List[Dict[str, str]],
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Set suggested prompts for the given assistant thread.
+    https://docs.slack.dev/reference/methods/assistant.threads.setSuggestedPrompts
+    """
+    kwargs.update({"channel_id": channel_id, "thread_ts": thread_ts, "prompts": prompts})
+    if title is not None:
+        kwargs.update({"title": title})
+    return self.api_call("assistant.threads.setSuggestedPrompts", json=kwargs)
+
+

Set suggested prompts for the given assistant thread. +https://docs.slack.dev/reference/methods/assistant.threads.setSuggestedPrompts

+
+
+def assistant_threads_setTitle(self, *, channel_id: str, thread_ts: str, title: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def assistant_threads_setTitle(
+    self,
+    *,
+    channel_id: str,
+    thread_ts: str,
+    title: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Set the title for the given assistant thread.
+    https://docs.slack.dev/reference/methods/assistant.threads.setTitle
+    """
+    kwargs.update({"channel_id": channel_id, "thread_ts": thread_ts, "title": title})
+    return self.api_call("assistant.threads.setTitle", params=kwargs)
+
+

Set the title for the given assistant thread. +https://docs.slack.dev/reference/methods/assistant.threads.setTitle

+
+
+def auth_revoke(self, *, test: bool | None = None, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def auth_revoke(
+    self,
+    *,
+    test: Optional[bool] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Revokes a token.
+    https://docs.slack.dev/reference/methods/auth.revoke
+    """
+    kwargs.update({"test": test})
+    return self.api_call("auth.revoke", http_verb="GET", params=kwargs)
+
+ +
+
+def auth_teams_list(self,
cursor: str | None = None,
limit: int | None = None,
include_icon: bool | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def auth_teams_list(
+    self,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    include_icon: Optional[bool] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """List the workspaces a token can access.
+    https://docs.slack.dev/reference/methods/auth.teams.list
+    """
+    kwargs.update({"cursor": cursor, "limit": limit, "include_icon": include_icon})
+    return self.api_call("auth.teams.list", params=kwargs)
+
+

List the workspaces a token can access. +https://docs.slack.dev/reference/methods/auth.teams.list

+
+
+def auth_test(self, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def auth_test(
+    self,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Checks authentication & identity.
+    https://docs.slack.dev/reference/methods/auth.test
+    """
+    return self.api_call("auth.test", params=kwargs)
+
+

Checks authentication & identity. +https://docs.slack.dev/reference/methods/auth.test

+
+
+def bookmarks_add(self,
*,
channel_id: str,
title: str,
type: str,
emoji: str | None = None,
entity_id: str | None = None,
link: str | None = None,
parent_id: str | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def bookmarks_add(
+    self,
+    *,
+    channel_id: str,
+    title: str,
+    type: str,
+    emoji: Optional[str] = None,
+    entity_id: Optional[str] = None,
+    link: Optional[str] = None,  # include when type is 'link'
+    parent_id: Optional[str] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Add bookmark to a channel.
+    https://docs.slack.dev/reference/methods/bookmarks.add
+    """
+    kwargs.update(
+        {
+            "channel_id": channel_id,
+            "title": title,
+            "type": type,
+            "emoji": emoji,
+            "entity_id": entity_id,
+            "link": link,
+            "parent_id": parent_id,
+        }
+    )
+    return self.api_call("bookmarks.add", http_verb="POST", params=kwargs)
+
+ +
+
+def bookmarks_edit(self,
*,
bookmark_id: str,
channel_id: str,
emoji: str | None = None,
link: str | None = None,
title: str | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def bookmarks_edit(
+    self,
+    *,
+    bookmark_id: str,
+    channel_id: str,
+    emoji: Optional[str] = None,
+    link: Optional[str] = None,
+    title: Optional[str] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Edit bookmark.
+    https://docs.slack.dev/reference/methods/bookmarks.edit
+    """
+    kwargs.update(
+        {
+            "bookmark_id": bookmark_id,
+            "channel_id": channel_id,
+            "emoji": emoji,
+            "link": link,
+            "title": title,
+        }
+    )
+    return self.api_call("bookmarks.edit", http_verb="POST", params=kwargs)
+
+ +
+
+def bookmarks_list(self, *, channel_id: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def bookmarks_list(
+    self,
+    *,
+    channel_id: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """List bookmark for the channel.
+    https://docs.slack.dev/reference/methods/bookmarks.list
+    """
+    kwargs.update({"channel_id": channel_id})
+    return self.api_call("bookmarks.list", http_verb="POST", params=kwargs)
+
+ +
+
+def bookmarks_remove(self, *, bookmark_id: str, channel_id: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def bookmarks_remove(
+    self,
+    *,
+    bookmark_id: str,
+    channel_id: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Remove bookmark from the channel.
+    https://docs.slack.dev/reference/methods/bookmarks.remove
+    """
+    kwargs.update({"bookmark_id": bookmark_id, "channel_id": channel_id})
+    return self.api_call("bookmarks.remove", http_verb="POST", params=kwargs)
+
+ +
+
+def bots_info(self, *, bot: str | None = None, team_id: str | None = None, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def bots_info(
+    self,
+    *,
+    bot: Optional[str] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Gets information about a bot user.
+    https://docs.slack.dev/reference/methods/bots.info
+    """
+    kwargs.update({"bot": bot, "team_id": team_id})
+    return self.api_call("bots.info", http_verb="GET", params=kwargs)
+
+

Gets information about a bot user. +https://docs.slack.dev/reference/methods/bots.info

+
+
+def calls_add(self,
*,
external_unique_id: str,
join_url: str,
created_by: str | None = None,
date_start: int | None = None,
desktop_app_join_url: str | None = None,
external_display_id: str | None = None,
title: str | None = None,
users: str | Sequence[Dict[str, str]] | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def calls_add(
+    self,
+    *,
+    external_unique_id: str,
+    join_url: str,
+    created_by: Optional[str] = None,
+    date_start: Optional[int] = None,
+    desktop_app_join_url: Optional[str] = None,
+    external_display_id: Optional[str] = None,
+    title: Optional[str] = None,
+    users: Optional[Union[str, Sequence[Dict[str, str]]]] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Registers a new Call.
+    https://docs.slack.dev/reference/methods/calls.add
+    """
+    kwargs.update(
+        {
+            "external_unique_id": external_unique_id,
+            "join_url": join_url,
+            "created_by": created_by,
+            "date_start": date_start,
+            "desktop_app_join_url": desktop_app_join_url,
+            "external_display_id": external_display_id,
+            "title": title,
+        }
+    )
+    _update_call_participants(
+        kwargs,
+        users if users is not None else kwargs.get("users"),  # type: ignore[arg-type]
+    )
+    return self.api_call("calls.add", http_verb="POST", params=kwargs)
+
+ +
+
+def calls_end(self, *, id: str, duration: int | None = None, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def calls_end(
+    self,
+    *,
+    id: str,
+    duration: Optional[int] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Ends a Call.
+    https://docs.slack.dev/reference/methods/calls.end
+    """
+    kwargs.update({"id": id, "duration": duration})
+    return self.api_call("calls.end", http_verb="POST", params=kwargs)
+
+ +
+
+def calls_info(self, *, id: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def calls_info(
+    self,
+    *,
+    id: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Returns information about a Call.
+    https://docs.slack.dev/reference/methods/calls.info
+    """
+    kwargs.update({"id": id})
+    return self.api_call("calls.info", http_verb="POST", params=kwargs)
+
+

Returns information about a Call. +https://docs.slack.dev/reference/methods/calls.info

+
+
+def calls_participants_add(self, *, id: str, users: str | Sequence[Dict[str, str]], **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def calls_participants_add(
+    self,
+    *,
+    id: str,
+    users: Union[str, Sequence[Dict[str, str]]],
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Registers new participants added to a Call.
+    https://docs.slack.dev/reference/methods/calls.participants.add
+    """
+    kwargs.update({"id": id})
+    _update_call_participants(kwargs, users)
+    return self.api_call("calls.participants.add", http_verb="POST", params=kwargs)
+
+

Registers new participants added to a Call. +https://docs.slack.dev/reference/methods/calls.participants.add

+
+
+def calls_participants_remove(self, *, id: str, users: str | Sequence[Dict[str, str]], **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def calls_participants_remove(
+    self,
+    *,
+    id: str,
+    users: Union[str, Sequence[Dict[str, str]]],
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Registers participants removed from a Call.
+    https://docs.slack.dev/reference/methods/calls.participants.remove
+    """
+    kwargs.update({"id": id})
+    _update_call_participants(kwargs, users)
+    return self.api_call("calls.participants.remove", http_verb="POST", params=kwargs)
+
+

Registers participants removed from a Call. +https://docs.slack.dev/reference/methods/calls.participants.remove

+
+
+def calls_update(self,
*,
id: str,
desktop_app_join_url: str | None = None,
join_url: str | None = None,
title: str | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def calls_update(
+    self,
+    *,
+    id: str,
+    desktop_app_join_url: Optional[str] = None,
+    join_url: Optional[str] = None,
+    title: Optional[str] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Updates information about a Call.
+    https://docs.slack.dev/reference/methods/calls.update
+    """
+    kwargs.update(
+        {
+            "id": id,
+            "desktop_app_join_url": desktop_app_join_url,
+            "join_url": join_url,
+            "title": title,
+        }
+    )
+    return self.api_call("calls.update", http_verb="POST", params=kwargs)
+
+ +
+
+def canvases_access_delete(self,
*,
canvas_id: str,
channel_ids: str | Sequence[str] | None = None,
user_ids: str | Sequence[str] | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def canvases_access_delete(
+    self,
+    *,
+    canvas_id: str,
+    channel_ids: Optional[Union[Sequence[str], str]] = None,
+    user_ids: Optional[Union[Sequence[str], str]] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Create a Channel Canvas for a channel
+    https://docs.slack.dev/reference/methods/canvases.access.delete
+    """
+    kwargs.update({"canvas_id": canvas_id})
+    if channel_ids is not None:
+        if isinstance(channel_ids, (list, tuple)):
+            kwargs.update({"channel_ids": ",".join(channel_ids)})
+        else:
+            kwargs.update({"channel_ids": channel_ids})
+    if user_ids is not None:
+        if isinstance(user_ids, (list, tuple)):
+            kwargs.update({"user_ids": ",".join(user_ids)})
+        else:
+            kwargs.update({"user_ids": user_ids})
+    return self.api_call("canvases.access.delete", params=kwargs)
+
+ +
+
+def canvases_access_set(self,
*,
canvas_id: str,
access_level: str,
channel_ids: str | Sequence[str] | None = None,
user_ids: str | Sequence[str] | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def canvases_access_set(
+    self,
+    *,
+    canvas_id: str,
+    access_level: str,
+    channel_ids: Optional[Union[Sequence[str], str]] = None,
+    user_ids: Optional[Union[Sequence[str], str]] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Sets the access level to a canvas for specified entities
+    https://docs.slack.dev/reference/methods/canvases.access.set
+    """
+    kwargs.update({"canvas_id": canvas_id, "access_level": access_level})
+    if channel_ids is not None:
+        if isinstance(channel_ids, (list, tuple)):
+            kwargs.update({"channel_ids": ",".join(channel_ids)})
+        else:
+            kwargs.update({"channel_ids": channel_ids})
+    if user_ids is not None:
+        if isinstance(user_ids, (list, tuple)):
+            kwargs.update({"user_ids": ",".join(user_ids)})
+        else:
+            kwargs.update({"user_ids": user_ids})
+
+    return self.api_call("canvases.access.set", params=kwargs)
+
+

Sets the access level to a canvas for specified entities +https://docs.slack.dev/reference/methods/canvases.access.set

+
+
+def canvases_create(self, *, title: str | None = None, document_content: Dict[str, str], **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def canvases_create(
+    self,
+    *,
+    title: Optional[str] = None,
+    document_content: Dict[str, str],
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Create Canvas for a user
+    https://docs.slack.dev/reference/methods/canvases.create
+    """
+    kwargs.update({"title": title, "document_content": document_content})
+    return self.api_call("canvases.create", json=kwargs)
+
+ +
+
+def canvases_delete(self, *, canvas_id: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def canvases_delete(
+    self,
+    *,
+    canvas_id: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Deletes a canvas
+    https://docs.slack.dev/reference/methods/canvases.delete
+    """
+    kwargs.update({"canvas_id": canvas_id})
+    return self.api_call("canvases.delete", params=kwargs)
+
+ +
+
+def canvases_edit(self, *, canvas_id: str, changes: Sequence[Dict[str, Any]], **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def canvases_edit(
+    self,
+    *,
+    canvas_id: str,
+    changes: Sequence[Dict[str, Any]],
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Update an existing canvas
+    https://docs.slack.dev/reference/methods/canvases.edit
+    """
+    kwargs.update({"canvas_id": canvas_id, "changes": changes})
+    return self.api_call("canvases.edit", json=kwargs)
+
+ +
+
+def canvases_sections_lookup(self, *, canvas_id: str, criteria: Dict[str, Any], **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def canvases_sections_lookup(
+    self,
+    *,
+    canvas_id: str,
+    criteria: Dict[str, Any],
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Find sections matching the provided criteria
+    https://docs.slack.dev/reference/methods/canvases.sections.lookup
+    """
+    kwargs.update({"canvas_id": canvas_id, "criteria": json.dumps(criteria)})
+    return self.api_call("canvases.sections.lookup", params=kwargs)
+
+

Find sections matching the provided criteria +https://docs.slack.dev/reference/methods/canvases.sections.lookup

+
+
+def channels_archive(self, *, channel: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def channels_archive(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Archives a channel."""
+    kwargs.update({"channel": channel})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("channels.archive", json=kwargs)
+
+

Archives a channel.

+
+
+def channels_create(self, *, name: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def channels_create(
+    self,
+    *,
+    name: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Creates a channel."""
+    kwargs.update({"name": name})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("channels.create", json=kwargs)
+
+

Creates a channel.

+
+
+def channels_history(self, *, channel: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def channels_history(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Fetches history of messages and events from a channel."""
+    kwargs.update({"channel": channel})
+    return self.api_call("channels.history", http_verb="GET", params=kwargs)
+
+

Fetches history of messages and events from a channel.

+
+
+def channels_info(self, *, channel: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def channels_info(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Gets information about a channel."""
+    kwargs.update({"channel": channel})
+    return self.api_call("channels.info", http_verb="GET", params=kwargs)
+
+

Gets information about a channel.

+
+
+def channels_invite(self, *, channel: str, user: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def channels_invite(
+    self,
+    *,
+    channel: str,
+    user: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Invites a user to a channel."""
+    kwargs.update({"channel": channel, "user": user})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("channels.invite", json=kwargs)
+
+

Invites a user to a channel.

+
+
+def channels_join(self, *, name: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def channels_join(
+    self,
+    *,
+    name: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Joins a channel, creating it if needed."""
+    kwargs.update({"name": name})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("channels.join", json=kwargs)
+
+

Joins a channel, creating it if needed.

+
+
+def channels_kick(self, *, channel: str, user: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def channels_kick(
+    self,
+    *,
+    channel: str,
+    user: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Removes a user from a channel."""
+    kwargs.update({"channel": channel, "user": user})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("channels.kick", json=kwargs)
+
+

Removes a user from a channel.

+
+
+def channels_leave(self, *, channel: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def channels_leave(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Leaves a channel."""
+    kwargs.update({"channel": channel})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("channels.leave", json=kwargs)
+
+

Leaves a channel.

+
+
+def channels_list(self, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def channels_list(
+    self,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Lists all channels in a Slack team."""
+    return self.api_call("channels.list", http_verb="GET", params=kwargs)
+
+

Lists all channels in a Slack team.

+
+
+def channels_mark(self, *, channel: str, ts: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def channels_mark(
+    self,
+    *,
+    channel: str,
+    ts: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Sets the read cursor in a channel."""
+    kwargs.update({"channel": channel, "ts": ts})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("channels.mark", json=kwargs)
+
+

Sets the read cursor in a channel.

+
+
+def channels_rename(self, *, channel: str, name: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def channels_rename(
+    self,
+    *,
+    channel: str,
+    name: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Renames a channel."""
+    kwargs.update({"channel": channel, "name": name})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("channels.rename", json=kwargs)
+
+

Renames a channel.

+
+
+def channels_replies(self, *, channel: str, thread_ts: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def channels_replies(
+    self,
+    *,
+    channel: str,
+    thread_ts: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Retrieve a thread of messages posted to a channel"""
+    kwargs.update({"channel": channel, "thread_ts": thread_ts})
+    return self.api_call("channels.replies", http_verb="GET", params=kwargs)
+
+

Retrieve a thread of messages posted to a channel

+
+
+def channels_setPurpose(self, *, channel: str, purpose: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def channels_setPurpose(
+    self,
+    *,
+    channel: str,
+    purpose: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Sets the purpose for a channel."""
+    kwargs.update({"channel": channel, "purpose": purpose})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("channels.setPurpose", json=kwargs)
+
+

Sets the purpose for a channel.

+
+
+def channels_setTopic(self, *, channel: str, topic: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def channels_setTopic(
+    self,
+    *,
+    channel: str,
+    topic: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Sets the topic for a channel."""
+    kwargs.update({"channel": channel, "topic": topic})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("channels.setTopic", json=kwargs)
+
+

Sets the topic for a channel.

+
+
+def channels_unarchive(self, *, channel: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def channels_unarchive(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Unarchives a channel."""
+    kwargs.update({"channel": channel})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("channels.unarchive", json=kwargs)
+
+

Unarchives a channel.

+
+
+def chat_appendStream(self, *, channel: str, ts: str, markdown_text: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def chat_appendStream(
+    self,
+    *,
+    channel: str,
+    ts: str,
+    markdown_text: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Appends text to an existing streaming conversation.
+    https://docs.slack.dev/reference/methods/chat.appendStream
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "ts": ts,
+            "markdown_text": markdown_text,
+        }
+    )
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("chat.appendStream", json=kwargs)
+
+

Appends text to an existing streaming conversation. +https://docs.slack.dev/reference/methods/chat.appendStream

+
+
+def chat_delete(self, *, channel: str, ts: str, as_user: bool | None = None, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def chat_delete(
+    self,
+    *,
+    channel: str,
+    ts: str,
+    as_user: Optional[bool] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Deletes a message.
+    https://docs.slack.dev/reference/methods/chat.delete
+    """
+    kwargs.update({"channel": channel, "ts": ts, "as_user": as_user})
+    return self.api_call("chat.delete", params=kwargs)
+
+ +
+
+def chat_deleteScheduledMessage(self,
*,
channel: str,
scheduled_message_id: str,
as_user: bool | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def chat_deleteScheduledMessage(
+    self,
+    *,
+    channel: str,
+    scheduled_message_id: str,
+    as_user: Optional[bool] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Deletes a scheduled message.
+    https://docs.slack.dev/reference/methods/chat.deleteScheduledMessage
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "scheduled_message_id": scheduled_message_id,
+            "as_user": as_user,
+        }
+    )
+    return self.api_call("chat.deleteScheduledMessage", params=kwargs)
+
+ +
+ +
+
+ +Expand source code + +
def chat_getPermalink(
+    self,
+    *,
+    channel: str,
+    message_ts: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Retrieve a permalink URL for a specific extant message
+    https://docs.slack.dev/reference/methods/chat.getPermalink
+    """
+    kwargs.update({"channel": channel, "message_ts": message_ts})
+    return self.api_call("chat.getPermalink", http_verb="GET", params=kwargs)
+
+

Retrieve a permalink URL for a specific extant message +https://docs.slack.dev/reference/methods/chat.getPermalink

+
+
+def chat_meMessage(self, *, channel: str, text: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def chat_meMessage(
+    self,
+    *,
+    channel: str,
+    text: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Share a me message into a channel.
+    https://docs.slack.dev/reference/methods/chat.meMessage
+    """
+    kwargs.update({"channel": channel, "text": text})
+    return self.api_call("chat.meMessage", params=kwargs)
+
+ +
+
+def chat_postEphemeral(self,
*,
channel: str,
user: str,
text: str | None = None,
as_user: bool | None = None,
attachments: str | Sequence[Dict | Attachment] | None = None,
blocks: str | Sequence[Dict | Block] | None = None,
thread_ts: str | None = None,
icon_emoji: str | None = None,
icon_url: str | None = None,
link_names: bool | None = None,
username: str | None = None,
parse: str | None = None,
markdown_text: str | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def chat_postEphemeral(
+    self,
+    *,
+    channel: str,
+    user: str,
+    text: Optional[str] = None,
+    as_user: Optional[bool] = None,
+    attachments: Optional[Union[str, Sequence[Union[Dict, Attachment]]]] = None,
+    blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None,
+    thread_ts: Optional[str] = None,
+    icon_emoji: Optional[str] = None,
+    icon_url: Optional[str] = None,
+    link_names: Optional[bool] = None,
+    username: Optional[str] = None,
+    parse: Optional[str] = None,
+    markdown_text: Optional[str] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Sends an ephemeral message to a user in a channel.
+    https://docs.slack.dev/reference/methods/chat.postEphemeral
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "user": user,
+            "text": text,
+            "as_user": as_user,
+            "attachments": attachments,
+            "blocks": blocks,
+            "thread_ts": thread_ts,
+            "icon_emoji": icon_emoji,
+            "icon_url": icon_url,
+            "link_names": link_names,
+            "username": username,
+            "parse": parse,
+            "markdown_text": markdown_text,
+        }
+    )
+    _parse_web_class_objects(kwargs)
+    kwargs = _remove_none_values(kwargs)
+    _warn_if_message_text_content_is_missing("chat.postEphemeral", kwargs)
+    # NOTE: intentionally using json over params for the API methods using blocks/attachments
+    return self.api_call("chat.postEphemeral", json=kwargs)
+
+

Sends an ephemeral message to a user in a channel. +https://docs.slack.dev/reference/methods/chat.postEphemeral

+
+
+def chat_postMessage(self,
*,
channel: str,
text: str | None = None,
as_user: bool | None = None,
attachments: str | Sequence[Dict | Attachment] | None = None,
blocks: str | Sequence[Dict | Block] | None = None,
thread_ts: str | None = None,
reply_broadcast: bool | None = None,
unfurl_links: bool | None = None,
unfurl_media: bool | None = None,
container_id: str | None = None,
icon_emoji: str | None = None,
icon_url: str | None = None,
mrkdwn: bool | None = None,
link_names: bool | None = None,
username: str | None = None,
parse: str | None = None,
metadata: Dict | Metadata | EventAndEntityMetadata | None = None,
markdown_text: str | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def chat_postMessage(
+    self,
+    *,
+    channel: str,
+    text: Optional[str] = None,
+    as_user: Optional[bool] = None,
+    attachments: Optional[Union[str, Sequence[Union[Dict, Attachment]]]] = None,
+    blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None,
+    thread_ts: Optional[str] = None,
+    reply_broadcast: Optional[bool] = None,
+    unfurl_links: Optional[bool] = None,
+    unfurl_media: Optional[bool] = None,
+    container_id: Optional[str] = None,
+    icon_emoji: Optional[str] = None,
+    icon_url: Optional[str] = None,
+    mrkdwn: Optional[bool] = None,
+    link_names: Optional[bool] = None,
+    username: Optional[str] = None,
+    parse: Optional[str] = None,  # none, full
+    metadata: Optional[Union[Dict, Metadata, EventAndEntityMetadata]] = None,
+    markdown_text: Optional[str] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Sends a message to a channel.
+    https://docs.slack.dev/reference/methods/chat.postMessage
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "text": text,
+            "as_user": as_user,
+            "attachments": attachments,
+            "blocks": blocks,
+            "thread_ts": thread_ts,
+            "reply_broadcast": reply_broadcast,
+            "unfurl_links": unfurl_links,
+            "unfurl_media": unfurl_media,
+            "container_id": container_id,
+            "icon_emoji": icon_emoji,
+            "icon_url": icon_url,
+            "mrkdwn": mrkdwn,
+            "link_names": link_names,
+            "username": username,
+            "parse": parse,
+            "metadata": metadata,
+            "markdown_text": markdown_text,
+        }
+    )
+    _parse_web_class_objects(kwargs)
+    kwargs = _remove_none_values(kwargs)
+    _warn_if_message_text_content_is_missing("chat.postMessage", kwargs)
+    # NOTE: intentionally using json over params for the API methods using blocks/attachments
+    return self.api_call("chat.postMessage", json=kwargs)
+
+ +
+
+def chat_scheduleMessage(self,
*,
channel: str,
post_at: str | int,
text: str | None = None,
as_user: bool | None = None,
attachments: str | Sequence[Dict | Attachment] | None = None,
blocks: str | Sequence[Dict | Block] | None = None,
thread_ts: str | None = None,
parse: str | None = None,
reply_broadcast: bool | None = None,
unfurl_links: bool | None = None,
unfurl_media: bool | None = None,
link_names: bool | None = None,
metadata: Dict | Metadata | None = None,
markdown_text: str | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def chat_scheduleMessage(
+    self,
+    *,
+    channel: str,
+    post_at: Union[str, int],
+    text: Optional[str] = None,
+    as_user: Optional[bool] = None,
+    attachments: Optional[Union[str, Sequence[Union[Dict, Attachment]]]] = None,
+    blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None,
+    thread_ts: Optional[str] = None,
+    parse: Optional[str] = None,
+    reply_broadcast: Optional[bool] = None,
+    unfurl_links: Optional[bool] = None,
+    unfurl_media: Optional[bool] = None,
+    link_names: Optional[bool] = None,
+    metadata: Optional[Union[Dict, Metadata]] = None,
+    markdown_text: Optional[str] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Schedules a message.
+    https://docs.slack.dev/reference/methods/chat.scheduleMessage
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "post_at": post_at,
+            "text": text,
+            "as_user": as_user,
+            "attachments": attachments,
+            "blocks": blocks,
+            "thread_ts": thread_ts,
+            "reply_broadcast": reply_broadcast,
+            "parse": parse,
+            "unfurl_links": unfurl_links,
+            "unfurl_media": unfurl_media,
+            "link_names": link_names,
+            "metadata": metadata,
+            "markdown_text": markdown_text,
+        }
+    )
+    _parse_web_class_objects(kwargs)
+    kwargs = _remove_none_values(kwargs)
+    _warn_if_message_text_content_is_missing("chat.scheduleMessage", kwargs)
+    # NOTE: intentionally using json over params for the API methods using blocks/attachments
+    return self.api_call("chat.scheduleMessage", json=kwargs)
+
+ +
+
+def chat_scheduledMessages_list(self,
*,
channel: str | None = None,
cursor: str | None = None,
latest: str | None = None,
limit: int | None = None,
oldest: str | None = None,
team_id: str | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def chat_scheduledMessages_list(
+    self,
+    *,
+    channel: Optional[str] = None,
+    cursor: Optional[str] = None,
+    latest: Optional[str] = None,
+    limit: Optional[int] = None,
+    oldest: Optional[str] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Lists all scheduled messages.
+    https://docs.slack.dev/reference/methods/chat.scheduledMessages.list
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "cursor": cursor,
+            "latest": latest,
+            "limit": limit,
+            "oldest": oldest,
+            "team_id": team_id,
+        }
+    )
+    return self.api_call("chat.scheduledMessages.list", params=kwargs)
+
+ +
+
+def chat_startStream(self,
*,
channel: str,
thread_ts: str,
markdown_text: str | None = None,
recipient_team_id: str | None = None,
recipient_user_id: str | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def chat_startStream(
+    self,
+    *,
+    channel: str,
+    thread_ts: str,
+    markdown_text: Optional[str] = None,
+    recipient_team_id: Optional[str] = None,
+    recipient_user_id: Optional[str] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Starts a new streaming conversation.
+    https://docs.slack.dev/reference/methods/chat.startStream
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "thread_ts": thread_ts,
+            "markdown_text": markdown_text,
+            "recipient_team_id": recipient_team_id,
+            "recipient_user_id": recipient_user_id,
+        }
+    )
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("chat.startStream", json=kwargs)
+
+

Starts a new streaming conversation. +https://docs.slack.dev/reference/methods/chat.startStream

+
+
+def chat_stopStream(self,
*,
channel: str,
ts: str,
markdown_text: str | None = None,
blocks: str | Sequence[Dict | Block] | None = None,
metadata: Dict | Metadata | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def chat_stopStream(
+    self,
+    *,
+    channel: str,
+    ts: str,
+    markdown_text: Optional[str] = None,
+    blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None,
+    metadata: Optional[Union[Dict, Metadata]] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Stops a streaming conversation.
+    https://docs.slack.dev/reference/methods/chat.stopStream
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "ts": ts,
+            "markdown_text": markdown_text,
+            "blocks": blocks,
+            "metadata": metadata,
+        }
+    )
+    _parse_web_class_objects(kwargs)
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("chat.stopStream", json=kwargs)
+
+ +
+
+def chat_unfurl(self,
*,
channel: str | None = None,
ts: str | None = None,
source: str | None = None,
unfurl_id: str | None = None,
unfurls: Dict[str, Dict] | None = None,
metadata: Dict | EventAndEntityMetadata | None = None,
user_auth_blocks: str | Sequence[Dict | Block] | None = None,
user_auth_message: str | None = None,
user_auth_required: bool | None = None,
user_auth_url: str | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def chat_unfurl(
+    self,
+    *,
+    channel: Optional[str] = None,
+    ts: Optional[str] = None,
+    source: Optional[str] = None,
+    unfurl_id: Optional[str] = None,
+    unfurls: Optional[Dict[str, Dict]] = None,  # or user_auth_*
+    metadata: Optional[Union[Dict, EventAndEntityMetadata]] = None,
+    user_auth_blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None,
+    user_auth_message: Optional[str] = None,
+    user_auth_required: Optional[bool] = None,
+    user_auth_url: Optional[str] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Provide custom unfurl behavior for user-posted URLs.
+    https://docs.slack.dev/reference/methods/chat.unfurl
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "ts": ts,
+            "source": source,
+            "unfurl_id": unfurl_id,
+            "unfurls": unfurls,
+            "metadata": metadata,
+            "user_auth_blocks": user_auth_blocks,
+            "user_auth_message": user_auth_message,
+            "user_auth_required": user_auth_required,
+            "user_auth_url": user_auth_url,
+        }
+    )
+    _parse_web_class_objects(kwargs)  # for user_auth_blocks
+    kwargs = _remove_none_values(kwargs)
+    # NOTE: intentionally using json over params for API methods using blocks/attachments
+    return self.api_call("chat.unfurl", json=kwargs)
+
+

Provide custom unfurl behavior for user-posted URLs. +https://docs.slack.dev/reference/methods/chat.unfurl

+
+
+def chat_update(self,
*,
channel: str,
ts: str,
text: str | None = None,
attachments: str | Sequence[Dict | Attachment] | None = None,
blocks: str | Sequence[Dict | Block] | None = None,
as_user: bool | None = None,
file_ids: str | Sequence[str] | None = None,
link_names: bool | None = None,
parse: str | None = None,
reply_broadcast: bool | None = None,
metadata: Dict | Metadata | None = None,
markdown_text: str | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def chat_update(
+    self,
+    *,
+    channel: str,
+    ts: str,
+    text: Optional[str] = None,
+    attachments: Optional[Union[str, Sequence[Union[Dict, Attachment]]]] = None,
+    blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None,
+    as_user: Optional[bool] = None,
+    file_ids: Optional[Union[str, Sequence[str]]] = None,
+    link_names: Optional[bool] = None,
+    parse: Optional[str] = None,  # none, full
+    reply_broadcast: Optional[bool] = None,
+    metadata: Optional[Union[Dict, Metadata]] = None,
+    markdown_text: Optional[str] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Updates a message in a channel.
+    https://docs.slack.dev/reference/methods/chat.update
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "ts": ts,
+            "text": text,
+            "attachments": attachments,
+            "blocks": blocks,
+            "as_user": as_user,
+            "link_names": link_names,
+            "parse": parse,
+            "reply_broadcast": reply_broadcast,
+            "metadata": metadata,
+            "markdown_text": markdown_text,
+        }
+    )
+    if isinstance(file_ids, (list, tuple)):
+        kwargs.update({"file_ids": ",".join(file_ids)})
+    else:
+        kwargs.update({"file_ids": file_ids})
+    _parse_web_class_objects(kwargs)
+    kwargs = _remove_none_values(kwargs)
+    _warn_if_message_text_content_is_missing("chat.update", kwargs)
+    # NOTE: intentionally using json over params for API methods using blocks/attachments
+    return self.api_call("chat.update", json=kwargs)
+
+ +
+
+def conversations_acceptSharedInvite(self,
*,
channel_name: str,
channel_id: str | None = None,
invite_id: str | None = None,
free_trial_accepted: bool | None = None,
is_private: bool | None = None,
team_id: str | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def conversations_acceptSharedInvite(
+    self,
+    *,
+    channel_name: str,
+    channel_id: Optional[str] = None,
+    invite_id: Optional[str] = None,
+    free_trial_accepted: Optional[bool] = None,
+    is_private: Optional[bool] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Accepts an invitation to a Slack Connect channel.
+    https://docs.slack.dev/reference/methods/conversations.acceptSharedInvite
+    """
+    if channel_id is None and invite_id is None:
+        raise e.SlackRequestError("Either channel_id or invite_id must be provided.")
+    kwargs.update(
+        {
+            "channel_name": channel_name,
+            "channel_id": channel_id,
+            "invite_id": invite_id,
+            "free_trial_accepted": free_trial_accepted,
+            "is_private": is_private,
+            "team_id": team_id,
+        }
+    )
+    return self.api_call("conversations.acceptSharedInvite", http_verb="POST", params=kwargs)
+
+

Accepts an invitation to a Slack Connect channel. +https://docs.slack.dev/reference/methods/conversations.acceptSharedInvite

+
+
+def conversations_approveSharedInvite(self, *, invite_id: str, target_team: str | None = None, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def conversations_approveSharedInvite(
+    self,
+    *,
+    invite_id: str,
+    target_team: Optional[str] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Approves an invitation to a Slack Connect channel.
+    https://docs.slack.dev/reference/methods/conversations.approveSharedInvite
+    """
+    kwargs.update({"invite_id": invite_id, "target_team": target_team})
+    return self.api_call("conversations.approveSharedInvite", http_verb="POST", params=kwargs)
+
+

Approves an invitation to a Slack Connect channel. +https://docs.slack.dev/reference/methods/conversations.approveSharedInvite

+
+
+def conversations_archive(self, *, channel: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def conversations_archive(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Archives a conversation.
+    https://docs.slack.dev/reference/methods/conversations.archive
+    """
+    kwargs.update({"channel": channel})
+    return self.api_call("conversations.archive", params=kwargs)
+
+ +
+
+def conversations_canvases_create(self, *, channel_id: str, document_content: Dict[str, str], **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def conversations_canvases_create(
+    self,
+    *,
+    channel_id: str,
+    document_content: Dict[str, str],
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Create a Channel Canvas for a channel
+    https://docs.slack.dev/reference/methods/conversations.canvases.create
+    """
+    kwargs.update({"channel_id": channel_id, "document_content": document_content})
+    return self.api_call("conversations.canvases.create", json=kwargs)
+
+ +
+
+def conversations_close(self, *, channel: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def conversations_close(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Closes a direct message or multi-person direct message.
+    https://docs.slack.dev/reference/methods/conversations.close
+    """
+    kwargs.update({"channel": channel})
+    return self.api_call("conversations.close", params=kwargs)
+
+

Closes a direct message or multi-person direct message. +https://docs.slack.dev/reference/methods/conversations.close

+
+
+def conversations_create(self,
*,
name: str,
is_private: bool | None = None,
team_id: str | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def conversations_create(
+    self,
+    *,
+    name: str,
+    is_private: Optional[bool] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Initiates a public or private channel-based conversation
+    https://docs.slack.dev/reference/methods/conversations.create
+    """
+    kwargs.update({"name": name, "is_private": is_private, "team_id": team_id})
+    return self.api_call("conversations.create", params=kwargs)
+
+

Initiates a public or private channel-based conversation +https://docs.slack.dev/reference/methods/conversations.create

+
+
+def conversations_declineSharedInvite(self, *, invite_id: str, target_team: str | None = None, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def conversations_declineSharedInvite(
+    self,
+    *,
+    invite_id: str,
+    target_team: Optional[str] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Declines a Slack Connect channel invite.
+    https://docs.slack.dev/reference/methods/conversations.declineSharedInvite
+    """
+    kwargs.update({"invite_id": invite_id, "target_team": target_team})
+    return self.api_call("conversations.declineSharedInvite", http_verb="GET", params=kwargs)
+
+ +
+
+def conversations_externalInvitePermissions_set(self, *, action: str, channel: str, target_team: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def conversations_externalInvitePermissions_set(
+    self, *, action: str, channel: str, target_team: str, **kwargs
+) -> Union[Future, SlackResponse]:
+    """Sets a team in a shared External Limited channel to a shared Slack Connect channel or vice versa.
+    https://docs.slack.dev/reference/methods/conversations.externalInvitePermissions.set
+    """
+    kwargs.update(
+        {
+            "action": action,
+            "channel": channel,
+            "target_team": target_team,
+        }
+    )
+    return self.api_call("conversations.externalInvitePermissions.set", params=kwargs)
+
+

Sets a team in a shared External Limited channel to a shared Slack Connect channel or vice versa. +https://docs.slack.dev/reference/methods/conversations.externalInvitePermissions.set

+
+
+def conversations_history(self,
*,
channel: str,
cursor: str | None = None,
inclusive: bool | None = None,
include_all_metadata: bool | None = None,
latest: str | None = None,
limit: int | None = None,
oldest: str | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def conversations_history(
+    self,
+    *,
+    channel: str,
+    cursor: Optional[str] = None,
+    inclusive: Optional[bool] = None,
+    include_all_metadata: Optional[bool] = None,
+    latest: Optional[str] = None,
+    limit: Optional[int] = None,
+    oldest: Optional[str] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Fetches a conversation's history of messages and events.
+    https://docs.slack.dev/reference/methods/conversations.history
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "cursor": cursor,
+            "inclusive": inclusive,
+            "include_all_metadata": include_all_metadata,
+            "limit": limit,
+            "latest": latest,
+            "oldest": oldest,
+        }
+    )
+    return self.api_call("conversations.history", http_verb="GET", params=kwargs)
+
+

Fetches a conversation's history of messages and events. +https://docs.slack.dev/reference/methods/conversations.history

+
+
+def conversations_info(self,
*,
channel: str,
include_locale: bool | None = None,
include_num_members: bool | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def conversations_info(
+    self,
+    *,
+    channel: str,
+    include_locale: Optional[bool] = None,
+    include_num_members: Optional[bool] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Retrieve information about a conversation.
+    https://docs.slack.dev/reference/methods/conversations.info
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "include_locale": include_locale,
+            "include_num_members": include_num_members,
+        }
+    )
+    return self.api_call("conversations.info", http_verb="GET", params=kwargs)
+
+

Retrieve information about a conversation. +https://docs.slack.dev/reference/methods/conversations.info

+
+
+def conversations_invite(self,
*,
channel: str,
users: str | Sequence[str],
force: bool | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def conversations_invite(
+    self,
+    *,
+    channel: str,
+    users: Union[str, Sequence[str]],
+    force: Optional[bool] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Invites users to a channel.
+    https://docs.slack.dev/reference/methods/conversations.invite
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "force": force,
+        }
+    )
+    if isinstance(users, (list, tuple)):
+        kwargs.update({"users": ",".join(users)})
+    else:
+        kwargs.update({"users": users})
+    return self.api_call("conversations.invite", params=kwargs)
+
+ +
+
+def conversations_inviteShared(self,
*,
channel: str,
emails: str | Sequence[str] | None = None,
user_ids: str | Sequence[str] | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def conversations_inviteShared(
+    self,
+    *,
+    channel: str,
+    emails: Optional[Union[str, Sequence[str]]] = None,
+    user_ids: Optional[Union[str, Sequence[str]]] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Sends an invitation to a Slack Connect channel.
+    https://docs.slack.dev/reference/methods/conversations.inviteShared
+    """
+    if emails is None and user_ids is None:
+        raise e.SlackRequestError("Either emails or user ids must be provided.")
+    kwargs.update({"channel": channel})
+    if isinstance(emails, (list, tuple)):
+        kwargs.update({"emails": ",".join(emails)})
+    else:
+        kwargs.update({"emails": emails})
+    if isinstance(user_ids, (list, tuple)):
+        kwargs.update({"user_ids": ",".join(user_ids)})
+    else:
+        kwargs.update({"user_ids": user_ids})
+    return self.api_call("conversations.inviteShared", http_verb="GET", params=kwargs)
+
+

Sends an invitation to a Slack Connect channel. +https://docs.slack.dev/reference/methods/conversations.inviteShared

+
+
+def conversations_join(self, *, channel: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def conversations_join(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Joins an existing conversation.
+    https://docs.slack.dev/reference/methods/conversations.join
+    """
+    kwargs.update({"channel": channel})
+    return self.api_call("conversations.join", params=kwargs)
+
+ +
+
+def conversations_kick(self, *, channel: str, user: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def conversations_kick(
+    self,
+    *,
+    channel: str,
+    user: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Removes a user from a conversation.
+    https://docs.slack.dev/reference/methods/conversations.kick
+    """
+    kwargs.update({"channel": channel, "user": user})
+    return self.api_call("conversations.kick", params=kwargs)
+
+ +
+
+def conversations_leave(self, *, channel: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def conversations_leave(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Leaves a conversation.
+    https://docs.slack.dev/reference/methods/conversations.leave
+    """
+    kwargs.update({"channel": channel})
+    return self.api_call("conversations.leave", params=kwargs)
+
+ +
+
+def conversations_list(self,
*,
cursor: str | None = None,
exclude_archived: bool | None = None,
limit: int | None = None,
team_id: str | None = None,
types: str | Sequence[str] | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def conversations_list(
+    self,
+    *,
+    cursor: Optional[str] = None,
+    exclude_archived: Optional[bool] = None,
+    limit: Optional[int] = None,
+    team_id: Optional[str] = None,
+    types: Optional[Union[str, Sequence[str]]] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Lists all channels in a Slack team.
+    https://docs.slack.dev/reference/methods/conversations.list
+    """
+    kwargs.update(
+        {
+            "cursor": cursor,
+            "exclude_archived": exclude_archived,
+            "limit": limit,
+            "team_id": team_id,
+        }
+    )
+    if isinstance(types, (list, tuple)):
+        kwargs.update({"types": ",".join(types)})
+    else:
+        kwargs.update({"types": types})
+    return self.api_call("conversations.list", http_verb="GET", params=kwargs)
+
+ +
+
+def conversations_listConnectInvites(self,
*,
count: int | None = None,
cursor: str | None = None,
team_id: str | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def conversations_listConnectInvites(
+    self,
+    *,
+    count: Optional[int] = None,
+    cursor: Optional[str] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """List shared channel invites that have been generated
+    or received but have not yet been approved by all parties.
+    https://docs.slack.dev/reference/methods/conversations.listConnectInvites
+    """
+    kwargs.update({"count": count, "cursor": cursor, "team_id": team_id})
+    return self.api_call("conversations.listConnectInvites", params=kwargs)
+
+

List shared channel invites that have been generated +or received but have not yet been approved by all parties. +https://docs.slack.dev/reference/methods/conversations.listConnectInvites

+
+
+def conversations_mark(self, *, channel: str, ts: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def conversations_mark(
+    self,
+    *,
+    channel: str,
+    ts: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Sets the read cursor in a channel.
+    https://docs.slack.dev/reference/methods/conversations.mark
+    """
+    kwargs.update({"channel": channel, "ts": ts})
+    return self.api_call("conversations.mark", params=kwargs)
+
+ +
+
+def conversations_members(self, *, channel: str, cursor: str | None = None, limit: int | None = None, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def conversations_members(
+    self,
+    *,
+    channel: str,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Retrieve members of a conversation.
+    https://docs.slack.dev/reference/methods/conversations.members
+    """
+    kwargs.update({"channel": channel, "cursor": cursor, "limit": limit})
+    return self.api_call("conversations.members", http_verb="GET", params=kwargs)
+
+ +
+
+def conversations_open(self,
*,
channel: str | None = None,
return_im: bool | None = None,
users: str | Sequence[str] | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def conversations_open(
+    self,
+    *,
+    channel: Optional[str] = None,
+    return_im: Optional[bool] = None,
+    users: Optional[Union[str, Sequence[str]]] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Opens or resumes a direct message or multi-person direct message.
+    https://docs.slack.dev/reference/methods/conversations.open
+    """
+    if channel is None and users is None:
+        raise e.SlackRequestError("Either channel or users must be provided.")
+    kwargs.update({"channel": channel, "return_im": return_im})
+    if isinstance(users, (list, tuple)):
+        kwargs.update({"users": ",".join(users)})
+    else:
+        kwargs.update({"users": users})
+    return self.api_call("conversations.open", params=kwargs)
+
+

Opens or resumes a direct message or multi-person direct message. +https://docs.slack.dev/reference/methods/conversations.open

+
+
+def conversations_rename(self, *, channel: str, name: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def conversations_rename(
+    self,
+    *,
+    channel: str,
+    name: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Renames a conversation.
+    https://docs.slack.dev/reference/methods/conversations.rename
+    """
+    kwargs.update({"channel": channel, "name": name})
+    return self.api_call("conversations.rename", params=kwargs)
+
+ +
+
+def conversations_replies(self,
*,
channel: str,
ts: str,
cursor: str | None = None,
inclusive: bool | None = None,
include_all_metadata: bool | None = None,
latest: str | None = None,
limit: int | None = None,
oldest: str | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def conversations_replies(
+    self,
+    *,
+    channel: str,
+    ts: str,
+    cursor: Optional[str] = None,
+    inclusive: Optional[bool] = None,
+    include_all_metadata: Optional[bool] = None,
+    latest: Optional[str] = None,
+    limit: Optional[int] = None,
+    oldest: Optional[str] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Retrieve a thread of messages posted to a conversation
+    https://docs.slack.dev/reference/methods/conversations.replies
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "ts": ts,
+            "cursor": cursor,
+            "inclusive": inclusive,
+            "include_all_metadata": include_all_metadata,
+            "limit": limit,
+            "latest": latest,
+            "oldest": oldest,
+        }
+    )
+    return self.api_call("conversations.replies", http_verb="GET", params=kwargs)
+
+

Retrieve a thread of messages posted to a conversation +https://docs.slack.dev/reference/methods/conversations.replies

+
+
+def conversations_requestSharedInvite_approve(self,
*,
invite_id: str,
channel_id: str | None = None,
is_external_limited: str | None = None,
message: Dict[str, Any] | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def conversations_requestSharedInvite_approve(
+    self,
+    *,
+    invite_id: str,
+    channel_id: Optional[str] = None,
+    is_external_limited: Optional[str] = None,
+    message: Optional[Dict[str, Any]] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Approve a request to add an external user to a channel. This also sends them a Slack Connect invite.
+    https://docs.slack.dev/reference/methods/conversations.requestSharedInvite.approve
+    """
+    kwargs.update(
+        {
+            "invite_id": invite_id,
+            "channel_id": channel_id,
+            "is_external_limited": is_external_limited,
+        }
+    )
+    if message is not None:
+        kwargs.update({"message": json.dumps(message)})
+    return self.api_call("conversations.requestSharedInvite.approve", params=kwargs)
+
+

Approve a request to add an external user to a channel. This also sends them a Slack Connect invite. +https://docs.slack.dev/reference/methods/conversations.requestSharedInvite.approve

+
+
+def conversations_requestSharedInvite_deny(self, *, invite_id: str, message: str | None = None, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def conversations_requestSharedInvite_deny(
+    self,
+    *,
+    invite_id: str,
+    message: Optional[str] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Deny a request to invite an external user to a channel.
+    https://docs.slack.dev/reference/methods/conversations.requestSharedInvite.deny
+    """
+    kwargs.update({"invite_id": invite_id, "message": message})
+    return self.api_call("conversations.requestSharedInvite.deny", params=kwargs)
+
+

Deny a request to invite an external user to a channel. +https://docs.slack.dev/reference/methods/conversations.requestSharedInvite.deny

+
+
+def conversations_requestSharedInvite_list(self,
*,
cursor: str | None = None,
include_approved: bool | None = None,
include_denied: bool | None = None,
include_expired: bool | None = None,
invite_ids: str | Sequence[str] | None = None,
limit: int | None = None,
user_id: str | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def conversations_requestSharedInvite_list(
+    self,
+    *,
+    cursor: Optional[str] = None,
+    include_approved: Optional[bool] = None,
+    include_denied: Optional[bool] = None,
+    include_expired: Optional[bool] = None,
+    invite_ids: Optional[Union[str, Sequence[str]]] = None,
+    limit: Optional[int] = None,
+    user_id: Optional[str] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Lists requests to add external users to channels with ability to filter.
+    https://docs.slack.dev/reference/methods/conversations.requestSharedInvite.list
+    """
+    kwargs.update(
+        {
+            "cursor": cursor,
+            "include_approved": include_approved,
+            "include_denied": include_denied,
+            "include_expired": include_expired,
+            "limit": limit,
+            "user_id": user_id,
+        }
+    )
+    if invite_ids is not None:
+        if isinstance(invite_ids, (list, tuple)):
+            kwargs.update({"invite_ids": ",".join(invite_ids)})
+        else:
+            kwargs.update({"invite_ids": invite_ids})
+    return self.api_call("conversations.requestSharedInvite.list", params=kwargs)
+
+

Lists requests to add external users to channels with ability to filter. +https://docs.slack.dev/reference/methods/conversations.requestSharedInvite.list

+
+
+def conversations_setPurpose(self, *, channel: str, purpose: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def conversations_setPurpose(
+    self,
+    *,
+    channel: str,
+    purpose: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Sets the purpose for a conversation.
+    https://docs.slack.dev/reference/methods/conversations.setPurpose
+    """
+    kwargs.update({"channel": channel, "purpose": purpose})
+    return self.api_call("conversations.setPurpose", params=kwargs)
+
+ +
+
+def conversations_setTopic(self, *, channel: str, topic: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def conversations_setTopic(
+    self,
+    *,
+    channel: str,
+    topic: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Sets the topic for a conversation.
+    https://docs.slack.dev/reference/methods/conversations.setTopic
+    """
+    kwargs.update({"channel": channel, "topic": topic})
+    return self.api_call("conversations.setTopic", params=kwargs)
+
+ +
+
+def conversations_unarchive(self, *, channel: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def conversations_unarchive(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Reverses conversation archival.
+    https://docs.slack.dev/reference/methods/conversations.unarchive
+    """
+    kwargs.update({"channel": channel})
+    return self.api_call("conversations.unarchive", params=kwargs)
+
+ +
+
+def dialog_open(self, *, dialog: Dict[str, Any], trigger_id: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def dialog_open(
+    self,
+    *,
+    dialog: Dict[str, Any],
+    trigger_id: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Open a dialog with a user.
+    https://docs.slack.dev/reference/methods/dialog.open
+    """
+    kwargs.update({"dialog": dialog, "trigger_id": trigger_id})
+    kwargs = _remove_none_values(kwargs)
+    # NOTE: As the dialog can be a dict, this API call works only with json format.
+    return self.api_call("dialog.open", json=kwargs)
+
+ +
+
+def dnd_endDnd(self, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def dnd_endDnd(
+    self,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Ends the current user's Do Not Disturb session immediately.
+    https://docs.slack.dev/reference/methods/dnd.endDnd
+    """
+    return self.api_call("dnd.endDnd", params=kwargs)
+
+

Ends the current user's Do Not Disturb session immediately. +https://docs.slack.dev/reference/methods/dnd.endDnd

+
+
+def dnd_endSnooze(self, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def dnd_endSnooze(
+    self,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Ends the current user's snooze mode immediately.
+    https://docs.slack.dev/reference/methods/dnd.endSnooze
+    """
+    return self.api_call("dnd.endSnooze", params=kwargs)
+
+

Ends the current user's snooze mode immediately. +https://docs.slack.dev/reference/methods/dnd.endSnooze

+
+
+def dnd_info(self, *, team_id: str | None = None, user: str | None = None, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def dnd_info(
+    self,
+    *,
+    team_id: Optional[str] = None,
+    user: Optional[str] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Retrieves a user's current Do Not Disturb status.
+    https://docs.slack.dev/reference/methods/dnd.info
+    """
+    kwargs.update({"team_id": team_id, "user": user})
+    return self.api_call("dnd.info", http_verb="GET", params=kwargs)
+
+

Retrieves a user's current Do Not Disturb status. +https://docs.slack.dev/reference/methods/dnd.info

+
+
+def dnd_setSnooze(self, *, num_minutes: str | int, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def dnd_setSnooze(
+    self,
+    *,
+    num_minutes: Union[int, str],
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Turns on Do Not Disturb mode for the current user, or changes its duration.
+    https://docs.slack.dev/reference/methods/dnd.setSnooze
+    """
+    kwargs.update({"num_minutes": num_minutes})
+    return self.api_call("dnd.setSnooze", http_verb="GET", params=kwargs)
+
+

Turns on Do Not Disturb mode for the current user, or changes its duration. +https://docs.slack.dev/reference/methods/dnd.setSnooze

+
+
+def dnd_teamInfo(self, users: str | Sequence[str], team_id: str | None = None, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def dnd_teamInfo(
+    self,
+    users: Union[str, Sequence[str]],
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Retrieves the Do Not Disturb status for users on a team.
+    https://docs.slack.dev/reference/methods/dnd.teamInfo
+    """
+    if isinstance(users, (list, tuple)):
+        kwargs.update({"users": ",".join(users)})
+    else:
+        kwargs.update({"users": users})
+    kwargs.update({"team_id": team_id})
+    return self.api_call("dnd.teamInfo", http_verb="GET", params=kwargs)
+
+

Retrieves the Do Not Disturb status for users on a team. +https://docs.slack.dev/reference/methods/dnd.teamInfo

+
+
+def emoji_list(self, include_categories: bool | None = None, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def emoji_list(
+    self,
+    include_categories: Optional[bool] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Lists custom emoji for a team.
+    https://docs.slack.dev/reference/methods/emoji.list
+    """
+    kwargs.update({"include_categories": include_categories})
+    return self.api_call("emoji.list", http_verb="GET", params=kwargs)
+
+ +
+
+def entity_presentDetails(self,
trigger_id: str,
metadata: Dict | EntityMetadata | None = None,
user_auth_required: bool | None = None,
user_auth_url: str | None = None,
error: Dict[str, Any] | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def entity_presentDetails(
+    self,
+    trigger_id: str,
+    metadata: Optional[Union[Dict, EntityMetadata]] = None,
+    user_auth_required: Optional[bool] = None,
+    user_auth_url: Optional[str] = None,
+    error: Optional[Dict[str, Any]] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Provides entity details for the flexpane.
+    https://docs.slack.dev/reference/methods/entity.presentDetails/
+    """
+    kwargs.update({"trigger_id": trigger_id})
+    if metadata is not None:
+        kwargs.update({"metadata": metadata})
+    if user_auth_required is not None:
+        kwargs.update({"user_auth_required": user_auth_required})
+    if user_auth_url is not None:
+        kwargs.update({"user_auth_url": user_auth_url})
+    if error is not None:
+        kwargs.update({"error": error})
+    _parse_web_class_objects(kwargs)
+    return self.api_call("entity.presentDetails", json=kwargs)
+
+

Provides entity details for the flexpane. +https://docs.slack.dev/reference/methods/entity.presentDetails/

+
+
+def files_comments_delete(self, *, file: str, id: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def files_comments_delete(
+    self,
+    *,
+    file: str,
+    id: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Deletes an existing comment on a file.
+    https://docs.slack.dev/reference/methods/files.comments.delete
+    """
+    kwargs.update({"file": file, "id": id})
+    return self.api_call("files.comments.delete", params=kwargs)
+
+ +
+
+def files_completeUploadExternal(self,
*,
files: List[Dict[str, str]],
channel_id: str | None = None,
channels: List[str] | None = None,
initial_comment: str | None = None,
thread_ts: str | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def files_completeUploadExternal(
+    self,
+    *,
+    files: List[Dict[str, str]],
+    channel_id: Optional[str] = None,
+    channels: Optional[List[str]] = None,
+    initial_comment: Optional[str] = None,
+    thread_ts: Optional[str] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Finishes an upload started with files.getUploadURLExternal.
+    https://docs.slack.dev/reference/methods/files.completeUploadExternal
+    """
+    _files = [{k: v for k, v in f.items() if v is not None} for f in files]
+    kwargs.update(
+        {
+            "files": json.dumps(_files),
+            "channel_id": channel_id,
+            "initial_comment": initial_comment,
+            "thread_ts": thread_ts,
+        }
+    )
+    if channels:
+        kwargs["channels"] = ",".join(channels)
+    return self.api_call("files.completeUploadExternal", params=kwargs)
+
+

Finishes an upload started with files.getUploadURLExternal. +https://docs.slack.dev/reference/methods/files.completeUploadExternal

+
+
+def files_delete(self, *, file: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def files_delete(
+    self,
+    *,
+    file: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Deletes a file.
+    https://docs.slack.dev/reference/methods/files.delete
+    """
+    kwargs.update({"file": file})
+    return self.api_call("files.delete", params=kwargs)
+
+ +
+
+def files_getUploadURLExternal(self,
*,
filename: str,
length: int,
alt_txt: str | None = None,
snippet_type: str | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def files_getUploadURLExternal(
+    self,
+    *,
+    filename: str,
+    length: int,
+    alt_txt: Optional[str] = None,
+    snippet_type: Optional[str] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Gets a URL for an edge external upload.
+    https://docs.slack.dev/reference/methods/files.getUploadURLExternal
+    """
+    kwargs.update(
+        {
+            "filename": filename,
+            "length": length,
+            "alt_txt": alt_txt,
+            "snippet_type": snippet_type,
+        }
+    )
+    return self.api_call("files.getUploadURLExternal", params=kwargs)
+
+ +
+
+def files_info(self,
*,
file: str,
count: int | None = None,
cursor: str | None = None,
limit: int | None = None,
page: int | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def files_info(
+    self,
+    *,
+    file: str,
+    count: Optional[int] = None,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    page: Optional[int] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Gets information about a team file.
+    https://docs.slack.dev/reference/methods/files.info
+    """
+    kwargs.update(
+        {
+            "file": file,
+            "count": count,
+            "cursor": cursor,
+            "limit": limit,
+            "page": page,
+        }
+    )
+    return self.api_call("files.info", http_verb="GET", params=kwargs)
+
+

Gets information about a team file. +https://docs.slack.dev/reference/methods/files.info

+
+
+def files_list(self,
*,
channel: str | None = None,
count: int | None = None,
page: int | None = None,
show_files_hidden_by_limit: bool | None = None,
team_id: str | None = None,
ts_from: str | None = None,
ts_to: str | None = None,
types: str | Sequence[str] | None = None,
user: str | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def files_list(
+    self,
+    *,
+    channel: Optional[str] = None,
+    count: Optional[int] = None,
+    page: Optional[int] = None,
+    show_files_hidden_by_limit: Optional[bool] = None,
+    team_id: Optional[str] = None,
+    ts_from: Optional[str] = None,
+    ts_to: Optional[str] = None,
+    types: Optional[Union[str, Sequence[str]]] = None,
+    user: Optional[str] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Lists & filters team files.
+    https://docs.slack.dev/reference/methods/files.list
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "count": count,
+            "page": page,
+            "show_files_hidden_by_limit": show_files_hidden_by_limit,
+            "team_id": team_id,
+            "ts_from": ts_from,
+            "ts_to": ts_to,
+            "user": user,
+        }
+    )
+    if isinstance(types, (list, tuple)):
+        kwargs.update({"types": ",".join(types)})
+    else:
+        kwargs.update({"types": types})
+    return self.api_call("files.list", http_verb="GET", params=kwargs)
+
+ +
+
+def files_remote_add(self,
*,
external_id: str,
external_url: str,
title: str,
filetype: str | None = None,
indexable_file_contents: str | bytes | io.IOBase | None = None,
preview_image: str | bytes | io.IOBase | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def files_remote_add(
+    self,
+    *,
+    external_id: str,
+    external_url: str,
+    title: str,
+    filetype: Optional[str] = None,
+    indexable_file_contents: Optional[Union[str, bytes, IOBase]] = None,
+    preview_image: Optional[Union[str, bytes, IOBase]] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Adds a file from a remote service.
+    https://docs.slack.dev/reference/methods/files.remote.add
+    """
+    kwargs.update(
+        {
+            "external_id": external_id,
+            "external_url": external_url,
+            "title": title,
+            "filetype": filetype,
+        }
+    )
+    files = None
+    # preview_image (file): Preview of the document via multipart/form-data.
+    if preview_image is not None or indexable_file_contents is not None:
+        files = {
+            "preview_image": preview_image,
+            "indexable_file_contents": indexable_file_contents,
+        }
+
+    return self.api_call(
+        # Intentionally using "POST" method over "GET" here
+        "files.remote.add",
+        http_verb="POST",
+        data=kwargs,
+        files=files,
+    )
+
+ +
+
+def files_remote_info(self, *, external_id: str | None = None, file: str | None = None, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def files_remote_info(
+    self,
+    *,
+    external_id: Optional[str] = None,
+    file: Optional[str] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Retrieve information about a remote file added to Slack.
+    https://docs.slack.dev/reference/methods/files.remote.info
+    """
+    kwargs.update({"external_id": external_id, "file": file})
+    return self.api_call("files.remote.info", http_verb="GET", params=kwargs)
+
+

Retrieve information about a remote file added to Slack. +https://docs.slack.dev/reference/methods/files.remote.info

+
+
+def files_remote_list(self,
*,
channel: str | None = None,
cursor: str | None = None,
limit: int | None = None,
ts_from: str | None = None,
ts_to: str | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def files_remote_list(
+    self,
+    *,
+    channel: Optional[str] = None,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    ts_from: Optional[str] = None,
+    ts_to: Optional[str] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Retrieve information about a remote file added to Slack.
+    https://docs.slack.dev/reference/methods/files.remote.list
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "cursor": cursor,
+            "limit": limit,
+            "ts_from": ts_from,
+            "ts_to": ts_to,
+        }
+    )
+    return self.api_call("files.remote.list", http_verb="GET", params=kwargs)
+
+

Retrieve information about a remote file added to Slack. +https://docs.slack.dev/reference/methods/files.remote.list

+
+
+def files_remote_remove(self, *, external_id: str | None = None, file: str | None = None, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def files_remote_remove(
+    self,
+    *,
+    external_id: Optional[str] = None,
+    file: Optional[str] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Remove a remote file.
+    https://docs.slack.dev/reference/methods/files.remote.remove
+    """
+    kwargs.update({"external_id": external_id, "file": file})
+    return self.api_call("files.remote.remove", http_verb="POST", params=kwargs)
+
+ +
+
+def files_remote_share(self,
*,
channels: str | Sequence[str],
external_id: str | None = None,
file: str | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def files_remote_share(
+    self,
+    *,
+    channels: Union[str, Sequence[str]],
+    external_id: Optional[str] = None,
+    file: Optional[str] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Share a remote file into a channel.
+    https://docs.slack.dev/reference/methods/files.remote.share
+    """
+    if external_id is None and file is None:
+        raise e.SlackRequestError("Either external_id or file must be provided.")
+    if isinstance(channels, (list, tuple)):
+        kwargs.update({"channels": ",".join(channels)})
+    else:
+        kwargs.update({"channels": channels})
+    kwargs.update({"external_id": external_id, "file": file})
+    return self.api_call("files.remote.share", http_verb="GET", params=kwargs)
+
+ +
+
+def files_remote_update(self,
*,
external_id: str | None = None,
external_url: str | None = None,
file: str | None = None,
title: str | None = None,
filetype: str | None = None,
indexable_file_contents: str | None = None,
preview_image: str | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def files_remote_update(
+    self,
+    *,
+    external_id: Optional[str] = None,
+    external_url: Optional[str] = None,
+    file: Optional[str] = None,
+    title: Optional[str] = None,
+    filetype: Optional[str] = None,
+    indexable_file_contents: Optional[str] = None,
+    preview_image: Optional[str] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Updates an existing remote file.
+    https://docs.slack.dev/reference/methods/files.remote.update
+    """
+    kwargs.update(
+        {
+            "external_id": external_id,
+            "external_url": external_url,
+            "file": file,
+            "title": title,
+            "filetype": filetype,
+        }
+    )
+    files = None
+    # preview_image (file): Preview of the document via multipart/form-data.
+    if preview_image is not None or indexable_file_contents is not None:
+        files = {
+            "preview_image": preview_image,
+            "indexable_file_contents": indexable_file_contents,
+        }
+
+    return self.api_call(
+        # Intentionally using "POST" method over "GET" here
+        "files.remote.update",
+        http_verb="POST",
+        data=kwargs,
+        files=files,
+    )
+
+ +
+
+def files_revokePublicURL(self, *, file: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def files_revokePublicURL(
+    self,
+    *,
+    file: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Revokes public/external sharing access for a file
+    https://docs.slack.dev/reference/methods/files.revokePublicURL
+    """
+    kwargs.update({"file": file})
+    return self.api_call("files.revokePublicURL", params=kwargs)
+
+

Revokes public/external sharing access for a file +https://docs.slack.dev/reference/methods/files.revokePublicURL

+
+
+def files_sharedPublicURL(self, *, file: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def files_sharedPublicURL(
+    self,
+    *,
+    file: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Enables a file for public/external sharing.
+    https://docs.slack.dev/reference/methods/files.sharedPublicURL
+    """
+    kwargs.update({"file": file})
+    return self.api_call("files.sharedPublicURL", params=kwargs)
+
+

Enables a file for public/external sharing. +https://docs.slack.dev/reference/methods/files.sharedPublicURL

+
+
+def files_upload(self,
*,
file: str | bytes | io.IOBase | None = None,
content: str | bytes | None = None,
filename: str | None = None,
filetype: str | None = None,
initial_comment: str | None = None,
thread_ts: str | None = None,
title: str | None = None,
channels: str | Sequence[str] | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def files_upload(
+    self,
+    *,
+    file: Optional[Union[str, bytes, IOBase]] = None,
+    content: Optional[Union[str, bytes]] = None,
+    filename: Optional[str] = None,
+    filetype: Optional[str] = None,
+    initial_comment: Optional[str] = None,
+    thread_ts: Optional[str] = None,
+    title: Optional[str] = None,
+    channels: Optional[Union[str, Sequence[str]]] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Uploads or creates a file.
+    https://docs.slack.dev/reference/methods/files.upload
+    """
+    _print_files_upload_v2_suggestion()
+
+    if file is None and content is None:
+        raise e.SlackRequestError("The file or content argument must be specified.")
+    if file is not None and content is not None:
+        raise e.SlackRequestError("You cannot specify both the file and the content argument.")
+
+    if isinstance(channels, (list, tuple)):
+        kwargs.update({"channels": ",".join(channels)})
+    else:
+        kwargs.update({"channels": channels})
+    kwargs.update(
+        {
+            "filename": filename,
+            "filetype": filetype,
+            "initial_comment": initial_comment,
+            "thread_ts": thread_ts,
+            "title": title,
+        }
+    )
+    if file:
+        if kwargs.get("filename") is None and isinstance(file, str):
+            # use the local filename if filename is missing
+            if kwargs.get("filename") is None:
+                kwargs["filename"] = file.split(os.path.sep)[-1]
+        return self.api_call("files.upload", files={"file": file}, data=kwargs)
+    else:
+        kwargs["content"] = content
+        return self.api_call("files.upload", data=kwargs)
+
+ +
+
+def files_upload_v2(self,
*,
filename: str | None = None,
file: str | bytes | io.IOBase | os.PathLike | None = None,
content: str | bytes | None = None,
title: str | None = None,
alt_txt: str | None = None,
snippet_type: str | None = None,
file_uploads: List[Dict[str, Any]] | None = None,
channel: str | None = None,
channels: List[str] | None = None,
initial_comment: str | None = None,
thread_ts: str | None = None,
request_file_info: bool = True,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def files_upload_v2(
+    self,
+    *,
+    # for sending a single file
+    filename: Optional[str] = None,  # you can skip this only when sending along with content parameter
+    file: Optional[Union[str, bytes, IOBase, os.PathLike]] = None,
+    content: Optional[Union[str, bytes]] = None,
+    title: Optional[str] = None,
+    alt_txt: Optional[str] = None,
+    snippet_type: Optional[str] = None,
+    # To upload multiple files at a time
+    file_uploads: Optional[List[Dict[str, Any]]] = None,
+    channel: Optional[str] = None,
+    channels: Optional[List[str]] = None,
+    initial_comment: Optional[str] = None,
+    thread_ts: Optional[str] = None,
+    request_file_info: bool = True,  # since v3.23, this flag is no longer necessary
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """This wrapper method provides an easy way to upload files using the following endpoints:
+
+    - step1: https://docs.slack.dev/reference/methods/files.getUploadURLExternal
+
+    - step2: "https://files.slack.com/upload/v1/..." URLs returned from files.getUploadURLExternal API
+
+    - step3: https://docs.slack.dev/reference/methods/files.completeUploadExternal
+        and https://docs.slack.dev/reference/methods/files.info
+
+    """
+    if file is None and content is None and file_uploads is None:
+        raise e.SlackRequestError("Any of file, content, and file_uploads must be specified.")
+    if file is not None and content is not None:
+        raise e.SlackRequestError("You cannot specify both the file and the content argument.")
+
+    # deprecated arguments:
+    filetype = kwargs.get("filetype")
+
+    if filetype is not None:
+        warnings.warn("The filetype parameter is no longer supported. Please remove it from the arguments.")
+
+    # step1: files.getUploadURLExternal per file
+    files: List[Dict[str, Any]] = []
+    if file_uploads is not None:
+        for f in file_uploads:
+            files.append(_to_v2_file_upload_item(f))
+    else:
+        f = _to_v2_file_upload_item(
+            {
+                "filename": filename,
+                "file": file,
+                "content": content,
+                "title": title,
+                "alt_txt": alt_txt,
+                "snippet_type": snippet_type,
+            }
+        )
+        files.append(f)
+
+    for f in files:
+        url_response = self.files_getUploadURLExternal(
+            filename=f.get("filename"),  # type: ignore[arg-type]
+            length=f.get("length"),  # type: ignore[arg-type]
+            alt_txt=f.get("alt_txt"),
+            snippet_type=f.get("snippet_type"),
+            token=kwargs.get("token"),
+        )
+        _validate_for_legacy_client(url_response)
+        f["file_id"] = url_response.get("file_id")  # type: ignore[union-attr, unused-ignore]
+        f["upload_url"] = url_response.get("upload_url")  # type: ignore[union-attr, unused-ignore]
+
+    # step2: "https://files.slack.com/upload/v1/..." per file
+    for f in files:
+        upload_result = self._upload_file(
+            url=f["upload_url"],
+            data=f["data"],
+            logger=self._logger,
+            timeout=self.timeout,
+            proxy=self.proxy,
+            ssl=self.ssl,
+        )
+        if upload_result.status != 200:
+            status = upload_result.status
+            body = upload_result.body
+            message = (
+                "Failed to upload a file "
+                f"(status: {status}, body: {body}, filename: {f.get('filename')}, title: {f.get('title')})"
+            )
+            raise e.SlackRequestError(message)
+
+    # step3: files.completeUploadExternal with all the sets of (file_id + title)
+    completion = self.files_completeUploadExternal(
+        files=[{"id": f["file_id"], "title": f["title"]} for f in files],
+        channel_id=channel,
+        channels=channels,
+        initial_comment=initial_comment,
+        thread_ts=thread_ts,
+        **kwargs,
+    )
+    if len(completion.get("files")) == 1:  # type: ignore[arg-type, union-attr, unused-ignore]
+        completion.data["file"] = completion.get("files")[0]  # type: ignore[index, union-attr, unused-ignore]
+    return completion
+
+

This wrapper method provides an easy way to upload files using the following endpoints:

+
+
+
+def functions_completeError(self, *, function_execution_id: str, error: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def functions_completeError(
+    self,
+    *,
+    function_execution_id: str,
+    error: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Signal the failure to execute a function
+    https://docs.slack.dev/reference/methods/functions.completeError
+    """
+    kwargs.update({"function_execution_id": function_execution_id, "error": error})
+    return self.api_call("functions.completeError", params=kwargs)
+
+ +
+
+def functions_completeSuccess(self, *, function_execution_id: str, outputs: Dict[str, Any], **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def functions_completeSuccess(
+    self,
+    *,
+    function_execution_id: str,
+    outputs: Dict[str, Any],
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Signal the successful completion of a function
+    https://docs.slack.dev/reference/methods/functions.completeSuccess
+    """
+    kwargs.update({"function_execution_id": function_execution_id, "outputs": json.dumps(outputs)})
+    return self.api_call("functions.completeSuccess", params=kwargs)
+
+

Signal the successful completion of a function +https://docs.slack.dev/reference/methods/functions.completeSuccess

+
+
+def groups_archive(self, *, channel: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def groups_archive(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Archives a private channel."""
+    kwargs.update({"channel": channel})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("groups.archive", json=kwargs)
+
+

Archives a private channel.

+
+
+def groups_create(self, *, name: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def groups_create(
+    self,
+    *,
+    name: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Creates a private channel."""
+    kwargs.update({"name": name})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("groups.create", json=kwargs)
+
+

Creates a private channel.

+
+
+def groups_createChild(self, *, channel: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def groups_createChild(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Clones and archives a private channel."""
+    kwargs.update({"channel": channel})
+    return self.api_call("groups.createChild", http_verb="GET", params=kwargs)
+
+

Clones and archives a private channel.

+
+
+def groups_history(self, *, channel: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def groups_history(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Fetches history of messages and events from a private channel."""
+    kwargs.update({"channel": channel})
+    return self.api_call("groups.history", http_verb="GET", params=kwargs)
+
+

Fetches history of messages and events from a private channel.

+
+
+def groups_info(self, *, channel: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def groups_info(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Gets information about a private channel."""
+    kwargs.update({"channel": channel})
+    return self.api_call("groups.info", http_verb="GET", params=kwargs)
+
+

Gets information about a private channel.

+
+
+def groups_invite(self, *, channel: str, user: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def groups_invite(
+    self,
+    *,
+    channel: str,
+    user: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Invites a user to a private channel."""
+    kwargs.update({"channel": channel, "user": user})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("groups.invite", json=kwargs)
+
+

Invites a user to a private channel.

+
+
+def groups_kick(self, *, channel: str, user: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def groups_kick(
+    self,
+    *,
+    channel: str,
+    user: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Removes a user from a private channel."""
+    kwargs.update({"channel": channel, "user": user})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("groups.kick", json=kwargs)
+
+

Removes a user from a private channel.

+
+
+def groups_leave(self, *, channel: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def groups_leave(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Leaves a private channel."""
+    kwargs.update({"channel": channel})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("groups.leave", json=kwargs)
+
+

Leaves a private channel.

+
+
+def groups_list(self, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def groups_list(
+    self,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Lists private channels that the calling user has access to."""
+    return self.api_call("groups.list", http_verb="GET", params=kwargs)
+
+

Lists private channels that the calling user has access to.

+
+
+def groups_mark(self, *, channel: str, ts: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def groups_mark(
+    self,
+    *,
+    channel: str,
+    ts: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Sets the read cursor in a private channel."""
+    kwargs.update({"channel": channel, "ts": ts})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("groups.mark", json=kwargs)
+
+

Sets the read cursor in a private channel.

+
+
+def groups_open(self, *, channel: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def groups_open(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Opens a private channel."""
+    kwargs.update({"channel": channel})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("groups.open", json=kwargs)
+
+

Opens a private channel.

+
+
+def groups_rename(self, *, channel: str, name: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def groups_rename(
+    self,
+    *,
+    channel: str,
+    name: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Renames a private channel."""
+    kwargs.update({"channel": channel, "name": name})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("groups.rename", json=kwargs)
+
+

Renames a private channel.

+
+
+def groups_replies(self, *, channel: str, thread_ts: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def groups_replies(
+    self,
+    *,
+    channel: str,
+    thread_ts: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Retrieve a thread of messages posted to a private channel"""
+    kwargs.update({"channel": channel, "thread_ts": thread_ts})
+    return self.api_call("groups.replies", http_verb="GET", params=kwargs)
+
+

Retrieve a thread of messages posted to a private channel

+
+
+def groups_setPurpose(self, *, channel: str, purpose: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def groups_setPurpose(
+    self,
+    *,
+    channel: str,
+    purpose: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Sets the purpose for a private channel."""
+    kwargs.update({"channel": channel, "purpose": purpose})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("groups.setPurpose", json=kwargs)
+
+

Sets the purpose for a private channel.

+
+
+def groups_setTopic(self, *, channel: str, topic: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def groups_setTopic(
+    self,
+    *,
+    channel: str,
+    topic: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Sets the topic for a private channel."""
+    kwargs.update({"channel": channel, "topic": topic})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("groups.setTopic", json=kwargs)
+
+

Sets the topic for a private channel.

+
+
+def groups_unarchive(self, *, channel: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def groups_unarchive(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Unarchives a private channel."""
+    kwargs.update({"channel": channel})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("groups.unarchive", json=kwargs)
+
+

Unarchives a private channel.

+
+
+def im_close(self, *, channel: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def im_close(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Close a direct message channel."""
+    kwargs.update({"channel": channel})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("im.close", json=kwargs)
+
+

Close a direct message channel.

+
+
+def im_history(self, *, channel: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def im_history(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Fetches history of messages and events from direct message channel."""
+    kwargs.update({"channel": channel})
+    return self.api_call("im.history", http_verb="GET", params=kwargs)
+
+

Fetches history of messages and events from direct message channel.

+
+
+def im_list(self, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def im_list(
+    self,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Lists direct message channels for the calling user."""
+    return self.api_call("im.list", http_verb="GET", params=kwargs)
+
+

Lists direct message channels for the calling user.

+
+
+def im_mark(self, *, channel: str, ts: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def im_mark(
+    self,
+    *,
+    channel: str,
+    ts: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Sets the read cursor in a direct message channel."""
+    kwargs.update({"channel": channel, "ts": ts})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("im.mark", json=kwargs)
+
+

Sets the read cursor in a direct message channel.

+
+
+def im_open(self, *, user: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def im_open(
+    self,
+    *,
+    user: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Opens a direct message channel."""
+    kwargs.update({"user": user})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("im.open", json=kwargs)
+
+

Opens a direct message channel.

+
+
+def im_replies(self, *, channel: str, thread_ts: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def im_replies(
+    self,
+    *,
+    channel: str,
+    thread_ts: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Retrieve a thread of messages posted to a direct message conversation"""
+    kwargs.update({"channel": channel, "thread_ts": thread_ts})
+    return self.api_call("im.replies", http_verb="GET", params=kwargs)
+
+

Retrieve a thread of messages posted to a direct message conversation

+
+
+def migration_exchange(self,
*,
users: str | Sequence[str],
team_id: str | None = None,
to_old: bool | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def migration_exchange(
+    self,
+    *,
+    users: Union[str, Sequence[str]],
+    team_id: Optional[str] = None,
+    to_old: Optional[bool] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """For Enterprise Grid workspaces, map local user IDs to global user IDs
+    https://docs.slack.dev/reference/methods/migration.exchange
+    """
+    if isinstance(users, (list, tuple)):
+        kwargs.update({"users": ",".join(users)})
+    else:
+        kwargs.update({"users": users})
+    kwargs.update({"team_id": team_id, "to_old": to_old})
+    return self.api_call("migration.exchange", http_verb="GET", params=kwargs)
+
+

For Enterprise Grid workspaces, map local user IDs to global user IDs +https://docs.slack.dev/reference/methods/migration.exchange

+
+
+def mpim_close(self, *, channel: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def mpim_close(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Closes a multiparty direct message channel."""
+    kwargs.update({"channel": channel})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("mpim.close", json=kwargs)
+
+

Closes a multiparty direct message channel.

+
+
+def mpim_history(self, *, channel: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def mpim_history(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Fetches history of messages and events from a multiparty direct message."""
+    kwargs.update({"channel": channel})
+    return self.api_call("mpim.history", http_verb="GET", params=kwargs)
+
+

Fetches history of messages and events from a multiparty direct message.

+
+
+def mpim_list(self, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def mpim_list(
+    self,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Lists multiparty direct message channels for the calling user."""
+    return self.api_call("mpim.list", http_verb="GET", params=kwargs)
+
+

Lists multiparty direct message channels for the calling user.

+
+
+def mpim_mark(self, *, channel: str, ts: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def mpim_mark(
+    self,
+    *,
+    channel: str,
+    ts: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Sets the read cursor in a multiparty direct message channel."""
+    kwargs.update({"channel": channel, "ts": ts})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("mpim.mark", json=kwargs)
+
+

Sets the read cursor in a multiparty direct message channel.

+
+
+def mpim_open(self, *, users: str | Sequence[str], **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def mpim_open(
+    self,
+    *,
+    users: Union[str, Sequence[str]],
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """This method opens a multiparty direct message."""
+    if isinstance(users, (list, tuple)):
+        kwargs.update({"users": ",".join(users)})
+    else:
+        kwargs.update({"users": users})
+    return self.api_call("mpim.open", params=kwargs)
+
+

This method opens a multiparty direct message.

+
+
+def mpim_replies(self, *, channel: str, thread_ts: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def mpim_replies(
+    self,
+    *,
+    channel: str,
+    thread_ts: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Retrieve a thread of messages posted to a direct message conversation from a
+    multiparty direct message.
+    """
+    kwargs.update({"channel": channel, "thread_ts": thread_ts})
+    return self.api_call("mpim.replies", http_verb="GET", params=kwargs)
+
+

Retrieve a thread of messages posted to a direct message conversation from a +multiparty direct message.

+
+
+def oauth_access(self,
*,
client_id: str,
client_secret: str,
code: str,
redirect_uri: str | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def oauth_access(
+    self,
+    *,
+    client_id: str,
+    client_secret: str,
+    code: str,
+    redirect_uri: Optional[str] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Exchanges a temporary OAuth verifier code for an access token.
+    https://docs.slack.dev/reference/methods/oauth.access
+    """
+    if redirect_uri is not None:
+        kwargs.update({"redirect_uri": redirect_uri})
+    kwargs.update({"code": code})
+    return self.api_call(
+        "oauth.access",
+        data=kwargs,
+        auth={"client_id": client_id, "client_secret": client_secret},
+    )
+
+

Exchanges a temporary OAuth verifier code for an access token. +https://docs.slack.dev/reference/methods/oauth.access

+
+
+def oauth_v2_access(self,
*,
client_id: str,
client_secret: str,
code: str | None = None,
redirect_uri: str | None = None,
grant_type: str | None = None,
refresh_token: str | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def oauth_v2_access(
+    self,
+    *,
+    client_id: str,
+    client_secret: str,
+    # This field is required when processing the OAuth redirect URL requests
+    # while it's absent for token rotation
+    code: Optional[str] = None,
+    redirect_uri: Optional[str] = None,
+    # This field is required for token rotation
+    grant_type: Optional[str] = None,
+    # This field is required for token rotation
+    refresh_token: Optional[str] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Exchanges a temporary OAuth verifier code for an access token.
+    https://docs.slack.dev/reference/methods/oauth.v2.access
+    """
+    if redirect_uri is not None:
+        kwargs.update({"redirect_uri": redirect_uri})
+    if code is not None:
+        kwargs.update({"code": code})
+    if grant_type is not None:
+        kwargs.update({"grant_type": grant_type})
+    if refresh_token is not None:
+        kwargs.update({"refresh_token": refresh_token})
+    return self.api_call(
+        "oauth.v2.access",
+        data=kwargs,
+        auth={"client_id": client_id, "client_secret": client_secret},
+    )
+
+

Exchanges a temporary OAuth verifier code for an access token. +https://docs.slack.dev/reference/methods/oauth.v2.access

+
+
+def oauth_v2_exchange(self, *, token: str, client_id: str, client_secret: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def oauth_v2_exchange(
+    self,
+    *,
+    token: str,
+    client_id: str,
+    client_secret: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Exchanges a legacy access token for a new expiring access token and refresh token
+    https://docs.slack.dev/reference/methods/oauth.v2.exchange
+    """
+    kwargs.update({"client_id": client_id, "client_secret": client_secret, "token": token})
+    return self.api_call("oauth.v2.exchange", params=kwargs)
+
+

Exchanges a legacy access token for a new expiring access token and refresh token +https://docs.slack.dev/reference/methods/oauth.v2.exchange

+
+
+def openid_connect_token(self,
client_id: str,
client_secret: str,
code: str | None = None,
redirect_uri: str | None = None,
grant_type: str | None = None,
refresh_token: str | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def openid_connect_token(
+    self,
+    client_id: str,
+    client_secret: str,
+    code: Optional[str] = None,
+    redirect_uri: Optional[str] = None,
+    grant_type: Optional[str] = None,
+    refresh_token: Optional[str] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Exchanges a temporary OAuth verifier code for an access token for Sign in with Slack.
+    https://docs.slack.dev/reference/methods/openid.connect.token
+    """
+    if redirect_uri is not None:
+        kwargs.update({"redirect_uri": redirect_uri})
+    if code is not None:
+        kwargs.update({"code": code})
+    if grant_type is not None:
+        kwargs.update({"grant_type": grant_type})
+    if refresh_token is not None:
+        kwargs.update({"refresh_token": refresh_token})
+    return self.api_call(
+        "openid.connect.token",
+        data=kwargs,
+        auth={"client_id": client_id, "client_secret": client_secret},
+    )
+
+

Exchanges a temporary OAuth verifier code for an access token for Sign in with Slack. +https://docs.slack.dev/reference/methods/openid.connect.token

+
+
+def openid_connect_userInfo(self, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def openid_connect_userInfo(
+    self,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Get the identity of a user who has authorized Sign in with Slack.
+    https://docs.slack.dev/reference/methods/openid.connect.userInfo
+    """
+    return self.api_call("openid.connect.userInfo", params=kwargs)
+
+

Get the identity of a user who has authorized Sign in with Slack. +https://docs.slack.dev/reference/methods/openid.connect.userInfo

+
+
+def pins_add(self, *, channel: str, timestamp: str | None = None, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def pins_add(
+    self,
+    *,
+    channel: str,
+    timestamp: Optional[str] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Pins an item to a channel.
+    https://docs.slack.dev/reference/methods/pins.add
+    """
+    kwargs.update({"channel": channel, "timestamp": timestamp})
+    return self.api_call("pins.add", params=kwargs)
+
+ +
+
+def pins_list(self, *, channel: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def pins_list(
+    self,
+    *,
+    channel: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Lists items pinned to a channel.
+    https://docs.slack.dev/reference/methods/pins.list
+    """
+    kwargs.update({"channel": channel})
+    return self.api_call("pins.list", http_verb="GET", params=kwargs)
+
+

Lists items pinned to a channel. +https://docs.slack.dev/reference/methods/pins.list

+
+
+def pins_remove(self, *, channel: str, timestamp: str | None = None, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def pins_remove(
+    self,
+    *,
+    channel: str,
+    timestamp: Optional[str] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Un-pins an item from a channel.
+    https://docs.slack.dev/reference/methods/pins.remove
+    """
+    kwargs.update({"channel": channel, "timestamp": timestamp})
+    return self.api_call("pins.remove", params=kwargs)
+
+ +
+
+def reactions_add(self, *, channel: str, name: str, timestamp: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def reactions_add(
+    self,
+    *,
+    channel: str,
+    name: str,
+    timestamp: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Adds a reaction to an item.
+    https://docs.slack.dev/reference/methods/reactions.add
+    """
+    kwargs.update({"channel": channel, "name": name, "timestamp": timestamp})
+    return self.api_call("reactions.add", params=kwargs)
+
+ +
+
+def reactions_get(self,
*,
channel: str | None = None,
file: str | None = None,
file_comment: str | None = None,
full: bool | None = None,
timestamp: str | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def reactions_get(
+    self,
+    *,
+    channel: Optional[str] = None,
+    file: Optional[str] = None,
+    file_comment: Optional[str] = None,
+    full: Optional[bool] = None,
+    timestamp: Optional[str] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Gets reactions for an item.
+    https://docs.slack.dev/reference/methods/reactions.get
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "file": file,
+            "file_comment": file_comment,
+            "full": full,
+            "timestamp": timestamp,
+        }
+    )
+    return self.api_call("reactions.get", http_verb="GET", params=kwargs)
+
+ +
+
+def reactions_list(self,
*,
count: int | None = None,
cursor: str | None = None,
full: bool | None = None,
limit: int | None = None,
page: int | None = None,
team_id: str | None = None,
user: str | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def reactions_list(
+    self,
+    *,
+    count: Optional[int] = None,
+    cursor: Optional[str] = None,
+    full: Optional[bool] = None,
+    limit: Optional[int] = None,
+    page: Optional[int] = None,
+    team_id: Optional[str] = None,
+    user: Optional[str] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Lists reactions made by a user.
+    https://docs.slack.dev/reference/methods/reactions.list
+    """
+    kwargs.update(
+        {
+            "count": count,
+            "cursor": cursor,
+            "full": full,
+            "limit": limit,
+            "page": page,
+            "team_id": team_id,
+            "user": user,
+        }
+    )
+    return self.api_call("reactions.list", http_verb="GET", params=kwargs)
+
+ +
+
+def reactions_remove(self,
*,
name: str,
channel: str | None = None,
file: str | None = None,
file_comment: str | None = None,
timestamp: str | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def reactions_remove(
+    self,
+    *,
+    name: str,
+    channel: Optional[str] = None,
+    file: Optional[str] = None,
+    file_comment: Optional[str] = None,
+    timestamp: Optional[str] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Removes a reaction from an item.
+    https://docs.slack.dev/reference/methods/reactions.remove
+    """
+    kwargs.update(
+        {
+            "name": name,
+            "channel": channel,
+            "file": file,
+            "file_comment": file_comment,
+            "timestamp": timestamp,
+        }
+    )
+    return self.api_call("reactions.remove", params=kwargs)
+
+ +
+
+def reminders_add(self,
*,
text: str,
time: str,
team_id: str | None = None,
user: str | None = None,
recurrence: str | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def reminders_add(
+    self,
+    *,
+    text: str,
+    time: str,
+    team_id: Optional[str] = None,
+    user: Optional[str] = None,
+    recurrence: Optional[str] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Creates a reminder.
+    https://docs.slack.dev/reference/methods/reminders.add
+    """
+    kwargs.update(
+        {
+            "text": text,
+            "time": time,
+            "team_id": team_id,
+            "user": user,
+            "recurrence": recurrence,
+        }
+    )
+    return self.api_call("reminders.add", params=kwargs)
+
+ +
+
+def reminders_complete(self, *, reminder: str, team_id: str | None = None, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def reminders_complete(
+    self,
+    *,
+    reminder: str,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Marks a reminder as complete.
+    https://docs.slack.dev/reference/methods/reminders.complete
+    """
+    kwargs.update({"reminder": reminder, "team_id": team_id})
+    return self.api_call("reminders.complete", params=kwargs)
+
+ +
+
+def reminders_delete(self, *, reminder: str, team_id: str | None = None, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def reminders_delete(
+    self,
+    *,
+    reminder: str,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Deletes a reminder.
+    https://docs.slack.dev/reference/methods/reminders.delete
+    """
+    kwargs.update({"reminder": reminder, "team_id": team_id})
+    return self.api_call("reminders.delete", params=kwargs)
+
+ +
+
+def reminders_info(self, *, reminder: str, team_id: str | None = None, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def reminders_info(
+    self,
+    *,
+    reminder: str,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Gets information about a reminder.
+    https://docs.slack.dev/reference/methods/reminders.info
+    """
+    kwargs.update({"reminder": reminder, "team_id": team_id})
+    return self.api_call("reminders.info", http_verb="GET", params=kwargs)
+
+ +
+
+def reminders_list(self, *, team_id: str | None = None, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def reminders_list(
+    self,
+    *,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Lists all reminders created by or for a given user.
+    https://docs.slack.dev/reference/methods/reminders.list
+    """
+    kwargs.update({"team_id": team_id})
+    return self.api_call("reminders.list", http_verb="GET", params=kwargs)
+
+

Lists all reminders created by or for a given user. +https://docs.slack.dev/reference/methods/reminders.list

+
+
+def rtm_connect(self,
*,
batch_presence_aware: bool | None = None,
presence_sub: bool | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def rtm_connect(
+    self,
+    *,
+    batch_presence_aware: Optional[bool] = None,
+    presence_sub: Optional[bool] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Starts a Real Time Messaging session.
+    https://docs.slack.dev/reference/methods/rtm.connect
+    """
+    kwargs.update({"batch_presence_aware": batch_presence_aware, "presence_sub": presence_sub})
+    return self.api_call("rtm.connect", http_verb="GET", params=kwargs)
+
+

Starts a Real Time Messaging session. +https://docs.slack.dev/reference/methods/rtm.connect

+
+
+def rtm_start(self,
*,
batch_presence_aware: bool | None = None,
include_locale: bool | None = None,
mpim_aware: bool | None = None,
no_latest: bool | None = None,
no_unreads: bool | None = None,
presence_sub: bool | None = None,
simple_latest: bool | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def rtm_start(
+    self,
+    *,
+    batch_presence_aware: Optional[bool] = None,
+    include_locale: Optional[bool] = None,
+    mpim_aware: Optional[bool] = None,
+    no_latest: Optional[bool] = None,
+    no_unreads: Optional[bool] = None,
+    presence_sub: Optional[bool] = None,
+    simple_latest: Optional[bool] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Starts a Real Time Messaging session.
+    https://docs.slack.dev/reference/methods/rtm.start
+    """
+    kwargs.update(
+        {
+            "batch_presence_aware": batch_presence_aware,
+            "include_locale": include_locale,
+            "mpim_aware": mpim_aware,
+            "no_latest": no_latest,
+            "no_unreads": no_unreads,
+            "presence_sub": presence_sub,
+            "simple_latest": simple_latest,
+        }
+    )
+    return self.api_call("rtm.start", http_verb="GET", params=kwargs)
+
+

Starts a Real Time Messaging session. +https://docs.slack.dev/reference/methods/rtm.start

+
+
+def search_all(self,
*,
query: str,
count: int | None = None,
highlight: bool | None = None,
page: int | None = None,
sort: str | None = None,
sort_dir: str | None = None,
team_id: str | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def search_all(
+    self,
+    *,
+    query: str,
+    count: Optional[int] = None,
+    highlight: Optional[bool] = None,
+    page: Optional[int] = None,
+    sort: Optional[str] = None,
+    sort_dir: Optional[str] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Searches for messages and files matching a query.
+    https://docs.slack.dev/reference/methods/search.all
+    """
+    kwargs.update(
+        {
+            "query": query,
+            "count": count,
+            "highlight": highlight,
+            "page": page,
+            "sort": sort,
+            "sort_dir": sort_dir,
+            "team_id": team_id,
+        }
+    )
+    return self.api_call("search.all", http_verb="GET", params=kwargs)
+
+

Searches for messages and files matching a query. +https://docs.slack.dev/reference/methods/search.all

+
+
+def search_files(self,
*,
query: str,
count: int | None = None,
highlight: bool | None = None,
page: int | None = None,
sort: str | None = None,
sort_dir: str | None = None,
team_id: str | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def search_files(
+    self,
+    *,
+    query: str,
+    count: Optional[int] = None,
+    highlight: Optional[bool] = None,
+    page: Optional[int] = None,
+    sort: Optional[str] = None,
+    sort_dir: Optional[str] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Searches for files matching a query.
+    https://docs.slack.dev/reference/methods/search.files
+    """
+    kwargs.update(
+        {
+            "query": query,
+            "count": count,
+            "highlight": highlight,
+            "page": page,
+            "sort": sort,
+            "sort_dir": sort_dir,
+            "team_id": team_id,
+        }
+    )
+    return self.api_call("search.files", http_verb="GET", params=kwargs)
+
+

Searches for files matching a query. +https://docs.slack.dev/reference/methods/search.files

+
+
+def search_messages(self,
*,
query: str,
count: int | None = None,
cursor: str | None = None,
highlight: bool | None = None,
page: int | None = None,
sort: str | None = None,
sort_dir: str | None = None,
team_id: str | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def search_messages(
+    self,
+    *,
+    query: str,
+    count: Optional[int] = None,
+    cursor: Optional[str] = None,
+    highlight: Optional[bool] = None,
+    page: Optional[int] = None,
+    sort: Optional[str] = None,
+    sort_dir: Optional[str] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Searches for messages matching a query.
+    https://docs.slack.dev/reference/methods/search.messages
+    """
+    kwargs.update(
+        {
+            "query": query,
+            "count": count,
+            "cursor": cursor,
+            "highlight": highlight,
+            "page": page,
+            "sort": sort,
+            "sort_dir": sort_dir,
+            "team_id": team_id,
+        }
+    )
+    return self.api_call("search.messages", http_verb="GET", params=kwargs)
+
+

Searches for messages matching a query. +https://docs.slack.dev/reference/methods/search.messages

+
+
+def slackLists_access_delete(self,
*,
list_id: str,
channel_ids: List[str] | None = None,
user_ids: List[str] | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def slackLists_access_delete(
+    self,
+    *,
+    list_id: str,
+    channel_ids: Optional[List[str]] = None,
+    user_ids: Optional[List[str]] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Revoke access to a List for specified entities.
+    https://docs.slack.dev/reference/methods/slackLists.access.delete
+    """
+    kwargs.update({"list_id": list_id, "channel_ids": channel_ids, "user_ids": user_ids})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("slackLists.access.delete", json=kwargs)
+
+

Revoke access to a List for specified entities. +https://docs.slack.dev/reference/methods/slackLists.access.delete

+
+
+def slackLists_access_set(self,
*,
list_id: str,
access_level: str,
channel_ids: List[str] | None = None,
user_ids: List[str] | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def slackLists_access_set(
+    self,
+    *,
+    list_id: str,
+    access_level: str,
+    channel_ids: Optional[List[str]] = None,
+    user_ids: Optional[List[str]] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Set the access level to a List for specified entities.
+    https://docs.slack.dev/reference/methods/slackLists.access.set
+    """
+    kwargs.update({"list_id": list_id, "access_level": access_level, "channel_ids": channel_ids, "user_ids": user_ids})
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("slackLists.access.set", json=kwargs)
+
+

Set the access level to a List for specified entities. +https://docs.slack.dev/reference/methods/slackLists.access.set

+
+
+def slackLists_create(self,
*,
name: str,
description_blocks: str | Sequence[Dict | RichTextBlock] | None = None,
schema: List[Dict[str, Any]] | None = None,
copy_from_list_id: str | None = None,
include_copied_list_records: bool | None = None,
todo_mode: bool | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def slackLists_create(
+    self,
+    *,
+    name: str,
+    description_blocks: Optional[Union[str, Sequence[Union[Dict, RichTextBlock]]]] = None,
+    schema: Optional[List[Dict[str, Any]]] = None,
+    copy_from_list_id: Optional[str] = None,
+    include_copied_list_records: Optional[bool] = None,
+    todo_mode: Optional[bool] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Creates a List.
+    https://docs.slack.dev/reference/methods/slackLists.create
+    """
+    kwargs.update(
+        {
+            "name": name,
+            "description_blocks": description_blocks,
+            "schema": schema,
+            "copy_from_list_id": copy_from_list_id,
+            "include_copied_list_records": include_copied_list_records,
+            "todo_mode": todo_mode,
+        }
+    )
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("slackLists.create", json=kwargs)
+
+ +
+
+def slackLists_download_get(self, *, list_id: str, job_id: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def slackLists_download_get(
+    self,
+    *,
+    list_id: str,
+    job_id: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Retrieve List download URL from an export job to download List contents.
+    https://docs.slack.dev/reference/methods/slackLists.download.get
+    """
+    kwargs.update(
+        {
+            "list_id": list_id,
+            "job_id": job_id,
+        }
+    )
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("slackLists.download.get", json=kwargs)
+
+

Retrieve List download URL from an export job to download List contents. +https://docs.slack.dev/reference/methods/slackLists.download.get

+
+
+def slackLists_download_start(self, *, list_id: str, include_archived: bool | None = None, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def slackLists_download_start(
+    self,
+    *,
+    list_id: str,
+    include_archived: Optional[bool] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Initiate a job to export List contents.
+    https://docs.slack.dev/reference/methods/slackLists.download.start
+    """
+    kwargs.update(
+        {
+            "list_id": list_id,
+            "include_archived": include_archived,
+        }
+    )
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("slackLists.download.start", json=kwargs)
+
+ +
+
+def slackLists_items_create(self,
*,
list_id: str,
duplicated_item_id: str | None = None,
parent_item_id: str | None = None,
initial_fields: List[Dict[str, Any]] | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def slackLists_items_create(
+    self,
+    *,
+    list_id: str,
+    duplicated_item_id: Optional[str] = None,
+    parent_item_id: Optional[str] = None,
+    initial_fields: Optional[List[Dict[str, Any]]] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Add a new item to an existing List.
+    https://docs.slack.dev/reference/methods/slackLists.items.create
+    """
+    kwargs.update(
+        {
+            "list_id": list_id,
+            "duplicated_item_id": duplicated_item_id,
+            "parent_item_id": parent_item_id,
+            "initial_fields": initial_fields,
+        }
+    )
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("slackLists.items.create", json=kwargs)
+
+ +
+
+def slackLists_items_delete(self, *, list_id: str, id: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def slackLists_items_delete(
+    self,
+    *,
+    list_id: str,
+    id: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Deletes an item from an existing List.
+    https://docs.slack.dev/reference/methods/slackLists.items.delete
+    """
+    kwargs.update(
+        {
+            "list_id": list_id,
+            "id": id,
+        }
+    )
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("slackLists.items.delete", json=kwargs)
+
+ +
+
+def slackLists_items_deleteMultiple(self, *, list_id: str, ids: List[str], **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def slackLists_items_deleteMultiple(
+    self,
+    *,
+    list_id: str,
+    ids: List[str],
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Deletes multiple items from an existing List.
+    https://docs.slack.dev/reference/methods/slackLists.items.deleteMultiple
+    """
+    kwargs.update(
+        {
+            "list_id": list_id,
+            "ids": ids,
+        }
+    )
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("slackLists.items.deleteMultiple", json=kwargs)
+
+ +
+
+def slackLists_items_info(self, *, list_id: str, id: str, include_is_subscribed: bool | None = None, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def slackLists_items_info(
+    self,
+    *,
+    list_id: str,
+    id: str,
+    include_is_subscribed: Optional[bool] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Get a row from a List.
+    https://docs.slack.dev/reference/methods/slackLists.items.info
+    """
+    kwargs.update(
+        {
+            "list_id": list_id,
+            "id": id,
+            "include_is_subscribed": include_is_subscribed,
+        }
+    )
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("slackLists.items.info", json=kwargs)
+
+ +
+
+def slackLists_items_list(self,
*,
list_id: str,
limit: int | None = None,
cursor: str | None = None,
archived: bool | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def slackLists_items_list(
+    self,
+    *,
+    list_id: str,
+    limit: Optional[int] = None,
+    cursor: Optional[str] = None,
+    archived: Optional[bool] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Get records from a List.
+    https://docs.slack.dev/reference/methods/slackLists.items.list
+    """
+    kwargs.update(
+        {
+            "list_id": list_id,
+            "limit": limit,
+            "cursor": cursor,
+            "archived": archived,
+        }
+    )
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("slackLists.items.list", json=kwargs)
+
+ +
+
+def slackLists_items_update(self, *, list_id: str, cells: List[Dict[str, Any]], **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def slackLists_items_update(
+    self,
+    *,
+    list_id: str,
+    cells: List[Dict[str, Any]],
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Updates cells in a List.
+    https://docs.slack.dev/reference/methods/slackLists.items.update
+    """
+    kwargs.update(
+        {
+            "list_id": list_id,
+            "cells": cells,
+        }
+    )
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("slackLists.items.update", json=kwargs)
+
+ +
+
+def slackLists_update(self,
*,
id: str,
name: str | None = None,
description_blocks: str | Sequence[Dict | RichTextBlock] | None = None,
todo_mode: bool | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def slackLists_update(
+    self,
+    *,
+    id: str,
+    name: Optional[str] = None,
+    description_blocks: Optional[Union[str, Sequence[Union[Dict, RichTextBlock]]]] = None,
+    todo_mode: Optional[bool] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Update a List.
+    https://docs.slack.dev/reference/methods/slackLists.update
+    """
+    kwargs.update(
+        {
+            "id": id,
+            "name": name,
+            "description_blocks": description_blocks,
+            "todo_mode": todo_mode,
+        }
+    )
+    kwargs = _remove_none_values(kwargs)
+    return self.api_call("slackLists.update", json=kwargs)
+
+ +
+
+def stars_add(self,
*,
channel: str | None = None,
file: str | None = None,
file_comment: str | None = None,
timestamp: str | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def stars_add(
+    self,
+    *,
+    channel: Optional[str] = None,
+    file: Optional[str] = None,
+    file_comment: Optional[str] = None,
+    timestamp: Optional[str] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Adds a star to an item.
+    https://docs.slack.dev/reference/methods/stars.add
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "file": file,
+            "file_comment": file_comment,
+            "timestamp": timestamp,
+        }
+    )
+    return self.api_call("stars.add", params=kwargs)
+
+ +
+
+def stars_list(self,
*,
count: int | None = None,
cursor: str | None = None,
limit: int | None = None,
page: int | None = None,
team_id: str | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def stars_list(
+    self,
+    *,
+    count: Optional[int] = None,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    page: Optional[int] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Lists stars for a user.
+    https://docs.slack.dev/reference/methods/stars.list
+    """
+    kwargs.update(
+        {
+            "count": count,
+            "cursor": cursor,
+            "limit": limit,
+            "page": page,
+            "team_id": team_id,
+        }
+    )
+    return self.api_call("stars.list", http_verb="GET", params=kwargs)
+
+ +
+
+def stars_remove(self,
*,
channel: str | None = None,
file: str | None = None,
file_comment: str | None = None,
timestamp: str | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def stars_remove(
+    self,
+    *,
+    channel: Optional[str] = None,
+    file: Optional[str] = None,
+    file_comment: Optional[str] = None,
+    timestamp: Optional[str] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Removes a star from an item.
+    https://docs.slack.dev/reference/methods/stars.remove
+    """
+    kwargs.update(
+        {
+            "channel": channel,
+            "file": file,
+            "file_comment": file_comment,
+            "timestamp": timestamp,
+        }
+    )
+    return self.api_call("stars.remove", params=kwargs)
+
+ +
+
+def team_accessLogs(self,
*,
before: str | int | None = None,
count: str | int | None = None,
page: str | int | None = None,
team_id: str | None = None,
cursor: str | None = None,
limit: int | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def team_accessLogs(
+    self,
+    *,
+    before: Optional[Union[int, str]] = None,
+    count: Optional[Union[int, str]] = None,
+    page: Optional[Union[int, str]] = None,
+    team_id: Optional[str] = None,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Gets the access logs for the current team.
+    https://docs.slack.dev/reference/methods/team.accessLogs
+    """
+    kwargs.update(
+        {
+            "before": before,
+            "count": count,
+            "page": page,
+            "team_id": team_id,
+            "cursor": cursor,
+            "limit": limit,
+        }
+    )
+    return self.api_call("team.accessLogs", http_verb="GET", params=kwargs)
+
+

Gets the access logs for the current team. +https://docs.slack.dev/reference/methods/team.accessLogs

+
+
+def team_billableInfo(self, *, team_id: str | None = None, user: str | None = None, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def team_billableInfo(
+    self,
+    *,
+    team_id: Optional[str] = None,
+    user: Optional[str] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Gets billable users information for the current team.
+    https://docs.slack.dev/reference/methods/team.billableInfo
+    """
+    kwargs.update({"team_id": team_id, "user": user})
+    return self.api_call("team.billableInfo", http_verb="GET", params=kwargs)
+
+

Gets billable users information for the current team. +https://docs.slack.dev/reference/methods/team.billableInfo

+
+
+def team_billing_info(self, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def team_billing_info(
+    self,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Reads a workspace's billing plan information.
+    https://docs.slack.dev/reference/methods/team.billing.info
+    """
+    return self.api_call("team.billing.info", params=kwargs)
+
+

Reads a workspace's billing plan information. +https://docs.slack.dev/reference/methods/team.billing.info

+
+
+def team_externalTeams_disconnect(self, *, target_team: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def team_externalTeams_disconnect(
+    self,
+    *,
+    target_team: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Disconnects an external organization.
+    https://docs.slack.dev/reference/methods/team.externalTeams.disconnect
+    """
+    kwargs.update(
+        {
+            "target_team": target_team,
+        }
+    )
+    return self.api_call("team.externalTeams.disconnect", params=kwargs)
+
+ +
+
+def team_externalTeams_list(self,
*,
connection_status_filter: str | None = None,
slack_connect_pref_filter: Sequence[str] | None = None,
sort_direction: str | None = None,
sort_field: str | None = None,
workspace_filter: Sequence[str] | None = None,
cursor: str | None = None,
limit: int | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def team_externalTeams_list(
+    self,
+    *,
+    connection_status_filter: Optional[str] = None,
+    slack_connect_pref_filter: Optional[Sequence[str]] = None,
+    sort_direction: Optional[str] = None,
+    sort_field: Optional[str] = None,
+    workspace_filter: Optional[Sequence[str]] = None,
+    cursor: Optional[str] = None,
+    limit: Optional[int] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Returns a list of all the external teams connected and details about the connection.
+    https://docs.slack.dev/reference/methods/team.externalTeams.list
+    """
+    kwargs.update(
+        {
+            "connection_status_filter": connection_status_filter,
+            "sort_direction": sort_direction,
+            "sort_field": sort_field,
+            "cursor": cursor,
+            "limit": limit,
+        }
+    )
+    if slack_connect_pref_filter is not None:
+        if isinstance(slack_connect_pref_filter, (list, tuple)):
+            kwargs.update({"slack_connect_pref_filter": ",".join(slack_connect_pref_filter)})
+        else:
+            kwargs.update({"slack_connect_pref_filter": slack_connect_pref_filter})
+    if workspace_filter is not None:
+        if isinstance(workspace_filter, (list, tuple)):
+            kwargs.update({"workspace_filter": ",".join(workspace_filter)})
+        else:
+            kwargs.update({"workspace_filter": workspace_filter})
+    return self.api_call("team.externalTeams.list", http_verb="GET", params=kwargs)
+
+

Returns a list of all the external teams connected and details about the connection. +https://docs.slack.dev/reference/methods/team.externalTeams.list

+
+
+def team_info(self, *, team: str | None = None, domain: str | None = None, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def team_info(
+    self,
+    *,
+    team: Optional[str] = None,
+    domain: Optional[str] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Gets information about the current team.
+    https://docs.slack.dev/reference/methods/team.info
+    """
+    kwargs.update({"team": team, "domain": domain})
+    return self.api_call("team.info", http_verb="GET", params=kwargs)
+
+

Gets information about the current team. +https://docs.slack.dev/reference/methods/team.info

+
+
+def team_integrationLogs(self,
*,
app_id: str | None = None,
change_type: str | None = None,
count: str | int | None = None,
page: str | int | None = None,
service_id: str | None = None,
team_id: str | None = None,
user: str | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def team_integrationLogs(
+    self,
+    *,
+    app_id: Optional[str] = None,
+    change_type: Optional[str] = None,
+    count: Optional[Union[int, str]] = None,
+    page: Optional[Union[int, str]] = None,
+    service_id: Optional[str] = None,
+    team_id: Optional[str] = None,
+    user: Optional[str] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Gets the integration logs for the current team.
+    https://docs.slack.dev/reference/methods/team.integrationLogs
+    """
+    kwargs.update(
+        {
+            "app_id": app_id,
+            "change_type": change_type,
+            "count": count,
+            "page": page,
+            "service_id": service_id,
+            "team_id": team_id,
+            "user": user,
+        }
+    )
+    return self.api_call("team.integrationLogs", http_verb="GET", params=kwargs)
+
+

Gets the integration logs for the current team. +https://docs.slack.dev/reference/methods/team.integrationLogs

+
+
+def team_preferences_list(self, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def team_preferences_list(
+    self,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Retrieve a list of a workspace's team preferences.
+    https://docs.slack.dev/reference/methods/team.preferences.list
+    """
+    return self.api_call("team.preferences.list", params=kwargs)
+
+

Retrieve a list of a workspace's team preferences. +https://docs.slack.dev/reference/methods/team.preferences.list

+
+
+def team_profile_get(self, *, visibility: str | None = None, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def team_profile_get(
+    self,
+    *,
+    visibility: Optional[str] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Retrieve a team's profile.
+    https://docs.slack.dev/reference/methods/team.profile.get
+    """
+    kwargs.update({"visibility": visibility})
+    return self.api_call("team.profile.get", http_verb="GET", params=kwargs)
+
+ +
+
+def tooling_tokens_rotate(self, *, refresh_token: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def tooling_tokens_rotate(
+    self,
+    *,
+    refresh_token: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Exchanges a refresh token for a new app configuration token
+    https://docs.slack.dev/reference/methods/tooling.tokens.rotate
+    """
+    kwargs.update({"refresh_token": refresh_token})
+    return self.api_call("tooling.tokens.rotate", params=kwargs)
+
+

Exchanges a refresh token for a new app configuration token +https://docs.slack.dev/reference/methods/tooling.tokens.rotate

+
+
+def usergroups_create(self,
*,
name: str,
channels: str | Sequence[str] | None = None,
description: str | None = None,
handle: str | None = None,
include_count: bool | None = None,
team_id: str | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def usergroups_create(
+    self,
+    *,
+    name: str,
+    channels: Optional[Union[str, Sequence[str]]] = None,
+    description: Optional[str] = None,
+    handle: Optional[str] = None,
+    include_count: Optional[bool] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Create a User Group
+    https://docs.slack.dev/reference/methods/usergroups.create
+    """
+    kwargs.update(
+        {
+            "name": name,
+            "description": description,
+            "handle": handle,
+            "include_count": include_count,
+            "team_id": team_id,
+        }
+    )
+    if isinstance(channels, (list, tuple)):
+        kwargs.update({"channels": ",".join(channels)})
+    else:
+        kwargs.update({"channels": channels})
+    return self.api_call("usergroups.create", params=kwargs)
+
+ +
+
+def usergroups_disable(self,
*,
usergroup: str,
include_count: bool | None = None,
team_id: str | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def usergroups_disable(
+    self,
+    *,
+    usergroup: str,
+    include_count: Optional[bool] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Disable an existing User Group
+    https://docs.slack.dev/reference/methods/usergroups.disable
+    """
+    kwargs.update({"usergroup": usergroup, "include_count": include_count, "team_id": team_id})
+    return self.api_call("usergroups.disable", params=kwargs)
+
+ +
+
+def usergroups_enable(self,
*,
usergroup: str,
include_count: bool | None = None,
team_id: str | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def usergroups_enable(
+    self,
+    *,
+    usergroup: str,
+    include_count: Optional[bool] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Enable a User Group
+    https://docs.slack.dev/reference/methods/usergroups.enable
+    """
+    kwargs.update({"usergroup": usergroup, "include_count": include_count, "team_id": team_id})
+    return self.api_call("usergroups.enable", params=kwargs)
+
+ +
+
+def usergroups_list(self,
*,
include_count: bool | None = None,
include_disabled: bool | None = None,
include_users: bool | None = None,
team_id: str | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def usergroups_list(
+    self,
+    *,
+    include_count: Optional[bool] = None,
+    include_disabled: Optional[bool] = None,
+    include_users: Optional[bool] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """List all User Groups for a team
+    https://docs.slack.dev/reference/methods/usergroups.list
+    """
+    kwargs.update(
+        {
+            "include_count": include_count,
+            "include_disabled": include_disabled,
+            "include_users": include_users,
+            "team_id": team_id,
+        }
+    )
+    return self.api_call("usergroups.list", http_verb="GET", params=kwargs)
+
+ +
+
+def usergroups_update(self,
*,
usergroup: str,
channels: str | Sequence[str] | None = None,
description: str | None = None,
handle: str | None = None,
include_count: bool | None = None,
name: str | None = None,
team_id: str | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def usergroups_update(
+    self,
+    *,
+    usergroup: str,
+    channels: Optional[Union[str, Sequence[str]]] = None,
+    description: Optional[str] = None,
+    handle: Optional[str] = None,
+    include_count: Optional[bool] = None,
+    name: Optional[str] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Update an existing User Group
+    https://docs.slack.dev/reference/methods/usergroups.update
+    """
+    kwargs.update(
+        {
+            "usergroup": usergroup,
+            "description": description,
+            "handle": handle,
+            "include_count": include_count,
+            "name": name,
+            "team_id": team_id,
+        }
+    )
+    if isinstance(channels, (list, tuple)):
+        kwargs.update({"channels": ",".join(channels)})
+    else:
+        kwargs.update({"channels": channels})
+    return self.api_call("usergroups.update", params=kwargs)
+
+ +
+
+def usergroups_users_list(self,
*,
usergroup: str,
include_disabled: bool | None = None,
team_id: str | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def usergroups_users_list(
+    self,
+    *,
+    usergroup: str,
+    include_disabled: Optional[bool] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """List all users in a User Group
+    https://docs.slack.dev/reference/methods/usergroups.users.list
+    """
+    kwargs.update(
+        {
+            "usergroup": usergroup,
+            "include_disabled": include_disabled,
+            "team_id": team_id,
+        }
+    )
+    return self.api_call("usergroups.users.list", http_verb="GET", params=kwargs)
+
+ +
+
+def usergroups_users_update(self,
*,
usergroup: str,
users: str | Sequence[str],
include_count: bool | None = None,
team_id: str | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def usergroups_users_update(
+    self,
+    *,
+    usergroup: str,
+    users: Union[str, Sequence[str]],
+    include_count: Optional[bool] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Update the list of users for a User Group
+    https://docs.slack.dev/reference/methods/usergroups.users.update
+    """
+    kwargs.update(
+        {
+            "usergroup": usergroup,
+            "include_count": include_count,
+            "team_id": team_id,
+        }
+    )
+    if isinstance(users, (list, tuple)):
+        kwargs.update({"users": ",".join(users)})
+    else:
+        kwargs.update({"users": users})
+    return self.api_call("usergroups.users.update", params=kwargs)
+
+

Update the list of users for a User Group +https://docs.slack.dev/reference/methods/usergroups.users.update

+
+
+def users_conversations(self,
*,
cursor: str | None = None,
exclude_archived: bool | None = None,
limit: int | None = None,
team_id: str | None = None,
types: str | Sequence[str] | None = None,
user: str | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def users_conversations(
+    self,
+    *,
+    cursor: Optional[str] = None,
+    exclude_archived: Optional[bool] = None,
+    limit: Optional[int] = None,
+    team_id: Optional[str] = None,
+    types: Optional[Union[str, Sequence[str]]] = None,
+    user: Optional[str] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """List conversations the calling user may access.
+    https://docs.slack.dev/reference/methods/users.conversations
+    """
+    kwargs.update(
+        {
+            "cursor": cursor,
+            "exclude_archived": exclude_archived,
+            "limit": limit,
+            "team_id": team_id,
+            "user": user,
+        }
+    )
+    if isinstance(types, (list, tuple)):
+        kwargs.update({"types": ",".join(types)})
+    else:
+        kwargs.update({"types": types})
+    return self.api_call("users.conversations", http_verb="GET", params=kwargs)
+
+

List conversations the calling user may access. +https://docs.slack.dev/reference/methods/users.conversations

+
+
+def users_deletePhoto(self, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def users_deletePhoto(
+    self,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Delete the user profile photo
+    https://docs.slack.dev/reference/methods/users.deletePhoto
+    """
+    return self.api_call("users.deletePhoto", http_verb="GET", params=kwargs)
+
+ +
+
+def users_discoverableContacts_lookup(self, email: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def users_discoverableContacts_lookup(
+    self,
+    email: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Lookup an email address to see if someone is on Slack
+    https://docs.slack.dev/reference/methods/users.discoverableContacts.lookup
+    """
+    kwargs.update({"email": email})
+    return self.api_call("users.discoverableContacts.lookup", params=kwargs)
+
+

Lookup an email address to see if someone is on Slack +https://docs.slack.dev/reference/methods/users.discoverableContacts.lookup

+
+
+def users_getPresence(self, *, user: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def users_getPresence(
+    self,
+    *,
+    user: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Gets user presence information.
+    https://docs.slack.dev/reference/methods/users.getPresence
+    """
+    kwargs.update({"user": user})
+    return self.api_call("users.getPresence", http_verb="GET", params=kwargs)
+
+ +
+
+def users_identity(self, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def users_identity(
+    self,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Get a user's identity.
+    https://docs.slack.dev/reference/methods/users.identity
+    """
+    return self.api_call("users.identity", http_verb="GET", params=kwargs)
+
+ +
+
+def users_info(self, *, user: str, include_locale: bool | None = None, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def users_info(
+    self,
+    *,
+    user: str,
+    include_locale: Optional[bool] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Gets information about a user.
+    https://docs.slack.dev/reference/methods/users.info
+    """
+    kwargs.update({"user": user, "include_locale": include_locale})
+    return self.api_call("users.info", http_verb="GET", params=kwargs)
+
+ +
+
+def users_list(self,
*,
cursor: str | None = None,
include_locale: bool | None = None,
limit: int | None = None,
team_id: str | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def users_list(
+    self,
+    *,
+    cursor: Optional[str] = None,
+    include_locale: Optional[bool] = None,
+    limit: Optional[int] = None,
+    team_id: Optional[str] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Lists all users in a Slack team.
+    https://docs.slack.dev/reference/methods/users.list
+    """
+    kwargs.update(
+        {
+            "cursor": cursor,
+            "include_locale": include_locale,
+            "limit": limit,
+            "team_id": team_id,
+        }
+    )
+    return self.api_call("users.list", http_verb="GET", params=kwargs)
+
+

Lists all users in a Slack team. +https://docs.slack.dev/reference/methods/users.list

+
+
+def users_lookupByEmail(self, *, email: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def users_lookupByEmail(
+    self,
+    *,
+    email: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Find a user with an email address.
+    https://docs.slack.dev/reference/methods/users.lookupByEmail
+    """
+    kwargs.update({"email": email})
+    return self.api_call("users.lookupByEmail", http_verb="GET", params=kwargs)
+
+ +
+
+def users_profile_get(self, *, user: str | None = None, include_labels: bool | None = None, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def users_profile_get(
+    self,
+    *,
+    user: Optional[str] = None,
+    include_labels: Optional[bool] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Retrieves a user's profile information.
+    https://docs.slack.dev/reference/methods/users.profile.get
+    """
+    kwargs.update({"user": user, "include_labels": include_labels})
+    return self.api_call("users.profile.get", http_verb="GET", params=kwargs)
+
+

Retrieves a user's profile information. +https://docs.slack.dev/reference/methods/users.profile.get

+
+
+def users_profile_set(self,
*,
name: str | None = None,
value: str | None = None,
user: str | None = None,
profile: Dict | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def users_profile_set(
+    self,
+    *,
+    name: Optional[str] = None,
+    value: Optional[str] = None,
+    user: Optional[str] = None,
+    profile: Optional[Dict] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Set the profile information for a user.
+    https://docs.slack.dev/reference/methods/users.profile.set
+    """
+    kwargs.update(
+        {
+            "name": name,
+            "profile": profile,
+            "user": user,
+            "value": value,
+        }
+    )
+    kwargs = _remove_none_values(kwargs)
+    # NOTE: Intentionally using json for the "profile" parameter
+    return self.api_call("users.profile.set", json=kwargs)
+
+

Set the profile information for a user. +https://docs.slack.dev/reference/methods/users.profile.set

+
+
+def users_setPhoto(self,
*,
image: str | io.IOBase,
crop_w: str | int | None = None,
crop_x: str | int | None = None,
crop_y: str | int | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def users_setPhoto(
+    self,
+    *,
+    image: Union[str, IOBase],
+    crop_w: Optional[Union[int, str]] = None,
+    crop_x: Optional[Union[int, str]] = None,
+    crop_y: Optional[Union[int, str]] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Set the user profile photo
+    https://docs.slack.dev/reference/methods/users.setPhoto
+    """
+    kwargs.update({"crop_w": crop_w, "crop_x": crop_x, "crop_y": crop_y})
+    return self.api_call("users.setPhoto", files={"image": image}, data=kwargs)
+
+ +
+
+def users_setPresence(self, *, presence: str, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def users_setPresence(
+    self,
+    *,
+    presence: str,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Manually sets user presence.
+    https://docs.slack.dev/reference/methods/users.setPresence
+    """
+    kwargs.update({"presence": presence})
+    return self.api_call("users.setPresence", params=kwargs)
+
+ +
+
+def views_open(self,
*,
trigger_id: str | None = None,
interactivity_pointer: str | None = None,
view: dict | View,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def views_open(
+    self,
+    *,
+    trigger_id: Optional[str] = None,
+    interactivity_pointer: Optional[str] = None,
+    view: Union[dict, View],
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Open a view for a user.
+    https://docs.slack.dev/reference/methods/views.open
+    See https://docs.slack.dev/surfaces/modals/ for details.
+    """
+    kwargs.update({"trigger_id": trigger_id, "interactivity_pointer": interactivity_pointer})
+    if isinstance(view, View):
+        kwargs.update({"view": view.to_dict()})
+    else:
+        kwargs.update({"view": view})
+    kwargs = _remove_none_values(kwargs)
+    # NOTE: Intentionally using json for the "view" parameter
+    return self.api_call("views.open", json=kwargs)
+
+ +
+
+def views_publish(self,
*,
user_id: str,
view: dict | View,
hash: str | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def views_publish(
+    self,
+    *,
+    user_id: str,
+    view: Union[dict, View],
+    hash: Optional[str] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Publish a static view for a User.
+    Create or update the view that comprises an
+    app's Home tab (https://docs.slack.dev/surfaces/app-home/)
+    https://docs.slack.dev/reference/methods/views.publish
+    """
+    kwargs.update({"user_id": user_id, "hash": hash})
+    if isinstance(view, View):
+        kwargs.update({"view": view.to_dict()})
+    else:
+        kwargs.update({"view": view})
+    kwargs = _remove_none_values(kwargs)
+    # NOTE: Intentionally using json for the "view" parameter
+    return self.api_call("views.publish", json=kwargs)
+
+

Publish a static view for a User. +Create or update the view that comprises an +app's Home tab (https://docs.slack.dev/surfaces/app-home/) +https://docs.slack.dev/reference/methods/views.publish

+
+
+def views_push(self,
*,
trigger_id: str | None = None,
interactivity_pointer: str | None = None,
view: dict | View,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def views_push(
+    self,
+    *,
+    trigger_id: Optional[str] = None,
+    interactivity_pointer: Optional[str] = None,
+    view: Union[dict, View],
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Push a view onto the stack of a root view.
+    Push a new view onto the existing view stack by passing a view
+    payload and a valid trigger_id generated from an interaction
+    within the existing modal.
+    Read the modals documentation (https://docs.slack.dev/surfaces/modals/)
+    to learn more about the lifecycle and intricacies of views.
+    https://docs.slack.dev/reference/methods/views.push
+    """
+    kwargs.update({"trigger_id": trigger_id, "interactivity_pointer": interactivity_pointer})
+    if isinstance(view, View):
+        kwargs.update({"view": view.to_dict()})
+    else:
+        kwargs.update({"view": view})
+    kwargs = _remove_none_values(kwargs)
+    # NOTE: Intentionally using json for the "view" parameter
+    return self.api_call("views.push", json=kwargs)
+
+

Push a view onto the stack of a root view. +Push a new view onto the existing view stack by passing a view +payload and a valid trigger_id generated from an interaction +within the existing modal. +Read the modals documentation (https://docs.slack.dev/surfaces/modals/) +to learn more about the lifecycle and intricacies of views. +https://docs.slack.dev/reference/methods/views.push

+
+
+def views_update(self,
*,
view: dict | View,
external_id: str | None = None,
view_id: str | None = None,
hash: str | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def views_update(
+    self,
+    *,
+    view: Union[dict, View],
+    external_id: Optional[str] = None,
+    view_id: Optional[str] = None,
+    hash: Optional[str] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Update an existing view.
+    Update a view by passing a new view definition along with the
+    view_id returned in views.open or the external_id.
+    See the modals documentation (https://docs.slack.dev/surfaces/modals/#updating_views)
+    to learn more about updating views and avoiding race conditions with the hash argument.
+    https://docs.slack.dev/reference/methods/views.update
+    """
+    if isinstance(view, View):
+        kwargs.update({"view": view.to_dict()})
+    else:
+        kwargs.update({"view": view})
+    if external_id:
+        kwargs.update({"external_id": external_id})
+    elif view_id:
+        kwargs.update({"view_id": view_id})
+    else:
+        raise e.SlackRequestError("Either view_id or external_id is required.")
+    kwargs.update({"hash": hash})
+    kwargs = _remove_none_values(kwargs)
+    # NOTE: Intentionally using json for the "view" parameter
+    return self.api_call("views.update", json=kwargs)
+
+

Update an existing view. +Update a view by passing a new view definition along with the +view_id returned in views.open or the external_id. +See the modals documentation (https://docs.slack.dev/surfaces/modals/#updating_views) +to learn more about updating views and avoiding race conditions with the hash argument. +https://docs.slack.dev/reference/methods/views.update

+
+ +
+
+ +Expand source code + +
def workflows_featured_add(
+    self,
+    *,
+    channel_id: str,
+    trigger_ids: Union[str, Sequence[str]],
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Add featured workflows to a channel.
+    https://docs.slack.dev/reference/methods/workflows.featured.add
+    """
+    kwargs.update({"channel_id": channel_id})
+    if isinstance(trigger_ids, (list, tuple)):
+        kwargs.update({"trigger_ids": ",".join(trigger_ids)})
+    else:
+        kwargs.update({"trigger_ids": trigger_ids})
+    return self.api_call("workflows.featured.add", params=kwargs)
+
+ +
+ +
+
+ +Expand source code + +
def workflows_featured_list(
+    self,
+    *,
+    channel_ids: Union[str, Sequence[str]],
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """List the featured workflows for specified channels.
+    https://docs.slack.dev/reference/methods/workflows.featured.list
+    """
+    if isinstance(channel_ids, (list, tuple)):
+        kwargs.update({"channel_ids": ",".join(channel_ids)})
+    else:
+        kwargs.update({"channel_ids": channel_ids})
+    return self.api_call("workflows.featured.list", params=kwargs)
+
+

List the featured workflows for specified channels. +https://docs.slack.dev/reference/methods/workflows.featured.list

+
+ +
+
+ +Expand source code + +
def workflows_featured_remove(
+    self,
+    *,
+    channel_id: str,
+    trigger_ids: Union[str, Sequence[str]],
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Remove featured workflows from a channel.
+    https://docs.slack.dev/reference/methods/workflows.featured.remove
+    """
+    kwargs.update({"channel_id": channel_id})
+    if isinstance(trigger_ids, (list, tuple)):
+        kwargs.update({"trigger_ids": ",".join(trigger_ids)})
+    else:
+        kwargs.update({"trigger_ids": trigger_ids})
+    return self.api_call("workflows.featured.remove", params=kwargs)
+
+ +
+ +
+
+ +Expand source code + +
def workflows_featured_set(
+    self,
+    *,
+    channel_id: str,
+    trigger_ids: Union[str, Sequence[str]],
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Set featured workflows for a channel.
+    https://docs.slack.dev/reference/methods/workflows.featured.set
+    """
+    kwargs.update({"channel_id": channel_id})
+    if isinstance(trigger_ids, (list, tuple)):
+        kwargs.update({"trigger_ids": ",".join(trigger_ids)})
+    else:
+        kwargs.update({"trigger_ids": trigger_ids})
+    return self.api_call("workflows.featured.set", params=kwargs)
+
+ +
+
+def workflows_stepCompleted(self, *, workflow_step_execute_id: str, outputs: dict | None = None, **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def workflows_stepCompleted(
+    self,
+    *,
+    workflow_step_execute_id: str,
+    outputs: Optional[dict] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Indicate a successful outcome of a workflow step's execution.
+    https://docs.slack.dev/reference/methods/workflows.stepCompleted
+    """
+    kwargs.update({"workflow_step_execute_id": workflow_step_execute_id})
+    if outputs is not None:
+        kwargs.update({"outputs": outputs})
+    kwargs = _remove_none_values(kwargs)
+    # NOTE: Intentionally using json for the "outputs" parameter
+    return self.api_call("workflows.stepCompleted", json=kwargs)
+
+

Indicate a successful outcome of a workflow step's execution. +https://docs.slack.dev/reference/methods/workflows.stepCompleted

+
+
+def workflows_stepFailed(self, *, workflow_step_execute_id: str, error: Dict[str, str], **kwargs) ‑> _asyncio.Future | LegacySlackResponse +
+
+
+ +Expand source code + +
def workflows_stepFailed(
+    self,
+    *,
+    workflow_step_execute_id: str,
+    error: Dict[str, str],
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Indicate an unsuccessful outcome of a workflow step's execution.
+    https://docs.slack.dev/reference/methods/workflows.stepFailed
+    """
+    kwargs.update(
+        {
+            "workflow_step_execute_id": workflow_step_execute_id,
+            "error": error,
+        }
+    )
+    kwargs = _remove_none_values(kwargs)
+    # NOTE: Intentionally using json for the "error" parameter
+    return self.api_call("workflows.stepFailed", json=kwargs)
+
+

Indicate an unsuccessful outcome of a workflow step's execution. +https://docs.slack.dev/reference/methods/workflows.stepFailed

+
+
+def workflows_updateStep(self,
*,
workflow_step_edit_id: str,
inputs: Dict[str, Any] | None = None,
outputs: List[Dict[str, str]] | None = None,
**kwargs) ‑> _asyncio.Future | LegacySlackResponse
+
+
+
+ +Expand source code + +
def workflows_updateStep(
+    self,
+    *,
+    workflow_step_edit_id: str,
+    inputs: Optional[Dict[str, Any]] = None,
+    outputs: Optional[List[Dict[str, str]]] = None,
+    **kwargs,
+) -> Union[Future, SlackResponse]:
+    """Update the configuration for a workflow extension step.
+    https://docs.slack.dev/reference/methods/workflows.updateStep
+    """
+    kwargs.update({"workflow_step_edit_id": workflow_step_edit_id})
+    if inputs is not None:
+        kwargs.update({"inputs": inputs})
+    if outputs is not None:
+        kwargs.update({"outputs": outputs})
+    kwargs = _remove_none_values(kwargs)
+    # NOTE: Intentionally using json for the "inputs" / "outputs" parameters
+    return self.api_call("workflows.updateStep", json=kwargs)
+
+

Update the configuration for a workflow extension step. +https://docs.slack.dev/reference/methods/workflows.updateStep

+
+
+

Inherited members

+ +
+
+
+
+ +
+ + + diff --git a/docs/reference/web/legacy_slack_response.html b/docs/reference/web/legacy_slack_response.html new file mode 100644 index 000000000..c0531066d --- /dev/null +++ b/docs/reference/web/legacy_slack_response.html @@ -0,0 +1,414 @@ + + + + + + +slack_sdk.web.legacy_slack_response API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.web.legacy_slack_response

+
+
+

A Python module for interacting and consuming responses from Slack.

+
+
+
+
+
+
+
+
+

Classes

+
+
+class LegacySlackResponse +(*,
client,
http_verb: str,
api_url: str,
req_args: dict,
data: dict | bytes,
headers: dict,
status_code: int,
use_sync_aiohttp: bool = True)
+
+
+
+ +Expand source code + +
class LegacySlackResponse(object):
+    """An iterable container of response data.
+
+    Attributes:
+        data (dict): The json-encoded content of the response. Along
+            with the headers and status code information.
+
+    Methods:
+        validate: Check if the response from Slack was successful.
+        get: Retrieves any key from the response data.
+        next: Retrieves the next portion of results,
+            if 'next_cursor' is present.
+
+    Example:
+    ```python
+    import os
+    import slack
+
+    client = slack.WebClient(token=os.environ['SLACK_API_TOKEN'])
+
+    response1 = client.auth_revoke(test='true')
+    assert not response1['revoked']
+
+    response2 = client.auth_test()
+    assert response2.get('ok', False)
+
+    users = []
+    for page in client.users_list(limit=2):
+        TODO: This example should specify when to break.
+        users = users + page['members']
+    ```
+
+    Note:
+        Some responses return collections of information
+        like channel and user lists. If they do it's likely
+        that you'll only receive a portion of results. This
+        object allows you to iterate over the response which
+        makes subsequent API requests until your code hits
+        'break' or there are no more results to be found.
+
+        Any attributes or methods prefixed with _underscores are
+        intended to be "private" internal use only. They may be changed or
+        removed at anytime.
+    """
+
+    def __init__(
+        self,
+        *,
+        client,
+        http_verb: str,
+        api_url: str,
+        req_args: dict,
+        data: Union[dict, bytes],  # data can be binary data
+        headers: dict,
+        status_code: int,
+        use_sync_aiohttp: bool = True,  # True for backward-compatibility
+    ):
+        self.http_verb = http_verb
+        self.api_url = api_url
+        self.req_args = req_args
+        self.data = data
+        self.headers = headers
+        self.status_code = status_code
+        self._initial_data = data
+        self._client = client  # LegacyWebClient
+        self._use_sync_aiohttp = use_sync_aiohttp
+        self._logger = logging.getLogger(__name__)
+
+    def __str__(self):
+        """Return the Response data if object is converted to a string."""
+        if isinstance(self.data, bytes):
+            raise ValueError("As the response.data is binary data, this operation is unsupported")
+        return f"{self.data}"
+
+    def __getitem__(self, key):
+        """Retrieves any key from the data store.
+
+        Note:
+            This is implemented so users can reference the
+            SlackResponse object like a dictionary.
+            e.g. response["ok"]
+
+        Returns:
+            The value from data or None.
+        """
+        if isinstance(self.data, bytes):
+            raise ValueError("As the response.data is binary data, this operation is unsupported")
+        return self.data.get(key, None)
+
+    def __iter__(self):
+        """Enables the ability to iterate over the response.
+        It's required for the iterator protocol.
+
+        Note:
+            This enables Slack cursor-based pagination.
+
+        Returns:
+            (SlackResponse) self
+        """
+        if isinstance(self.data, bytes):
+            raise ValueError("As the response.data is binary data, this operation is unsupported")
+        self._iteration = 0
+        self.data = self._initial_data
+        return self
+
+    def __next__(self):
+        """Retrieves the next portion of results, if 'next_cursor' is present.
+
+        Note:
+            Some responses return collections of information
+            like channel and user lists. If they do it's likely
+            that you'll only receive a portion of results. This
+            method allows you to iterate over the response until
+            your code hits 'break' or there are no more results
+            to be found.
+
+        Returns:
+            (SlackResponse) self
+                With the new response data now attached to this object.
+
+        Raises:
+            SlackApiError: If the request to the Slack API failed.
+            StopIteration: If 'next_cursor' is not present or empty.
+        """
+        if isinstance(self.data, bytes):
+            raise ValueError("As the response.data is binary data, this operation is unsupported")
+        self._iteration += 1
+        if self._iteration == 1:
+            return self
+        if self._next_cursor_is_present(self.data):
+            params = self.req_args.get("params", {})
+            if params is None:
+                params = {}
+            params.update({"cursor": self.data["response_metadata"]["next_cursor"]})
+            self.req_args.update({"params": params})
+
+            if self._use_sync_aiohttp:
+                # We no longer recommend going with this way
+                response = asyncio.get_event_loop().run_until_complete(
+                    self._client._request(
+                        http_verb=self.http_verb,
+                        api_url=self.api_url,
+                        req_args=self.req_args,
+                    )
+                )
+            else:
+                # This method sends a request in a synchronous way
+                response = self._client._request_for_pagination(api_url=self.api_url, req_args=self.req_args)
+
+            self.data = response["data"]
+            self.headers = response["headers"]
+            self.status_code = response["status_code"]
+            return self.validate()
+        else:
+            raise StopIteration
+
+    def get(self, key, default=None):
+        """Retrieves any key from the response data.
+
+        Note:
+            This is implemented so users can reference the
+            SlackResponse object like a dictionary.
+            e.g. response.get("ok", False)
+
+        Returns:
+            The value from data or the specified default.
+        """
+        if isinstance(self.data, bytes):
+            raise ValueError("As the response.data is binary data, this operation is unsupported")
+        return self.data.get(key, default)
+
+    def validate(self):
+        """Check if the response from Slack was successful.
+
+        Returns:
+            (SlackResponse)
+                This method returns it's own object. e.g. 'self'
+
+        Raises:
+            SlackApiError: The request to the Slack API failed.
+        """
+        if self._logger.level <= logging.DEBUG:
+            body = self.data if isinstance(self.data, dict) else "(binary)"
+            self._logger.debug(
+                "Received the following response - "
+                f"status: {self.status_code}, "
+                f"headers: {dict(self.headers)}, "
+                f"body: {body}"
+            )
+        if self.status_code == 200 and self.data and (isinstance(self.data, bytes) or self.data.get("ok", False)):
+            return self
+        msg = "The request to the Slack API failed."
+        raise e.SlackApiError(message=msg, response=self)
+
+    @staticmethod
+    def _next_cursor_is_present(data):
+        """Determine if the response contains 'next_cursor'
+        and 'next_cursor' is not empty.
+
+        Returns:
+            A boolean value.
+        """
+        present = (
+            "response_metadata" in data
+            and "next_cursor" in data["response_metadata"]
+            and data["response_metadata"]["next_cursor"] != ""
+        )
+        return present
+
+

An iterable container of response data.

+

Attributes

+
+
data : dict
+
The json-encoded content of the response. Along +with the headers and status code information.
+
+

Methods

+

validate: Check if the response from Slack was successful. +get: Retrieves any key from the response data. +next: Retrieves the next portion of results, +if 'next_cursor' is present.

+

Example:

+
import os
+import slack
+
+client = slack.WebClient(token=os.environ['SLACK_API_TOKEN'])
+
+response1 = client.auth_revoke(test='true')
+assert not response1['revoked']
+
+response2 = client.auth_test()
+assert response2.get('ok', False)
+
+users = []
+for page in client.users_list(limit=2):
+    TODO: This example should specify when to break.
+    users = users + page['members']
+
+

Note

+

Some responses return collections of information +like channel and user lists. If they do it's likely +that you'll only receive a portion of results. This +object allows you to iterate over the response which +makes subsequent API requests until your code hits +'break' or there are no more results to be found.

+

Any attributes or methods prefixed with _underscores are +intended to be "private" internal use only. They may be changed or +removed at anytime.

+

Methods

+
+
+def get(self, key, default=None) +
+
+
+ +Expand source code + +
def get(self, key, default=None):
+    """Retrieves any key from the response data.
+
+    Note:
+        This is implemented so users can reference the
+        SlackResponse object like a dictionary.
+        e.g. response.get("ok", False)
+
+    Returns:
+        The value from data or the specified default.
+    """
+    if isinstance(self.data, bytes):
+        raise ValueError("As the response.data is binary data, this operation is unsupported")
+    return self.data.get(key, default)
+
+

Retrieves any key from the response data.

+

Note

+

This is implemented so users can reference the +SlackResponse object like a dictionary. +e.g. response.get("ok", False)

+

Returns

+

The value from data or the specified default.

+
+
+def validate(self) +
+
+
+ +Expand source code + +
def validate(self):
+    """Check if the response from Slack was successful.
+
+    Returns:
+        (SlackResponse)
+            This method returns it's own object. e.g. 'self'
+
+    Raises:
+        SlackApiError: The request to the Slack API failed.
+    """
+    if self._logger.level <= logging.DEBUG:
+        body = self.data if isinstance(self.data, dict) else "(binary)"
+        self._logger.debug(
+            "Received the following response - "
+            f"status: {self.status_code}, "
+            f"headers: {dict(self.headers)}, "
+            f"body: {body}"
+        )
+    if self.status_code == 200 and self.data and (isinstance(self.data, bytes) or self.data.get("ok", False)):
+        return self
+    msg = "The request to the Slack API failed."
+    raise e.SlackApiError(message=msg, response=self)
+
+

Check if the response from Slack was successful.

+

Returns

+

(SlackResponse) +This method returns it's own object. e.g. 'self'

+

Raises

+
+
SlackApiError
+
The request to the Slack API failed.
+
+
+
+
+
+
+
+ +
+ + + diff --git a/docs/reference/web/slack_response.html b/docs/reference/web/slack_response.html new file mode 100644 index 000000000..6746b3c7d --- /dev/null +++ b/docs/reference/web/slack_response.html @@ -0,0 +1,385 @@ + + + + + + +slack_sdk.web.slack_response API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.web.slack_response

+
+
+

A Python module for interacting and consuming responses from Slack.

+
+
+
+
+
+
+
+
+

Classes

+
+
+class SlackResponse +(*,
client,
http_verb: str,
api_url: str,
req_args: dict,
data: dict | bytes,
headers: dict,
status_code: int)
+
+
+
+ +Expand source code + +
class SlackResponse:
+    """An iterable container of response data.
+
+    Attributes:
+        data (dict): The json-encoded content of the response. Along
+            with the headers and status code information.
+
+    Methods:
+        validate: Check if the response from Slack was successful.
+        get: Retrieves any key from the response data.
+        next: Retrieves the next portion of results,
+            if 'next_cursor' is present.
+
+    Example:
+    ```python
+    import os
+    import slack
+
+    client = slack.WebClient(token=os.environ['SLACK_API_TOKEN'])
+
+    response1 = client.auth_revoke(test='true')
+    assert not response1['revoked']
+
+    response2 = client.auth_test()
+    assert response2.get('ok', False)
+
+    users = []
+    for page in client.users_list(limit=2):
+        users = users + page['members']
+    ```
+
+    Note:
+        Some responses return collections of information
+        like channel and user lists. If they do it's likely
+        that you'll only receive a portion of results. This
+        object allows you to iterate over the response which
+        makes subsequent API requests until your code hits
+        'break' or there are no more results to be found.
+
+        Any attributes or methods prefixed with _underscores are
+        intended to be "private" internal use only. They may be changed or
+        removed at anytime.
+    """
+
+    def __init__(
+        self,
+        *,
+        client,
+        http_verb: str,
+        api_url: str,
+        req_args: dict,
+        data: Union[dict, bytes],  # data can be binary data
+        headers: dict,
+        status_code: int,
+    ):
+        self.http_verb = http_verb
+        self.api_url = api_url
+        self.req_args = req_args
+        self.data = data
+        self.headers = headers
+        self.status_code = status_code
+        self._initial_data = data
+        self._iteration = None  # for __iter__ & __next__
+        self._client = client
+        self._logger = logging.getLogger(__name__)
+
+    def __str__(self):
+        """Return the Response data if object is converted to a string."""
+        if isinstance(self.data, bytes):
+            raise ValueError("As the response.data is binary data, this operation is unsupported")
+        return f"{self.data}"
+
+    def __contains__(self, key: str) -> bool:
+        return self.get(key) is not None
+
+    def __getitem__(self, key):
+        """Retrieves any key from the data store.
+
+        Note:
+            This is implemented so users can reference the
+            SlackResponse object like a dictionary.
+            e.g. response["ok"]
+
+        Returns:
+            The value from data or None.
+        """
+        if isinstance(self.data, bytes):
+            raise ValueError("As the response.data is binary data, this operation is unsupported")
+        if self.data is None:
+            raise ValueError("As the response.data is empty, this operation is unsupported")
+        return self.data.get(key, None)
+
+    def __iter__(self):
+        """Enables the ability to iterate over the response.
+        It's required for the iterator protocol.
+
+        Note:
+            This enables Slack cursor-based pagination.
+
+        Returns:
+            (SlackResponse) self
+        """
+        self._iteration = 0
+        self.data = self._initial_data
+        return self
+
+    def __next__(self):
+        """Retrieves the next portion of results, if 'next_cursor' is present.
+
+        Note:
+            Some responses return collections of information
+            like channel and user lists. If they do it's likely
+            that you'll only receive a portion of results. This
+            method allows you to iterate over the response until
+            your code hits 'break' or there are no more results
+            to be found.
+
+        Returns:
+            (SlackResponse) self
+                With the new response data now attached to this object.
+
+        Raises:
+            SlackApiError: If the request to the Slack API failed.
+            StopIteration: If 'next_cursor' is not present or empty.
+        """
+        if isinstance(self.data, bytes):
+            raise ValueError("As the response.data is binary data, this operation is unsupported")
+        self._iteration += 1
+        if self._iteration == 1:
+            return self
+        if _next_cursor_is_present(self.data):
+            params = self.req_args.get("params", {})
+            if params is None:
+                params = {}
+            next_cursor = self.data.get("response_metadata", {}).get("next_cursor") or self.data.get("next_cursor")
+            params.update({"cursor": next_cursor})
+            self.req_args.update({"params": params})
+
+            # This method sends a request in a synchronous way
+            response = self._client._request_for_pagination(api_url=self.api_url, req_args=self.req_args)
+            self.data = response["data"]
+            self.headers = response["headers"]
+            self.status_code = response["status_code"]
+            return self.validate()
+        else:
+            raise StopIteration
+
+    @overload
+    def get(self, key: str, default: None = None) -> Optional[Any]:
+        ...
+
+    @overload
+    def get(self, key: str, default: T) -> T:
+        ...
+
+    def get(self, key, default=None):
+        """Retrieves any key from the response data.
+
+        Note:
+            This is implemented so users can reference the
+            SlackResponse object like a dictionary.
+            e.g. response.get("ok", False)
+
+        Returns:
+            The value from data or the specified default.
+        """
+        if isinstance(self.data, bytes):
+            raise ValueError("As the response.data is binary data, this operation is unsupported")
+        if self.data is None:
+            return None
+        return self.data.get(key, default)
+
+    def validate(self):
+        """Check if the response from Slack was successful.
+
+        Returns:
+            (SlackResponse)
+                This method returns it's own object. e.g. 'self'
+
+        Raises:
+            SlackApiError: The request to the Slack API failed.
+        """
+        if self.status_code == 200 and self.data and (isinstance(self.data, bytes) or self.data.get("ok", False)):
+            return self
+        msg = f"The request to the Slack API failed. (url: {self.api_url})"
+        raise e.SlackApiError(message=msg, response=self)
+
+

An iterable container of response data.

+

Attributes

+
+
data : dict
+
The json-encoded content of the response. Along +with the headers and status code information.
+
+

Methods

+

validate: Check if the response from Slack was successful. +get: Retrieves any key from the response data. +next: Retrieves the next portion of results, +if 'next_cursor' is present.

+

Example:

+
import os
+import slack
+
+client = slack.WebClient(token=os.environ['SLACK_API_TOKEN'])
+
+response1 = client.auth_revoke(test='true')
+assert not response1['revoked']
+
+response2 = client.auth_test()
+assert response2.get('ok', False)
+
+users = []
+for page in client.users_list(limit=2):
+    users = users + page['members']
+
+

Note

+

Some responses return collections of information +like channel and user lists. If they do it's likely +that you'll only receive a portion of results. This +object allows you to iterate over the response which +makes subsequent API requests until your code hits +'break' or there are no more results to be found.

+

Any attributes or methods prefixed with _underscores are +intended to be "private" internal use only. They may be changed or +removed at anytime.

+

Methods

+
+
+def get(self, key, default=None) +
+
+
+ +Expand source code + +
def get(self, key, default=None):
+    """Retrieves any key from the response data.
+
+    Note:
+        This is implemented so users can reference the
+        SlackResponse object like a dictionary.
+        e.g. response.get("ok", False)
+
+    Returns:
+        The value from data or the specified default.
+    """
+    if isinstance(self.data, bytes):
+        raise ValueError("As the response.data is binary data, this operation is unsupported")
+    if self.data is None:
+        return None
+    return self.data.get(key, default)
+
+

Retrieves any key from the response data.

+

Note

+

This is implemented so users can reference the +SlackResponse object like a dictionary. +e.g. response.get("ok", False)

+

Returns

+

The value from data or the specified default.

+
+
+def validate(self) +
+
+
+ +Expand source code + +
def validate(self):
+    """Check if the response from Slack was successful.
+
+    Returns:
+        (SlackResponse)
+            This method returns it's own object. e.g. 'self'
+
+    Raises:
+        SlackApiError: The request to the Slack API failed.
+    """
+    if self.status_code == 200 and self.data and (isinstance(self.data, bytes) or self.data.get("ok", False)):
+        return self
+    msg = f"The request to the Slack API failed. (url: {self.api_url})"
+    raise e.SlackApiError(message=msg, response=self)
+
+

Check if the response from Slack was successful.

+

Returns

+

(SlackResponse) +This method returns it's own object. e.g. 'self'

+

Raises

+
+
SlackApiError
+
The request to the Slack API failed.
+
+
+
+
+
+
+
+ +
+ + + diff --git a/docs/reference/webhook/async_client.html b/docs/reference/webhook/async_client.html new file mode 100644 index 000000000..15c7e4a45 --- /dev/null +++ b/docs/reference/webhook/async_client.html @@ -0,0 +1,537 @@ + + + + + + +slack_sdk.webhook.async_client API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.webhook.async_client

+
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class AsyncWebhookClient +(url: str,
timeout: int = 30,
ssl: ssl.SSLContext | None = None,
proxy: str | None = None,
session: aiohttp.client.ClientSession | None = None,
trust_env_in_session: bool = False,
auth: aiohttp.helpers.BasicAuth | None = None,
default_headers: Dict[str, str] | None = None,
user_agent_prefix: str | None = None,
user_agent_suffix: str | None = None,
logger: logging.Logger | None = None,
retry_handlers: List[slack_sdk.http_retry.async_handler.AsyncRetryHandler] | None = None)
+
+
+
+ +Expand source code + +
class AsyncWebhookClient:
+    url: str
+    timeout: int
+    ssl: Optional[SSLContext]
+    proxy: Optional[str]
+    session: Optional[ClientSession]
+    trust_env_in_session: bool
+    auth: Optional[BasicAuth]
+    default_headers: Dict[str, str]
+    logger: logging.Logger
+    retry_handlers: List[AsyncRetryHandler]
+
+    def __init__(
+        self,
+        url: str,
+        timeout: int = 30,
+        ssl: Optional[SSLContext] = None,
+        proxy: Optional[str] = None,
+        session: Optional[ClientSession] = None,
+        trust_env_in_session: bool = False,
+        auth: Optional[BasicAuth] = None,
+        default_headers: Optional[Dict[str, str]] = None,
+        user_agent_prefix: Optional[str] = None,
+        user_agent_suffix: Optional[str] = None,
+        logger: Optional[logging.Logger] = None,
+        retry_handlers: Optional[List[AsyncRetryHandler]] = None,
+    ):
+        """API client for Incoming Webhooks and `response_url`
+
+        https://docs.slack.dev/messaging/sending-messages-using-incoming-webhooks/
+
+        Args:
+            url: Complete URL to send data (e.g., `https://hooks.slack.com/XXX`)
+            timeout: Request timeout (in seconds)
+            ssl: `ssl.SSLContext` to use for requests
+            proxy: Proxy URL (e.g., `localhost:9000`, `http://localhost:9000`)
+            session: `aiohttp.ClientSession` instance
+            trust_env_in_session: True/False for `aiohttp.ClientSession`
+            auth: Basic auth info for `aiohttp.ClientSession`
+            default_headers: Request headers to add to all requests
+            user_agent_prefix: Prefix for User-Agent header value
+            user_agent_suffix: Suffix for User-Agent header value
+            logger: Custom logger
+        """
+        self.url = url
+        self.timeout = timeout
+        self.ssl = ssl
+        self.proxy = proxy
+        self.trust_env_in_session = trust_env_in_session
+        self.session = session
+        self.auth = auth
+        self.default_headers = default_headers if default_headers else {}
+        self.default_headers["User-Agent"] = get_user_agent(user_agent_prefix, user_agent_suffix)
+        self.logger = logger if logger is not None else logging.getLogger(__name__)
+        self.retry_handlers = retry_handlers if retry_handlers is not None else async_default_handlers()
+
+        if self.proxy is None or len(self.proxy.strip()) == 0:
+            env_variable = load_http_proxy_from_env(self.logger)
+            if env_variable is not None:
+                self.proxy = env_variable
+
+    async def send(
+        self,
+        *,
+        text: Optional[str] = None,
+        attachments: Optional[Sequence[Union[Dict[str, Any], Attachment]]] = None,
+        blocks: Optional[Sequence[Union[Dict[str, Any], Block]]] = None,
+        response_type: Optional[str] = None,
+        replace_original: Optional[bool] = None,
+        delete_original: Optional[bool] = None,
+        unfurl_links: Optional[bool] = None,
+        unfurl_media: Optional[bool] = None,
+        metadata: Optional[Dict[str, Any]] = None,
+        headers: Optional[Dict[str, str]] = None,
+    ) -> WebhookResponse:
+        """Performs a Slack API request and returns the result.
+
+        Args:
+            text: The text message (even when having blocks, setting this as well is recommended as it works as fallback)
+            attachments: A collection of attachments
+            blocks: A collection of Block Kit UI components
+            response_type: The type of message (either 'in_channel' or 'ephemeral')
+            replace_original: True if you use this option for response_url requests
+            delete_original: True if you use this option for response_url requests
+            unfurl_links: Option to indicate whether text url should unfurl
+            unfurl_media: Option to indicate whether media url should unfurl
+            metadata: Metadata attached to the message
+            headers: Request headers to append only for this request
+
+        Returns:
+            Webhook response
+        """
+        return await self.send_dict(
+            # It's fine to have None value elements here
+            # because _build_body() filters them out when constructing the actual body data
+            body={
+                "text": text,
+                "attachments": attachments,
+                "blocks": blocks,
+                "response_type": response_type,
+                "replace_original": replace_original,
+                "delete_original": delete_original,
+                "unfurl_links": unfurl_links,
+                "unfurl_media": unfurl_media,
+                "metadata": metadata,
+            },
+            headers=headers,
+        )
+
+    async def send_dict(self, body: Dict[str, Any], headers: Optional[Dict[str, str]] = None) -> WebhookResponse:
+        """Performs a Slack API request and returns the result.
+
+        Args:
+            body: JSON data structure (it's still a dict at this point),
+                if you give this argument, body_params and files will be skipped
+            headers: Request headers to append only for this request
+        Returns:
+            Webhook response
+        """
+        return await self._perform_http_request(
+            body=_build_body(body),  # type: ignore[arg-type]
+            headers=_build_request_headers(self.default_headers, headers),
+        )
+
+    async def _perform_http_request(self, *, body: Dict[str, Any], headers: Dict[str, str]) -> WebhookResponse:
+        str_body: str = json.dumps(body)
+        headers["Content-Type"] = "application/json;charset=utf-8"
+
+        session: Optional[ClientSession] = None
+        use_running_session = self.session and not self.session.closed
+        if use_running_session:
+            session = self.session
+        else:
+            session = aiohttp.ClientSession(
+                timeout=aiohttp.ClientTimeout(total=self.timeout),
+                auth=self.auth,
+                trust_env=self.trust_env_in_session,
+            )
+
+        last_error: Optional[Exception] = None
+        resp: Optional[WebhookResponse] = None
+        try:
+            request_kwargs = {
+                "headers": headers,
+                "data": str_body,
+                "ssl": self.ssl,
+                "proxy": self.proxy,
+            }
+            retry_request = RetryHttpRequest(
+                method="POST",
+                url=self.url,
+                headers=headers,  # type: ignore[arg-type]
+                body_params=body,
+            )
+
+            retry_state = RetryState()
+            counter_for_safety = 0
+            while counter_for_safety < 100:
+                counter_for_safety += 1
+                # If this is a retry, the next try started here. We can reset the flag.
+                retry_state.next_attempt_requested = False
+                retry_response: Optional[RetryHttpResponse] = None
+                response_body = ""
+
+                if self.logger.level <= logging.DEBUG:
+                    self.logger.debug(f"Sending a request - url: {self.url}, body: {str_body}, headers: {headers}")
+
+                try:
+                    async with session.request("POST", self.url, **request_kwargs) as res:  # type: ignore[arg-type, union-attr] # noqa: E501
+                        try:
+                            response_body = await res.text()
+                            retry_response = RetryHttpResponse(
+                                status_code=res.status,
+                                headers=res.headers,  # type: ignore[arg-type]
+                                data=response_body.encode("utf-8") if response_body is not None else None,
+                            )
+                        except aiohttp.ContentTypeError:
+                            self.logger.debug(f"No response data returned from the following API call: {self.url}")
+                            retry_response = RetryHttpResponse(
+                                status_code=res.status,
+                                headers=res.headers,  # type: ignore[arg-type]
+                            )
+
+                        if res.status == 429:
+                            for handler in self.retry_handlers:
+                                if await handler.can_retry_async(
+                                    state=retry_state,
+                                    request=retry_request,
+                                    response=retry_response,
+                                ):
+                                    if self.logger.level <= logging.DEBUG:
+                                        self.logger.info(
+                                            f"A retry handler found: {type(handler).__name__} "
+                                            f"for POST {self.url} - rate_limited"
+                                        )
+                                    await handler.prepare_for_next_attempt_async(
+                                        state=retry_state,
+                                        request=retry_request,
+                                        response=retry_response,
+                                    )
+                                    break
+
+                        if retry_state.next_attempt_requested is False:
+                            resp = WebhookResponse(
+                                url=self.url,
+                                status_code=res.status,
+                                body=response_body,
+                                headers=res.headers,  # type: ignore[arg-type]
+                            )
+                            _debug_log_response(self.logger, resp)
+                            return resp
+
+                except Exception as e:
+                    last_error = e
+                    for handler in self.retry_handlers:
+                        if await handler.can_retry_async(
+                            state=retry_state,
+                            request=retry_request,
+                            response=retry_response,
+                            error=e,
+                        ):
+                            if self.logger.level <= logging.DEBUG:
+                                self.logger.info(
+                                    f"A retry handler found: {type(handler).__name__} " f"for POST {self.url} - {e}"
+                                )
+                            await handler.prepare_for_next_attempt_async(
+                                state=retry_state,
+                                request=retry_request,
+                                response=retry_response,
+                                error=e,
+                            )
+                            break
+
+                    if retry_state.next_attempt_requested is False:
+                        raise last_error
+
+            if resp is not None:
+                return resp
+            raise last_error  # type: ignore[misc]
+
+        finally:
+            if not use_running_session:
+                await session.close()  # type: ignore[union-attr]
+
+        return resp
+
+

API client for Incoming Webhooks and response_url

+

https://docs.slack.dev/messaging/sending-messages-using-incoming-webhooks/

+

Args

+
+
url
+
Complete URL to send data (e.g., https://hooks.slack.com/XXX)
+
timeout
+
Request timeout (in seconds)
+
ssl
+
ssl.SSLContext to use for requests
+
proxy
+
Proxy URL (e.g., localhost:9000, http://localhost:9000)
+
session
+
aiohttp.ClientSession instance
+
trust_env_in_session
+
True/False for aiohttp.ClientSession
+
auth
+
Basic auth info for aiohttp.ClientSession
+
default_headers
+
Request headers to add to all requests
+
user_agent_prefix
+
Prefix for User-Agent header value
+
user_agent_suffix
+
Suffix for User-Agent header value
+
logger
+
Custom logger
+
+

Class variables

+
+
var auth : aiohttp.helpers.BasicAuth | None
+
+

The type of the None singleton.

+
+
var default_headers : Dict[str, str]
+
+

The type of the None singleton.

+
+
var logger : logging.Logger
+
+

The type of the None singleton.

+
+
var proxy : str | None
+
+

The type of the None singleton.

+
+
var retry_handlers : List[slack_sdk.http_retry.async_handler.AsyncRetryHandler]
+
+

The type of the None singleton.

+
+
var session : aiohttp.client.ClientSession | None
+
+

The type of the None singleton.

+
+
var ssl : ssl.SSLContext | None
+
+

The type of the None singleton.

+
+
var timeout : int
+
+

The type of the None singleton.

+
+
var trust_env_in_session : bool
+
+

The type of the None singleton.

+
+
var url : str
+
+

The type of the None singleton.

+
+
+

Methods

+
+
+async def send(self,
*,
text: str | None = None,
attachments: Sequence[Dict[str, Any] | Attachment] | None = None,
blocks: Sequence[Dict[str, Any] | Block] | None = None,
response_type: str | None = None,
replace_original: bool | None = None,
delete_original: bool | None = None,
unfurl_links: bool | None = None,
unfurl_media: bool | None = None,
metadata: Dict[str, Any] | None = None,
headers: Dict[str, str] | None = None) ‑> WebhookResponse
+
+
+
+ +Expand source code + +
async def send(
+    self,
+    *,
+    text: Optional[str] = None,
+    attachments: Optional[Sequence[Union[Dict[str, Any], Attachment]]] = None,
+    blocks: Optional[Sequence[Union[Dict[str, Any], Block]]] = None,
+    response_type: Optional[str] = None,
+    replace_original: Optional[bool] = None,
+    delete_original: Optional[bool] = None,
+    unfurl_links: Optional[bool] = None,
+    unfurl_media: Optional[bool] = None,
+    metadata: Optional[Dict[str, Any]] = None,
+    headers: Optional[Dict[str, str]] = None,
+) -> WebhookResponse:
+    """Performs a Slack API request and returns the result.
+
+    Args:
+        text: The text message (even when having blocks, setting this as well is recommended as it works as fallback)
+        attachments: A collection of attachments
+        blocks: A collection of Block Kit UI components
+        response_type: The type of message (either 'in_channel' or 'ephemeral')
+        replace_original: True if you use this option for response_url requests
+        delete_original: True if you use this option for response_url requests
+        unfurl_links: Option to indicate whether text url should unfurl
+        unfurl_media: Option to indicate whether media url should unfurl
+        metadata: Metadata attached to the message
+        headers: Request headers to append only for this request
+
+    Returns:
+        Webhook response
+    """
+    return await self.send_dict(
+        # It's fine to have None value elements here
+        # because _build_body() filters them out when constructing the actual body data
+        body={
+            "text": text,
+            "attachments": attachments,
+            "blocks": blocks,
+            "response_type": response_type,
+            "replace_original": replace_original,
+            "delete_original": delete_original,
+            "unfurl_links": unfurl_links,
+            "unfurl_media": unfurl_media,
+            "metadata": metadata,
+        },
+        headers=headers,
+    )
+
+

Performs a Slack API request and returns the result.

+

Args

+
+
text
+
The text message (even when having blocks, setting this as well is recommended as it works as fallback)
+
attachments
+
A collection of attachments
+
blocks
+
A collection of Block Kit UI components
+
response_type
+
The type of message (either 'in_channel' or 'ephemeral')
+
replace_original
+
True if you use this option for response_url requests
+
delete_original
+
True if you use this option for response_url requests
+
unfurl_links
+
Option to indicate whether text url should unfurl
+
unfurl_media
+
Option to indicate whether media url should unfurl
+
metadata
+
Metadata attached to the message
+
headers
+
Request headers to append only for this request
+
+

Returns

+

Webhook response

+
+
+async def send_dict(self, body: Dict[str, Any], headers: Dict[str, str] | None = None) ‑> WebhookResponse +
+
+
+ +Expand source code + +
async def send_dict(self, body: Dict[str, Any], headers: Optional[Dict[str, str]] = None) -> WebhookResponse:
+    """Performs a Slack API request and returns the result.
+
+    Args:
+        body: JSON data structure (it's still a dict at this point),
+            if you give this argument, body_params and files will be skipped
+        headers: Request headers to append only for this request
+    Returns:
+        Webhook response
+    """
+    return await self._perform_http_request(
+        body=_build_body(body),  # type: ignore[arg-type]
+        headers=_build_request_headers(self.default_headers, headers),
+    )
+
+

Performs a Slack API request and returns the result.

+

Args

+
+
body
+
JSON data structure (it's still a dict at this point), +if you give this argument, body_params and files will be skipped
+
headers
+
Request headers to append only for this request
+
+

Returns

+

Webhook response

+
+
+
+
+
+
+ +
+ + + diff --git a/docs/reference/webhook/client.html b/docs/reference/webhook/client.html new file mode 100644 index 000000000..aebec5a3f --- /dev/null +++ b/docs/reference/webhook/client.html @@ -0,0 +1,527 @@ + + + + + + +slack_sdk.webhook.client API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.webhook.client

+
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class WebhookClient +(url: str,
timeout: int = 30,
ssl: ssl.SSLContext | None = None,
proxy: str | None = None,
default_headers: Dict[str, str] | None = None,
user_agent_prefix: str | None = None,
user_agent_suffix: str | None = None,
logger: logging.Logger | None = None,
retry_handlers: List[RetryHandler] | None = None)
+
+
+
+ +Expand source code + +
class WebhookClient:
+    url: str
+    timeout: int
+    ssl: Optional[SSLContext]
+    proxy: Optional[str]
+    default_headers: Dict[str, str]
+    logger: logging.Logger
+    retry_handlers: List[RetryHandler]
+
+    def __init__(
+        self,
+        url: str,
+        timeout: int = 30,
+        ssl: Optional[SSLContext] = None,
+        proxy: Optional[str] = None,
+        default_headers: Optional[Dict[str, str]] = None,
+        user_agent_prefix: Optional[str] = None,
+        user_agent_suffix: Optional[str] = None,
+        logger: Optional[logging.Logger] = None,
+        retry_handlers: Optional[List[RetryHandler]] = None,
+    ):
+        """API client for Incoming Webhooks and `response_url`
+
+        https://docs.slack.dev/messaging/sending-messages-using-incoming-webhooks/
+
+        Args:
+            url: Complete URL to send data (e.g., `https://hooks.slack.com/XXX`)
+            timeout: Request timeout (in seconds)
+            ssl: `ssl.SSLContext` to use for requests
+            proxy: Proxy URL (e.g., `localhost:9000`, `http://localhost:9000`)
+            default_headers: Request headers to add to all requests
+            user_agent_prefix: Prefix for User-Agent header value
+            user_agent_suffix: Suffix for User-Agent header value
+            logger: Custom logger
+            retry_handlers: Retry handlers
+        """
+        self.url = url
+        self.timeout = timeout
+        self.ssl = ssl
+        self.proxy = proxy
+        self.default_headers = default_headers if default_headers else {}
+        self.default_headers["User-Agent"] = get_user_agent(user_agent_prefix, user_agent_suffix)
+        self.logger = logger if logger is not None else logging.getLogger(__name__)
+        self.retry_handlers = retry_handlers if retry_handlers is not None else default_retry_handlers()
+
+        if self.proxy is None or len(self.proxy.strip()) == 0:
+            env_variable = load_http_proxy_from_env(self.logger)
+            if env_variable is not None:
+                self.proxy = env_variable
+
+    def send(
+        self,
+        *,
+        text: Optional[str] = None,
+        attachments: Optional[Sequence[Union[Dict[str, Any], Attachment]]] = None,
+        blocks: Optional[Sequence[Union[Dict[str, Any], Block]]] = None,
+        response_type: Optional[str] = None,
+        replace_original: Optional[bool] = None,
+        delete_original: Optional[bool] = None,
+        unfurl_links: Optional[bool] = None,
+        unfurl_media: Optional[bool] = None,
+        metadata: Optional[Dict[str, Any]] = None,
+        headers: Optional[Dict[str, str]] = None,
+    ) -> WebhookResponse:
+        """Performs a Slack API request and returns the result.
+
+        Args:
+            text: The text message
+                (even when having blocks, setting this as well is recommended as it works as fallback)
+            attachments: A collection of attachments
+            blocks: A collection of Block Kit UI components
+            response_type: The type of message (either 'in_channel' or 'ephemeral')
+            replace_original: True if you use this option for response_url requests
+            delete_original: True if you use this option for response_url requests
+            unfurl_links: Option to indicate whether text url should unfurl
+            unfurl_media: Option to indicate whether media url should unfurl
+            metadata: Metadata attached to the message
+            headers: Request headers to append only for this request
+
+        Returns:
+            Webhook response
+        """
+        return self.send_dict(
+            # It's fine to have None value elements here
+            # because _build_body() filters them out when constructing the actual body data
+            body={
+                "text": text,
+                "attachments": attachments,
+                "blocks": blocks,
+                "response_type": response_type,
+                "replace_original": replace_original,
+                "delete_original": delete_original,
+                "unfurl_links": unfurl_links,
+                "unfurl_media": unfurl_media,
+                "metadata": metadata,
+            },
+            headers=headers,
+        )
+
+    def send_dict(self, body: Dict[str, Any], headers: Optional[Dict[str, str]] = None) -> WebhookResponse:
+        """Performs a Slack API request and returns the result.
+
+        Args:
+            body: JSON data structure (it's still a dict at this point),
+                if you give this argument, body_params and files will be skipped
+            headers: Request headers to append only for this request
+        Returns:
+            Webhook response
+        """
+        return self._perform_http_request(
+            body=_build_body(body),  # type: ignore[arg-type]
+            headers=_build_request_headers(self.default_headers, headers),
+        )
+
+    def _perform_http_request(self, *, body: Dict[str, Any], headers: Dict[str, str]) -> WebhookResponse:
+        raw_body = json.dumps(body)
+        headers["Content-Type"] = "application/json;charset=utf-8"
+
+        if self.logger.level <= logging.DEBUG:
+            self.logger.debug(f"Sending a request - url: {self.url}, body: {raw_body}, headers: {headers}")
+
+        url = self.url
+        # NOTE: Intentionally ignore the `http_verb` here
+        # Slack APIs accepts any API method requests with POST methods
+        req = Request(method="POST", url=url, data=raw_body.encode("utf-8"), headers=headers)
+        resp = None
+        last_error = Exception("undefined internal error")
+
+        retry_state = RetryState()
+        counter_for_safety = 0
+        while counter_for_safety < 100:
+            counter_for_safety += 1
+            # If this is a retry, the next try started here. We can reset the flag.
+            retry_state.next_attempt_requested = False
+
+            try:
+                resp = self._perform_http_request_internal(url, req)
+                # The resp is a 200 OK response
+                return resp
+
+            except HTTPError as e:
+                # read the response body here
+                charset = e.headers.get_content_charset() or "utf-8"
+                response_body: str = e.read().decode(charset)
+                # As adding new values to HTTPError#headers can be ignored, building a new dict object here
+                response_headers = dict(e.headers.items())
+                resp = WebhookResponse(
+                    url=url,
+                    status_code=e.code,
+                    body=response_body,
+                    headers=response_headers,
+                )
+                if e.code == 429:
+                    # for backward-compatibility with WebClient (v.2.5.0 or older)
+                    if "retry-after" not in resp.headers and "Retry-After" in resp.headers:
+                        resp.headers["retry-after"] = resp.headers["Retry-After"]
+                    if "Retry-After" not in resp.headers and "retry-after" in resp.headers:
+                        resp.headers["Retry-After"] = resp.headers["retry-after"]
+                _debug_log_response(self.logger, resp)
+
+                # Try to find a retry handler for this error
+                retry_request = RetryHttpRequest.from_urllib_http_request(req)
+                retry_response = RetryHttpResponse(
+                    status_code=e.code,
+                    headers={k: [v] for k, v in e.headers.items()},
+                    data=response_body.encode("utf-8") if response_body is not None else None,
+                )
+                for handler in self.retry_handlers:
+                    if handler.can_retry(
+                        state=retry_state,
+                        request=retry_request,
+                        response=retry_response,
+                        error=e,
+                    ):
+                        if self.logger.level <= logging.DEBUG:
+                            self.logger.info(
+                                f"A retry handler found: {type(handler).__name__} for {req.method} {req.full_url} - {e}"
+                            )
+                        handler.prepare_for_next_attempt(
+                            state=retry_state,
+                            request=retry_request,
+                            response=retry_response,
+                            error=e,
+                        )
+                        break
+
+                if retry_state.next_attempt_requested is False:
+                    return resp
+
+            except Exception as err:
+                last_error = err
+                self.logger.error(f"Failed to send a request to Slack API server: {err}")
+
+                # Try to find a retry handler for this error
+                retry_request = RetryHttpRequest.from_urllib_http_request(req)
+                for handler in self.retry_handlers:
+                    if handler.can_retry(
+                        state=retry_state,
+                        request=retry_request,
+                        response=None,
+                        error=err,
+                    ):
+                        if self.logger.level <= logging.DEBUG:
+                            self.logger.info(
+                                f"A retry handler found: {type(handler).__name__} for {req.method} {req.full_url} - {err}"
+                            )
+                        handler.prepare_for_next_attempt(
+                            state=retry_state,
+                            request=retry_request,
+                            response=None,
+                            error=err,
+                        )
+                        self.logger.info(f"Going to retry the same request: {req.method} {req.full_url}")
+                        break
+
+                if retry_state.next_attempt_requested is False:
+                    raise err
+
+        if resp is not None:
+            return resp
+        raise last_error
+
+    def _perform_http_request_internal(self, url: str, req: Request):
+        opener: Optional[OpenerDirector] = None
+        # for security (BAN-B310)
+        if url.lower().startswith("http"):
+            if self.proxy is not None:
+                if isinstance(self.proxy, str):
+                    opener = urllib.request.build_opener(
+                        ProxyHandler({"http": self.proxy, "https": self.proxy}),
+                        HTTPSHandler(context=self.ssl),
+                    )
+                else:
+                    raise SlackRequestError(f"Invalid proxy detected: {self.proxy} must be a str value")
+        else:
+            raise SlackRequestError(f"Invalid URL detected: {url}")
+
+        http_resp: Optional[HTTPResponse] = None
+        if opener:
+            http_resp = opener.open(req, timeout=self.timeout)
+        else:
+            http_resp = urlopen(req, context=self.ssl, timeout=self.timeout)
+        charset: str = http_resp.headers.get_content_charset() or "utf-8"
+        response_body: str = http_resp.read().decode(charset)
+        resp = WebhookResponse(
+            url=url,
+            status_code=http_resp.status,
+            body=response_body,
+            headers=http_resp.headers,  # type: ignore[arg-type]
+        )
+        _debug_log_response(self.logger, resp)
+        return resp
+
+

API client for Incoming Webhooks and response_url

+

https://docs.slack.dev/messaging/sending-messages-using-incoming-webhooks/

+

Args

+
+
url
+
Complete URL to send data (e.g., https://hooks.slack.com/XXX)
+
timeout
+
Request timeout (in seconds)
+
ssl
+
ssl.SSLContext to use for requests
+
proxy
+
Proxy URL (e.g., localhost:9000, http://localhost:9000)
+
default_headers
+
Request headers to add to all requests
+
user_agent_prefix
+
Prefix for User-Agent header value
+
user_agent_suffix
+
Suffix for User-Agent header value
+
logger
+
Custom logger
+
retry_handlers
+
Retry handlers
+
+

Class variables

+
+
var default_headers : Dict[str, str]
+
+

The type of the None singleton.

+
+
var logger : logging.Logger
+
+

The type of the None singleton.

+
+
var proxy : str | None
+
+

The type of the None singleton.

+
+
var retry_handlers : List[RetryHandler]
+
+

The type of the None singleton.

+
+
var ssl : ssl.SSLContext | None
+
+

The type of the None singleton.

+
+
var timeout : int
+
+

The type of the None singleton.

+
+
var url : str
+
+

The type of the None singleton.

+
+
+

Methods

+
+
+def send(self,
*,
text: str | None = None,
attachments: Sequence[Dict[str, Any] | Attachment] | None = None,
blocks: Sequence[Dict[str, Any] | Block] | None = None,
response_type: str | None = None,
replace_original: bool | None = None,
delete_original: bool | None = None,
unfurl_links: bool | None = None,
unfurl_media: bool | None = None,
metadata: Dict[str, Any] | None = None,
headers: Dict[str, str] | None = None) ‑> WebhookResponse
+
+
+
+ +Expand source code + +
def send(
+    self,
+    *,
+    text: Optional[str] = None,
+    attachments: Optional[Sequence[Union[Dict[str, Any], Attachment]]] = None,
+    blocks: Optional[Sequence[Union[Dict[str, Any], Block]]] = None,
+    response_type: Optional[str] = None,
+    replace_original: Optional[bool] = None,
+    delete_original: Optional[bool] = None,
+    unfurl_links: Optional[bool] = None,
+    unfurl_media: Optional[bool] = None,
+    metadata: Optional[Dict[str, Any]] = None,
+    headers: Optional[Dict[str, str]] = None,
+) -> WebhookResponse:
+    """Performs a Slack API request and returns the result.
+
+    Args:
+        text: The text message
+            (even when having blocks, setting this as well is recommended as it works as fallback)
+        attachments: A collection of attachments
+        blocks: A collection of Block Kit UI components
+        response_type: The type of message (either 'in_channel' or 'ephemeral')
+        replace_original: True if you use this option for response_url requests
+        delete_original: True if you use this option for response_url requests
+        unfurl_links: Option to indicate whether text url should unfurl
+        unfurl_media: Option to indicate whether media url should unfurl
+        metadata: Metadata attached to the message
+        headers: Request headers to append only for this request
+
+    Returns:
+        Webhook response
+    """
+    return self.send_dict(
+        # It's fine to have None value elements here
+        # because _build_body() filters them out when constructing the actual body data
+        body={
+            "text": text,
+            "attachments": attachments,
+            "blocks": blocks,
+            "response_type": response_type,
+            "replace_original": replace_original,
+            "delete_original": delete_original,
+            "unfurl_links": unfurl_links,
+            "unfurl_media": unfurl_media,
+            "metadata": metadata,
+        },
+        headers=headers,
+    )
+
+

Performs a Slack API request and returns the result.

+

Args

+
+
text
+
The text message +(even when having blocks, setting this as well is recommended as it works as fallback)
+
attachments
+
A collection of attachments
+
blocks
+
A collection of Block Kit UI components
+
response_type
+
The type of message (either 'in_channel' or 'ephemeral')
+
replace_original
+
True if you use this option for response_url requests
+
delete_original
+
True if you use this option for response_url requests
+
unfurl_links
+
Option to indicate whether text url should unfurl
+
unfurl_media
+
Option to indicate whether media url should unfurl
+
metadata
+
Metadata attached to the message
+
headers
+
Request headers to append only for this request
+
+

Returns

+

Webhook response

+
+
+def send_dict(self, body: Dict[str, Any], headers: Dict[str, str] | None = None) ‑> WebhookResponse +
+
+
+ +Expand source code + +
def send_dict(self, body: Dict[str, Any], headers: Optional[Dict[str, str]] = None) -> WebhookResponse:
+    """Performs a Slack API request and returns the result.
+
+    Args:
+        body: JSON data structure (it's still a dict at this point),
+            if you give this argument, body_params and files will be skipped
+        headers: Request headers to append only for this request
+    Returns:
+        Webhook response
+    """
+    return self._perform_http_request(
+        body=_build_body(body),  # type: ignore[arg-type]
+        headers=_build_request_headers(self.default_headers, headers),
+    )
+
+

Performs a Slack API request and returns the result.

+

Args

+
+
body
+
JSON data structure (it's still a dict at this point), +if you give this argument, body_params and files will be skipped
+
headers
+
Request headers to append only for this request
+
+

Returns

+

Webhook response

+
+
+
+
+
+
+ +
+ + + diff --git a/docs/reference/webhook/index.html b/docs/reference/webhook/index.html new file mode 100644 index 000000000..51c387ef4 --- /dev/null +++ b/docs/reference/webhook/index.html @@ -0,0 +1,585 @@ + + + + + + +slack_sdk.webhook API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.webhook

+
+
+

You can use slack_sdk.webhook.WebhookClient for Incoming Webhooks +and message responses using response_url in payloads.

+
+
+

Sub-modules

+
+
slack_sdk.webhook.async_client
+
+
+
+
slack_sdk.webhook.client
+
+
+
+
slack_sdk.webhook.internal_utils
+
+
+
+
slack_sdk.webhook.webhook_response
+
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class WebhookClient +(url: str,
timeout: int = 30,
ssl: ssl.SSLContext | None = None,
proxy: str | None = None,
default_headers: Dict[str, str] | None = None,
user_agent_prefix: str | None = None,
user_agent_suffix: str | None = None,
logger: logging.Logger | None = None,
retry_handlers: List[RetryHandler] | None = None)
+
+
+
+ +Expand source code + +
class WebhookClient:
+    url: str
+    timeout: int
+    ssl: Optional[SSLContext]
+    proxy: Optional[str]
+    default_headers: Dict[str, str]
+    logger: logging.Logger
+    retry_handlers: List[RetryHandler]
+
+    def __init__(
+        self,
+        url: str,
+        timeout: int = 30,
+        ssl: Optional[SSLContext] = None,
+        proxy: Optional[str] = None,
+        default_headers: Optional[Dict[str, str]] = None,
+        user_agent_prefix: Optional[str] = None,
+        user_agent_suffix: Optional[str] = None,
+        logger: Optional[logging.Logger] = None,
+        retry_handlers: Optional[List[RetryHandler]] = None,
+    ):
+        """API client for Incoming Webhooks and `response_url`
+
+        https://docs.slack.dev/messaging/sending-messages-using-incoming-webhooks/
+
+        Args:
+            url: Complete URL to send data (e.g., `https://hooks.slack.com/XXX`)
+            timeout: Request timeout (in seconds)
+            ssl: `ssl.SSLContext` to use for requests
+            proxy: Proxy URL (e.g., `localhost:9000`, `http://localhost:9000`)
+            default_headers: Request headers to add to all requests
+            user_agent_prefix: Prefix for User-Agent header value
+            user_agent_suffix: Suffix for User-Agent header value
+            logger: Custom logger
+            retry_handlers: Retry handlers
+        """
+        self.url = url
+        self.timeout = timeout
+        self.ssl = ssl
+        self.proxy = proxy
+        self.default_headers = default_headers if default_headers else {}
+        self.default_headers["User-Agent"] = get_user_agent(user_agent_prefix, user_agent_suffix)
+        self.logger = logger if logger is not None else logging.getLogger(__name__)
+        self.retry_handlers = retry_handlers if retry_handlers is not None else default_retry_handlers()
+
+        if self.proxy is None or len(self.proxy.strip()) == 0:
+            env_variable = load_http_proxy_from_env(self.logger)
+            if env_variable is not None:
+                self.proxy = env_variable
+
+    def send(
+        self,
+        *,
+        text: Optional[str] = None,
+        attachments: Optional[Sequence[Union[Dict[str, Any], Attachment]]] = None,
+        blocks: Optional[Sequence[Union[Dict[str, Any], Block]]] = None,
+        response_type: Optional[str] = None,
+        replace_original: Optional[bool] = None,
+        delete_original: Optional[bool] = None,
+        unfurl_links: Optional[bool] = None,
+        unfurl_media: Optional[bool] = None,
+        metadata: Optional[Dict[str, Any]] = None,
+        headers: Optional[Dict[str, str]] = None,
+    ) -> WebhookResponse:
+        """Performs a Slack API request and returns the result.
+
+        Args:
+            text: The text message
+                (even when having blocks, setting this as well is recommended as it works as fallback)
+            attachments: A collection of attachments
+            blocks: A collection of Block Kit UI components
+            response_type: The type of message (either 'in_channel' or 'ephemeral')
+            replace_original: True if you use this option for response_url requests
+            delete_original: True if you use this option for response_url requests
+            unfurl_links: Option to indicate whether text url should unfurl
+            unfurl_media: Option to indicate whether media url should unfurl
+            metadata: Metadata attached to the message
+            headers: Request headers to append only for this request
+
+        Returns:
+            Webhook response
+        """
+        return self.send_dict(
+            # It's fine to have None value elements here
+            # because _build_body() filters them out when constructing the actual body data
+            body={
+                "text": text,
+                "attachments": attachments,
+                "blocks": blocks,
+                "response_type": response_type,
+                "replace_original": replace_original,
+                "delete_original": delete_original,
+                "unfurl_links": unfurl_links,
+                "unfurl_media": unfurl_media,
+                "metadata": metadata,
+            },
+            headers=headers,
+        )
+
+    def send_dict(self, body: Dict[str, Any], headers: Optional[Dict[str, str]] = None) -> WebhookResponse:
+        """Performs a Slack API request and returns the result.
+
+        Args:
+            body: JSON data structure (it's still a dict at this point),
+                if you give this argument, body_params and files will be skipped
+            headers: Request headers to append only for this request
+        Returns:
+            Webhook response
+        """
+        return self._perform_http_request(
+            body=_build_body(body),  # type: ignore[arg-type]
+            headers=_build_request_headers(self.default_headers, headers),
+        )
+
+    def _perform_http_request(self, *, body: Dict[str, Any], headers: Dict[str, str]) -> WebhookResponse:
+        raw_body = json.dumps(body)
+        headers["Content-Type"] = "application/json;charset=utf-8"
+
+        if self.logger.level <= logging.DEBUG:
+            self.logger.debug(f"Sending a request - url: {self.url}, body: {raw_body}, headers: {headers}")
+
+        url = self.url
+        # NOTE: Intentionally ignore the `http_verb` here
+        # Slack APIs accepts any API method requests with POST methods
+        req = Request(method="POST", url=url, data=raw_body.encode("utf-8"), headers=headers)
+        resp = None
+        last_error = Exception("undefined internal error")
+
+        retry_state = RetryState()
+        counter_for_safety = 0
+        while counter_for_safety < 100:
+            counter_for_safety += 1
+            # If this is a retry, the next try started here. We can reset the flag.
+            retry_state.next_attempt_requested = False
+
+            try:
+                resp = self._perform_http_request_internal(url, req)
+                # The resp is a 200 OK response
+                return resp
+
+            except HTTPError as e:
+                # read the response body here
+                charset = e.headers.get_content_charset() or "utf-8"
+                response_body: str = e.read().decode(charset)
+                # As adding new values to HTTPError#headers can be ignored, building a new dict object here
+                response_headers = dict(e.headers.items())
+                resp = WebhookResponse(
+                    url=url,
+                    status_code=e.code,
+                    body=response_body,
+                    headers=response_headers,
+                )
+                if e.code == 429:
+                    # for backward-compatibility with WebClient (v.2.5.0 or older)
+                    if "retry-after" not in resp.headers and "Retry-After" in resp.headers:
+                        resp.headers["retry-after"] = resp.headers["Retry-After"]
+                    if "Retry-After" not in resp.headers and "retry-after" in resp.headers:
+                        resp.headers["Retry-After"] = resp.headers["retry-after"]
+                _debug_log_response(self.logger, resp)
+
+                # Try to find a retry handler for this error
+                retry_request = RetryHttpRequest.from_urllib_http_request(req)
+                retry_response = RetryHttpResponse(
+                    status_code=e.code,
+                    headers={k: [v] for k, v in e.headers.items()},
+                    data=response_body.encode("utf-8") if response_body is not None else None,
+                )
+                for handler in self.retry_handlers:
+                    if handler.can_retry(
+                        state=retry_state,
+                        request=retry_request,
+                        response=retry_response,
+                        error=e,
+                    ):
+                        if self.logger.level <= logging.DEBUG:
+                            self.logger.info(
+                                f"A retry handler found: {type(handler).__name__} for {req.method} {req.full_url} - {e}"
+                            )
+                        handler.prepare_for_next_attempt(
+                            state=retry_state,
+                            request=retry_request,
+                            response=retry_response,
+                            error=e,
+                        )
+                        break
+
+                if retry_state.next_attempt_requested is False:
+                    return resp
+
+            except Exception as err:
+                last_error = err
+                self.logger.error(f"Failed to send a request to Slack API server: {err}")
+
+                # Try to find a retry handler for this error
+                retry_request = RetryHttpRequest.from_urllib_http_request(req)
+                for handler in self.retry_handlers:
+                    if handler.can_retry(
+                        state=retry_state,
+                        request=retry_request,
+                        response=None,
+                        error=err,
+                    ):
+                        if self.logger.level <= logging.DEBUG:
+                            self.logger.info(
+                                f"A retry handler found: {type(handler).__name__} for {req.method} {req.full_url} - {err}"
+                            )
+                        handler.prepare_for_next_attempt(
+                            state=retry_state,
+                            request=retry_request,
+                            response=None,
+                            error=err,
+                        )
+                        self.logger.info(f"Going to retry the same request: {req.method} {req.full_url}")
+                        break
+
+                if retry_state.next_attempt_requested is False:
+                    raise err
+
+        if resp is not None:
+            return resp
+        raise last_error
+
+    def _perform_http_request_internal(self, url: str, req: Request):
+        opener: Optional[OpenerDirector] = None
+        # for security (BAN-B310)
+        if url.lower().startswith("http"):
+            if self.proxy is not None:
+                if isinstance(self.proxy, str):
+                    opener = urllib.request.build_opener(
+                        ProxyHandler({"http": self.proxy, "https": self.proxy}),
+                        HTTPSHandler(context=self.ssl),
+                    )
+                else:
+                    raise SlackRequestError(f"Invalid proxy detected: {self.proxy} must be a str value")
+        else:
+            raise SlackRequestError(f"Invalid URL detected: {url}")
+
+        http_resp: Optional[HTTPResponse] = None
+        if opener:
+            http_resp = opener.open(req, timeout=self.timeout)
+        else:
+            http_resp = urlopen(req, context=self.ssl, timeout=self.timeout)
+        charset: str = http_resp.headers.get_content_charset() or "utf-8"
+        response_body: str = http_resp.read().decode(charset)
+        resp = WebhookResponse(
+            url=url,
+            status_code=http_resp.status,
+            body=response_body,
+            headers=http_resp.headers,  # type: ignore[arg-type]
+        )
+        _debug_log_response(self.logger, resp)
+        return resp
+
+

API client for Incoming Webhooks and response_url

+

https://docs.slack.dev/messaging/sending-messages-using-incoming-webhooks/

+

Args

+
+
url
+
Complete URL to send data (e.g., https://hooks.slack.com/XXX)
+
timeout
+
Request timeout (in seconds)
+
ssl
+
ssl.SSLContext to use for requests
+
proxy
+
Proxy URL (e.g., localhost:9000, http://localhost:9000)
+
default_headers
+
Request headers to add to all requests
+
user_agent_prefix
+
Prefix for User-Agent header value
+
user_agent_suffix
+
Suffix for User-Agent header value
+
logger
+
Custom logger
+
retry_handlers
+
Retry handlers
+
+

Class variables

+
+
var default_headers : Dict[str, str]
+
+

The type of the None singleton.

+
+
var logger : logging.Logger
+
+

The type of the None singleton.

+
+
var proxy : str | None
+
+

The type of the None singleton.

+
+
var retry_handlers : List[RetryHandler]
+
+

The type of the None singleton.

+
+
var ssl : ssl.SSLContext | None
+
+

The type of the None singleton.

+
+
var timeout : int
+
+

The type of the None singleton.

+
+
var url : str
+
+

The type of the None singleton.

+
+
+

Methods

+
+
+def send(self,
*,
text: str | None = None,
attachments: Sequence[Dict[str, Any] | Attachment] | None = None,
blocks: Sequence[Dict[str, Any] | Block] | None = None,
response_type: str | None = None,
replace_original: bool | None = None,
delete_original: bool | None = None,
unfurl_links: bool | None = None,
unfurl_media: bool | None = None,
metadata: Dict[str, Any] | None = None,
headers: Dict[str, str] | None = None) ‑> WebhookResponse
+
+
+
+ +Expand source code + +
def send(
+    self,
+    *,
+    text: Optional[str] = None,
+    attachments: Optional[Sequence[Union[Dict[str, Any], Attachment]]] = None,
+    blocks: Optional[Sequence[Union[Dict[str, Any], Block]]] = None,
+    response_type: Optional[str] = None,
+    replace_original: Optional[bool] = None,
+    delete_original: Optional[bool] = None,
+    unfurl_links: Optional[bool] = None,
+    unfurl_media: Optional[bool] = None,
+    metadata: Optional[Dict[str, Any]] = None,
+    headers: Optional[Dict[str, str]] = None,
+) -> WebhookResponse:
+    """Performs a Slack API request and returns the result.
+
+    Args:
+        text: The text message
+            (even when having blocks, setting this as well is recommended as it works as fallback)
+        attachments: A collection of attachments
+        blocks: A collection of Block Kit UI components
+        response_type: The type of message (either 'in_channel' or 'ephemeral')
+        replace_original: True if you use this option for response_url requests
+        delete_original: True if you use this option for response_url requests
+        unfurl_links: Option to indicate whether text url should unfurl
+        unfurl_media: Option to indicate whether media url should unfurl
+        metadata: Metadata attached to the message
+        headers: Request headers to append only for this request
+
+    Returns:
+        Webhook response
+    """
+    return self.send_dict(
+        # It's fine to have None value elements here
+        # because _build_body() filters them out when constructing the actual body data
+        body={
+            "text": text,
+            "attachments": attachments,
+            "blocks": blocks,
+            "response_type": response_type,
+            "replace_original": replace_original,
+            "delete_original": delete_original,
+            "unfurl_links": unfurl_links,
+            "unfurl_media": unfurl_media,
+            "metadata": metadata,
+        },
+        headers=headers,
+    )
+
+

Performs a Slack API request and returns the result.

+

Args

+
+
text
+
The text message +(even when having blocks, setting this as well is recommended as it works as fallback)
+
attachments
+
A collection of attachments
+
blocks
+
A collection of Block Kit UI components
+
response_type
+
The type of message (either 'in_channel' or 'ephemeral')
+
replace_original
+
True if you use this option for response_url requests
+
delete_original
+
True if you use this option for response_url requests
+
unfurl_links
+
Option to indicate whether text url should unfurl
+
unfurl_media
+
Option to indicate whether media url should unfurl
+
metadata
+
Metadata attached to the message
+
headers
+
Request headers to append only for this request
+
+

Returns

+

Webhook response

+
+
+def send_dict(self, body: Dict[str, Any], headers: Dict[str, str] | None = None) ‑> WebhookResponse +
+
+
+ +Expand source code + +
def send_dict(self, body: Dict[str, Any], headers: Optional[Dict[str, str]] = None) -> WebhookResponse:
+    """Performs a Slack API request and returns the result.
+
+    Args:
+        body: JSON data structure (it's still a dict at this point),
+            if you give this argument, body_params and files will be skipped
+        headers: Request headers to append only for this request
+    Returns:
+        Webhook response
+    """
+    return self._perform_http_request(
+        body=_build_body(body),  # type: ignore[arg-type]
+        headers=_build_request_headers(self.default_headers, headers),
+    )
+
+

Performs a Slack API request and returns the result.

+

Args

+
+
body
+
JSON data structure (it's still a dict at this point), +if you give this argument, body_params and files will be skipped
+
headers
+
Request headers to append only for this request
+
+

Returns

+

Webhook response

+
+
+
+
+class WebhookResponse +(*, url: str, status_code: int, body: str, headers: Dict[str, Any]) +
+
+
+ +Expand source code + +
class WebhookResponse:
+    def __init__(
+        self,
+        *,
+        url: str,
+        status_code: int,
+        body: str,
+        headers: Dict[str, Any],
+    ):
+        self.api_url = url
+        self.status_code = status_code
+        self.body = body
+        self.headers = headers
+
+
+
+
+
+
+ +
+ + + diff --git a/docs/reference/webhook/internal_utils.html b/docs/reference/webhook/internal_utils.html new file mode 100644 index 000000000..118a7100f --- /dev/null +++ b/docs/reference/webhook/internal_utils.html @@ -0,0 +1,66 @@ + + + + + + +slack_sdk.webhook.internal_utils API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.webhook.internal_utils

+
+
+
+
+
+
+
+
+
+
+
+
+ +
+ + + diff --git a/docs/reference/webhook/webhook_response.html b/docs/reference/webhook/webhook_response.html new file mode 100644 index 000000000..f0c372b97 --- /dev/null +++ b/docs/reference/webhook/webhook_response.html @@ -0,0 +1,101 @@ + + + + + + +slack_sdk.webhook.webhook_response API documentation + + + + + + + + + + + +
+
+
+

Module slack_sdk.webhook.webhook_response

+
+
+
+
+
+
+
+
+
+
+

Classes

+
+
+class WebhookResponse +(*, url: str, status_code: int, body: str, headers: Dict[str, Any]) +
+
+
+ +Expand source code + +
class WebhookResponse:
+    def __init__(
+        self,
+        *,
+        url: str,
+        status_code: int,
+        body: str,
+        headers: Dict[str, Any],
+    ):
+        self.api_url = url
+        self.status_code = status_code
+        self.body = body
+        self.headers = headers
+
+
+
+
+
+
+ +
+ + + diff --git a/integration_tests/audit_logs/test_async_client.py b/integration_tests/audit_logs/test_async_client.py new file mode 100644 index 000000000..aae4888e3 --- /dev/null +++ b/integration_tests/audit_logs/test_async_client.py @@ -0,0 +1,44 @@ +import os +import unittest + +from integration_tests.env_variable_names import ( + SLACK_SDK_TEST_GRID_ORG_ADMIN_USER_TOKEN, +) +from integration_tests.helpers import async_test +from slack_sdk.audit_logs.async_client import AsyncAuditLogsClient + + +class TestAuditLogsClient(unittest.TestCase): + def setUp(self): + self.client = AsyncAuditLogsClient(token=os.environ[SLACK_SDK_TEST_GRID_ORG_ADMIN_USER_TOKEN]) + + def tearDown(self): + pass + + @async_test + async def test_api_call(self): + api_response = await self.client.api_call(path="schemas") + self.assertEqual(200, api_response.status_code) + self.assertTrue(api_response.raw_body.startswith("""{"schemas":[{""")) + self.assertIsNotNone(api_response.body.get("schemas")) + + @async_test + async def test_schemas(self): + api_response = await self.client.schemas() + self.assertEqual(200, api_response.status_code) + self.assertTrue(api_response.raw_body.startswith("""{"schemas":[{""")) + self.assertIsNotNone(api_response.body.get("schemas")) + + @async_test + async def test_actions(self): + api_response = await self.client.actions() + self.assertEqual(200, api_response.status_code) + self.assertTrue(api_response.raw_body.startswith("""{"actions":{""")) + self.assertIsNotNone(api_response.body.get("actions")) + + @async_test + async def test_logs(self): + api_response = await self.client.logs(action="user_login", limit=1) + self.assertEqual(200, api_response.status_code) + self.assertTrue(api_response.raw_body.startswith("""{"entries":[{""")) + self.assertIsNotNone(api_response.body.get("entries")) diff --git a/integration_tests/audit_logs/test_client.py b/integration_tests/audit_logs/test_client.py new file mode 100644 index 000000000..fa9966039 --- /dev/null +++ b/integration_tests/audit_logs/test_client.py @@ -0,0 +1,39 @@ +import os +import unittest + +from integration_tests.env_variable_names import ( + SLACK_SDK_TEST_GRID_ORG_ADMIN_USER_TOKEN, +) +from slack_sdk.audit_logs import AuditLogsClient + + +class TestAuditLogsClient(unittest.TestCase): + def setUp(self): + self.client = AuditLogsClient(token=os.environ[SLACK_SDK_TEST_GRID_ORG_ADMIN_USER_TOKEN]) + + def tearDown(self): + pass + + def test_api_call(self): + api_response = self.client.api_call(path="schemas") + self.assertEqual(200, api_response.status_code) + self.assertTrue(api_response.raw_body.startswith("""{"schemas":[{""")) + self.assertIsNotNone(api_response.body.get("schemas")) + + def test_schemas(self): + api_response = self.client.schemas() + self.assertEqual(200, api_response.status_code) + self.assertTrue(api_response.raw_body.startswith("""{"schemas":[{""")) + self.assertIsNotNone(api_response.body.get("schemas")) + + def test_actions(self): + api_response = self.client.actions() + self.assertEqual(200, api_response.status_code) + self.assertTrue(api_response.raw_body.startswith("""{"actions":{""")) + self.assertIsNotNone(api_response.body.get("actions")) + + def test_logs(self): + api_response = self.client.logs(action="user_login", limit=1) + self.assertEqual(200, api_response.status_code) + self.assertTrue(api_response.raw_body.startswith("""{"entries":[{""")) + self.assertIsNotNone(api_response.body.get("entries")) diff --git a/integration_tests/audit_logs/test_pagination.py b/integration_tests/audit_logs/test_pagination.py new file mode 100644 index 000000000..cdee8b178 --- /dev/null +++ b/integration_tests/audit_logs/test_pagination.py @@ -0,0 +1,27 @@ +import logging +import os +import unittest + +from integration_tests.env_variable_names import ( + SLACK_SDK_TEST_GRID_ORG_ADMIN_USER_TOKEN, +) +from slack_sdk.audit_logs import AuditLogsClient + + +class TestAuditLogsClient(unittest.TestCase): + def setUp(self): + self.client = AuditLogsClient(token=os.environ[SLACK_SDK_TEST_GRID_ORG_ADMIN_USER_TOKEN]) + + def tearDown(self): + pass + + def test_pagination(self): + call_count = 0 + response = None + ids = [] + while call_count < 10 and (response is None or response.status_code != 429): + cursor = response.body["response_metadata"]["next_cursor"] if response is not None else None + response = self.client.logs(action="user_login", limit=1, cursor=cursor) + ids += map(lambda v: v["id"], response.body.get("entries", [])) + call_count += 1 + self.assertGreaterEqual(len(set(ids)), 10) diff --git a/integration_tests/env_variable_names.py b/integration_tests/env_variable_names.py new file mode 100644 index 000000000..47deb1038 --- /dev/null +++ b/integration_tests/env_variable_names.py @@ -0,0 +1,39 @@ +# Tokens for tests +# * Create a Slack app from https://api.slack.com/apps +# * Add all the bot scopes, user scopes +# * Install the app to your development workspace +SLACK_SDK_TEST_BOT_TOKEN = "SLACK_SDK_TEST_BOT_TOKEN" +SLACK_SDK_TEST_USER_TOKEN = "SLACK_SDK_TEST_USER_TOKEN" + +# Bot token for RTMClient tests +# * Create a classic app from https://api.slack.com/apps?new_classic_app=1 +# * Add `bot` scope +# * Install the app to your development workspace +SLACK_SDK_TEST_CLASSIC_APP_BOT_TOKEN = "SLACK_SDK_TEST_CLASSIC_APP_BOT_TOKEN" +SLACK_SDK_TEST_RTM_TEST_CHANNEL_ID = "SLACK_SDK_TEST_RTM_TEST_CHANNEL_ID" + +# Bot token for WebClient tests +SLACK_SDK_TEST_WEB_TEST_CHANNEL_ID = "SLACK_SDK_TEST_WEB_TEST_CHANNEL_ID" +SLACK_SDK_TEST_WEB_TEST_USER_ID = "SLACK_SDK_TEST_WEB_TEST_USER_ID" + +# Testing with Grid workspaces +SLACK_SDK_TEST_GRID_ORG_ADMIN_USER_TOKEN = "SLACK_SDK_TEST_GRID_ORG_ADMIN_USER_TOKEN" +SLACK_SDK_TEST_GRID_WORKSPACE_ADMIN_USER_TOKEN = "SLACK_SDK_TEST_GRID_WORKSPACE_ADMIN_USER_TOKEN" +SLACK_SDK_TEST_GRID_WORKSPACE_USER_TOKEN = "SLACK_SDK_TEST_GRID_WORKSPACE_USER_TOKEN" +SLACK_SDK_TEST_GRID_WORKSPACE_BOT_TOKEN = "SLACK_SDK_TEST_GRID_WORKSPACE_BOT_TOKEN" +SLACK_SDK_TEST_GRID_IDP_USERGROUP_ID = "SLACK_SDK_TEST_GRID_IDP_USERGROUP_ID" +SLACK_SDK_TEST_GRID_IDP_USERGROUP_ID_2 = "SLACK_SDK_TEST_GRID_IDP_USERGROUP_ID_2" +SLACK_SDK_TEST_GRID_TEAM_ID = "SLACK_SDK_TEST_GRID_TEAM_ID" +SLACK_SDK_TEST_GRID_TEAM_ID_2 = "SLACK_SDK_TEST_GRID_TEAM_ID_2" +SLACK_SDK_TEST_GRID_USER_ID = "SLACK_SDK_TEST_GRID_USER_ID" +# The following user must be a full member, who is not a primary owner +SLACK_SDK_TEST_GRID_USER_ID_ADMIN_AUTH = "SLACK_SDK_TEST_GRID_USER_ID_ADMIN_AUTH" + +# Webhook +SLACK_SDK_TEST_INCOMING_WEBHOOK_URL = "SLACK_SDK_TEST_INCOMING_WEBHOOK_URL" +SLACK_SDK_TEST_INCOMING_WEBHOOK_CHANNEL_NAME = "SLACK_SDK_TEST_INCOMING_WEBHOOK_CHANNEL_NAME" + +# For Slack Connect shared tests +SLACK_SDK_TEST_CONNECT_INVITE_SENDER_BOT_TOKEN = "SLACK_SDK_TEST_CONNECT_INVITE_SENDER_BOT_TOKEN" +SLACK_SDK_TEST_CONNECT_INVITE_RECEIVER_BOT_TOKEN = "SLACK_SDK_TEST_CONNECT_INVITE_RECEIVER_BOT_TOKEN" +SLACK_SDK_TEST_CONNECT_INVITE_RECEIVER_BOT_USER_ID = "SLACK_SDK_TEST_CONNECT_INVITE_RECEIVER_BOT_USER_ID" diff --git a/integration_tests/helpers.py b/integration_tests/helpers.py new file mode 100644 index 000000000..983df01b5 --- /dev/null +++ b/integration_tests/helpers.py @@ -0,0 +1,26 @@ +import asyncio +import inspect +import sys +from asyncio.events import AbstractEventLoop + + +def async_test(coro): + loop: AbstractEventLoop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + + def wrapper(*args, **kwargs): + current_loop: AbstractEventLoop = asyncio.get_event_loop() + return current_loop.run_until_complete(coro(*args, **kwargs)) + + return wrapper + + +def is_not_specified() -> bool: + # get the caller's filepath + frame = inspect.stack()[1] + module = inspect.getmodule(frame[0]) + filepath: str = module.__file__ + + # ./scripts/run_integration_tests.sh web/test_issue_560.py + test_target: str = sys.argv[1] # e.g., web/test_issue_560.py + return not test_target or not filepath.endswith(test_target) diff --git a/integration_tests/rtm/test_issue_530.py b/integration_tests/rtm/test_issue_530.py new file mode 100644 index 000000000..cd992b7c3 --- /dev/null +++ b/integration_tests/rtm/test_issue_530.py @@ -0,0 +1,55 @@ +import asyncio +import collections +import logging +import unittest + +from integration_tests.helpers import async_test +from slack_sdk.rtm import RTMClient + + +class TestRTMClient(unittest.TestCase): + """Runs integration tests with real Slack API + + https://github.com/slackapi/python-slack-sdk/issues/530 + """ + + def setUp(self): + self.logger = logging.getLogger(__name__) + + def tearDown(self): + # Reset the decorators by @RTMClient.run_on + RTMClient._callbacks = collections.defaultdict(list) + + def test_issue_530(self): + try: + rtm_client = RTMClient(token="I am not a token", run_async=False, loop=asyncio.new_event_loop()) + rtm_client.start() + self.fail("Raising an error here was expected") + except Exception as e: + self.assertEqual( + "The request to the Slack API failed.\n" "The server responded with: {'ok': False, 'error': 'invalid_auth'}", + str(e), + ) + finally: + if not rtm_client._stopped: + rtm_client.stop() + + @async_test + async def test_issue_530_async(self): + try: + rtm_client = RTMClient(token="I am not a token", run_async=True) + await rtm_client.start() + self.fail("Raising an error here was expected") + except Exception as e: + self.assertEqual( + "The request to the Slack API failed.\n" "The server responded with: {'ok': False, 'error': 'invalid_auth'}", + str(e), + ) + finally: + if not rtm_client._stopped: + rtm_client.stop() + + # =============================================================================================== short test summary info =============================================================================================== + # FAILED integration_tests/rtm/test_issue_530.py::TestRTMClient::test_issue_530 - AssertionError: "'NoneType' object is not subscriptable" != "The server responded with: {'ok': False, 'error': 'invalid_auth'}" + # FAILED integration_tests/rtm/test_issue_530.py::TestRTMClient::test_issue_530_async - AssertionError: "'NoneType' object is not subscriptable" != "The server responded with: {'ok': False, 'error': 'invalid_auth'}" + # ====================================================================================== 2 failed, 1 skipped, 5 warnings in 1.54s ======================================================================================= diff --git a/integration_tests/rtm/test_issue_558.py b/integration_tests/rtm/test_issue_558.py new file mode 100644 index 000000000..0e18bb48d --- /dev/null +++ b/integration_tests/rtm/test_issue_558.py @@ -0,0 +1,81 @@ +import asyncio +import collections +import logging +import os +import unittest + +import pytest + +from integration_tests.env_variable_names import ( + SLACK_SDK_TEST_CLASSIC_APP_BOT_TOKEN, + SLACK_SDK_TEST_RTM_TEST_CHANNEL_ID, +) +from integration_tests.helpers import async_test, is_not_specified +from slack_sdk.rtm import RTMClient +from slack_sdk.web import WebClient + + +class TestRTMClient(unittest.TestCase): + """Runs integration tests with real Slack API + + https://github.com/slackapi/python-slack-sdk/issues/558 + """ + + def setUp(self): + self.logger = logging.getLogger(__name__) + self.bot_token = os.environ[SLACK_SDK_TEST_CLASSIC_APP_BOT_TOKEN] + + def tearDown(self): + # Reset the decorators by @RTMClient.run_on + RTMClient._callbacks = collections.defaultdict(list) + + @pytest.mark.skipif(condition=is_not_specified(), reason="Still unfixed") + @async_test + async def test_issue_558(self): + channel_id = os.environ[SLACK_SDK_TEST_RTM_TEST_CHANNEL_ID] + text = "This message was sent by ! (test_issue_558)" + + self.message_count, self.reaction_count = 0, 0 + + async def process_messages(**payload): + self.logger.debug(payload) + self.message_count += 1 + await asyncio.sleep(10) # this used to block all other handlers + + async def process_reactions(**payload): + self.logger.debug(payload) + self.reaction_count += 1 + + rtm = RTMClient(token=self.bot_token, run_async=True) + RTMClient.on(event="message", callback=process_messages) + RTMClient.on(event="reaction_added", callback=process_reactions) + + web_client = WebClient(token=self.bot_token, run_async=True) + message = await web_client.chat_postMessage(channel=channel_id, text=text) + self.assertFalse("error" in message) + ts = message["ts"] + await asyncio.sleep(3) + + # intentionally not waiting here + rtm.start() + await asyncio.sleep(3) + + try: + first_reaction = await web_client.reactions_add(channel=channel_id, timestamp=ts, name="eyes") + self.assertFalse("error" in first_reaction) + await asyncio.sleep(2) + + message = await web_client.chat_postMessage(channel=channel_id, text=text) + self.assertFalse("error" in message) + # used to start blocking here + + # This reaction_add event won't be handled due to a bug + second_reaction = await web_client.reactions_add(channel=channel_id, timestamp=ts, name="tada") + self.assertFalse("error" in second_reaction) + await asyncio.sleep(2) + + self.assertEqual(self.message_count, 1) + self.assertEqual(self.reaction_count, 2) # used to fail + finally: + if not rtm._stopped: + rtm.stop() diff --git a/integration_tests/rtm/test_issue_569.py b/integration_tests/rtm/test_issue_569.py new file mode 100644 index 000000000..f2c1a6bd6 --- /dev/null +++ b/integration_tests/rtm/test_issue_569.py @@ -0,0 +1,140 @@ +import asyncio +import collections +import logging +import os +import threading +import time +import unittest + +import psutil +import pytest + +from integration_tests.env_variable_names import ( + SLACK_SDK_TEST_CLASSIC_APP_BOT_TOKEN, + SLACK_SDK_TEST_RTM_TEST_CHANNEL_ID, +) +from integration_tests.helpers import async_test, is_not_specified +from slack_sdk.rtm import RTMClient +from slack_sdk.web import WebClient +from slack_sdk.web.legacy_client import LegacyWebClient + + +class TestRTMClient(unittest.TestCase): + """Runs integration tests with real Slack API + + https://github.com/slackapi/python-slack-sdk/issues/569 + """ + + def setUp(self): + if not hasattr(self, "logger"): + self.logger = logging.getLogger(__name__) + self.channel_id = os.environ[SLACK_SDK_TEST_RTM_TEST_CHANNEL_ID] + self.bot_token = os.environ[SLACK_SDK_TEST_CLASSIC_APP_BOT_TOKEN] + + if not hasattr(self, "cpu_monitor") or not TestRTMClient.cpu_monitor.is_alive(): + + def run_cpu_monitor(self): + self.logger.debug("Starting CPU monitor in another thread...") + TestRTMClient.cpu_usage = 0 + while True: + p = psutil.Process(os.getpid()) + current_cpu_usage: float = p.cpu_percent(interval=0.5) + self.logger.debug(current_cpu_usage) + if current_cpu_usage > TestRTMClient.cpu_usage: + TestRTMClient.cpu_usage = current_cpu_usage + + TestRTMClient.cpu_monitor = threading.Thread(target=run_cpu_monitor, args=[self]) + TestRTMClient.cpu_monitor.daemon = True + TestRTMClient.cpu_monitor.start() + + self.rtm_client = None + self.web_client = None + + def tearDown(self): + # Reset the decorators by @RTMClient.run_on + RTMClient._callbacks = collections.defaultdict(list) + # Stop the Client + if hasattr(self, "rtm_client") and not self.rtm_client._stopped: + self.rtm_client.stop() + + @pytest.mark.skipif(condition=is_not_specified(), reason="To avoid rate_limited errors") + def test_cpu_usage(self): + self.rtm_client = RTMClient(token=self.bot_token, run_async=False, loop=asyncio.new_event_loop()) + self.web_client = WebClient(token=self.bot_token) + + self.call_count = 0 + TestRTMClient.cpu_usage = 0 + + @RTMClient.run_on(event="message") + def send_reply(**payload): + self.logger.debug(payload) + event = payload["data"] + if "text" in event: + if not str(event["text"]).startswith("Current CPU usage:"): + web_client = payload["web_client"] + for i in range(0, 3): + new_message = web_client.chat_postMessage( + channel=event["channel"], + text=f"Current CPU usage: {TestRTMClient.cpu_usage} % (test_cpu_usage)", + ) + self.logger.debug(new_message) + self.call_count += 1 + + def connect(): + self.logger.debug("Starting RTM Client...") + self.rtm_client.start() + + rtm = threading.Thread(target=connect) + rtm.daemon = True + + rtm.start() + time.sleep(5) + + text = "This message was sent by ! (test_cpu_usage)" + new_message = self.web_client.chat_postMessage(channel=self.channel_id, text=text) + self.assertFalse("error" in new_message) + + time.sleep(5) + self.assertLess(TestRTMClient.cpu_usage, 30, "Too high CPU usage detected") + self.assertEqual(self.call_count, 3, "The RTM handler failed") + + # > self.assertLess(TestRTMClient.cpu_usage, 30, "Too high CPU usage detected") + # E AssertionError: 100.2 not less than 30 : Too high CPU usage detected + # + # integration_tests/rtm/test_rtm_client.py:160: AssertionError + + @async_test + async def test_cpu_usage_async(self): + self.rtm_client = RTMClient(token=self.bot_token, run_async=True) + self.web_client = LegacyWebClient(token=self.bot_token, run_async=True) + + self.call_count = 0 + TestRTMClient.cpu_usage = 0 + + @RTMClient.run_on(event="message") + async def send_reply_async(**payload): + self.logger.debug(payload) + event = payload["data"] + if "text" in event: + if not str(event["text"]).startswith("Current CPU usage:"): + web_client = payload["web_client"] + for i in range(0, 3): + new_message = await web_client.chat_postMessage( + channel=event["channel"], + text=f"Current CPU usage: {TestRTMClient.cpu_usage} % (test_cpu_usage_async)", + ) + self.logger.debug(new_message) + self.call_count += 1 + + # intentionally not waiting here + self.rtm_client.start() + + await asyncio.sleep(5) + + text = "This message was sent by ! (test_cpu_usage_async)" + new_message = await self.web_client.chat_postMessage(channel=self.channel_id, text=text) + self.assertFalse("error" in new_message) + + await asyncio.sleep(5) + self.assertLess(TestRTMClient.cpu_usage, 30, "Too high CPU usage detected") + self.assertEqual(self.call_count, 3, "The RTM handler failed") diff --git a/integration_tests/rtm/test_issue_605.py b/integration_tests/rtm/test_issue_605.py new file mode 100644 index 000000000..68e3ce867 --- /dev/null +++ b/integration_tests/rtm/test_issue_605.py @@ -0,0 +1,109 @@ +import collections +import logging +import os +import threading +import time +import unittest + +import pytest + +from integration_tests.env_variable_names import ( + SLACK_SDK_TEST_CLASSIC_APP_BOT_TOKEN, + SLACK_SDK_TEST_RTM_TEST_CHANNEL_ID, +) +from integration_tests.helpers import is_not_specified +from slack_sdk.rtm import RTMClient +from slack_sdk.web import WebClient + + +class TestRTMClient(unittest.TestCase): + """Runs integration tests with real Slack API + + https://github.com/slackapi/python-slack-sdk/issues/605 + """ + + def setUp(self): + self.logger = logging.getLogger(__name__) + self.bot_token = os.environ[SLACK_SDK_TEST_CLASSIC_APP_BOT_TOKEN] + self.channel_id = os.environ[SLACK_SDK_TEST_RTM_TEST_CHANNEL_ID] + self.rtm_client = RTMClient(token=self.bot_token, run_async=False) + + def tearDown(self): + # Reset the decorators by @RTMClient.run_on + RTMClient._callbacks = collections.defaultdict(list) + + @pytest.mark.skipif(condition=is_not_specified(), reason="To avoid rate_limited errors") + def test_issue_605(self): + self.text = "This message was sent to verify issue #605" + self.called = False + + @RTMClient.run_on(event="message") + def process_messages(**payload): + self.logger.info(payload) + self.called = True + + def connect(): + self.logger.debug("Starting RTM Client...") + self.rtm_client.start() + + t = threading.Thread(target=connect) + t.daemon = True + try: + t.start() + self.assertFalse(self.called) + + time.sleep(3) + + self.web_client = WebClient( + token=self.bot_token, + run_async=False, + ) + new_message = self.web_client.chat_postMessage(channel=self.channel_id, text=self.text) + self.assertFalse("error" in new_message) + + time.sleep(5) + self.assertTrue(self.called) + finally: + t.join(0.3) + + # --- a/slack/rtm/client.py + # +++ b/slack/rtm/client.py + # @@ -10,7 +10,6 @@ import inspect + # import signal + # from typing import Optional, Callable, DefaultDict + # from ssl import SSLContext + # -from threading import current_thread, main_thread + # + # # ThirdParty Imports + # import asyncio + # @@ -186,7 +185,8 @@ class RTMClient(object): + # SlackApiError: Unable to retrieve RTM URL from Slack. + # """ + # # TODO: Add Windows support for graceful shutdowns. + # - if os.name != "nt" and current_thread() == main_thread(): + # + # if os.name != "nt" and current_thread() == main_thread(): + # + if os.name != "nt": + # signals = (signal.SIGHUP, signal.SIGTERM, signal.SIGINT) + # for s in signals: + # self._event_loop.add_signal_handler(s, self.stop) + + # Exception in thread Thread-1: + # Traceback (most recent call last): + # File "/path-to-python/asyncio/unix_events.py", line 95, in add_signal_handler + # signal.set_wakeup_fd(self._csock.fileno()) + # ValueError: set_wakeup_fd only works in main thread + # + # During handling of the above exception, another exception occurred: + # + # Traceback (most recent call last): + # File "/path-to-python/threading.py", line 932, in _bootstrap_inner + # self.run() + # File "/path-to-python/threading.py", line 870, in run + # self._target(*self._args, **self._kwargs) + # File "/path-to-project/python-slackclient/integration_tests/rtm/test_issue_605.py", line 29, in connect + # self.rtm_client.start() + # File "/path-to-project/python-slackclient/slack/rtm/client.py", line 192, in start + # self._event_loop.add_signal_handler(s, self.stop) + # File "/path-to-python/asyncio/unix_events.py", line 97, in add_signal_handler + # raise RuntimeError(str(exc)) + # RuntimeError: set_wakeup_fd only works in main thread diff --git a/integration_tests/rtm/test_issue_611.py b/integration_tests/rtm/test_issue_611.py new file mode 100644 index 000000000..4df8de337 --- /dev/null +++ b/integration_tests/rtm/test_issue_611.py @@ -0,0 +1,84 @@ +import asyncio +import collections +import logging +import os +import unittest + +import pytest + +from integration_tests.env_variable_names import ( + SLACK_SDK_TEST_CLASSIC_APP_BOT_TOKEN, + SLACK_SDK_TEST_RTM_TEST_CHANNEL_ID, +) +from integration_tests.helpers import async_test, is_not_specified +from slack_sdk.rtm import RTMClient +from slack_sdk.web import WebClient + + +class TestRTMClient(unittest.TestCase): + """Runs integration tests with real Slack API + + https://github.com/slackapi/python-slack-sdk/issues/611 + """ + + def setUp(self): + self.logger = logging.getLogger(__name__) + self.bot_token = os.environ[SLACK_SDK_TEST_CLASSIC_APP_BOT_TOKEN] + + def tearDown(self): + # Reset the decorators by @RTMClient.run_on + RTMClient._callbacks = collections.defaultdict(list) + + @pytest.mark.skipif(condition=is_not_specified(), reason="To avoid rate limited errors") + @async_test + async def test_issue_611(self): + channel_id = os.environ[SLACK_SDK_TEST_RTM_TEST_CHANNEL_ID] + text = "This message was sent by ! (test_issue_611)" + + self.message_count, self.reaction_count = 0, 0 + + async def process_messages(**payload): + self.logger.info(payload) + if "subtype" in payload["data"] and payload["data"]["subtype"] == "message_replied": + return # skip + + self.message_count += 1 + raise Exception("something is wrong!") # This causes the termination of the process + + async def process_reactions(**payload): + self.logger.info(payload) + self.reaction_count += 1 + + rtm = RTMClient(token=self.bot_token, run_async=True) + RTMClient.on(event="message", callback=process_messages) + RTMClient.on(event="reaction_added", callback=process_reactions) + + web_client = WebClient(token=self.bot_token, run_async=True) + message = await web_client.chat_postMessage(channel=channel_id, text=text) + ts = message["ts"] + + await asyncio.sleep(3) + + # intentionally not waiting here + rtm.start() + + try: + await asyncio.sleep(3) + + first_reaction = await web_client.reactions_add(channel=channel_id, timestamp=ts, name="eyes") + self.assertFalse("error" in first_reaction) + await asyncio.sleep(2) + + should_be_ignored = await web_client.chat_postMessage(channel=channel_id, text="Hello?", thread_ts=ts) + self.assertFalse("error" in should_be_ignored) + await asyncio.sleep(2) + + second_reaction = await web_client.reactions_add(channel=channel_id, timestamp=ts, name="tada") + self.assertFalse("error" in second_reaction) + await asyncio.sleep(2) + + self.assertEqual(self.message_count, 1) + self.assertEqual(self.reaction_count, 2) + finally: + if not rtm._stopped: + rtm.stop() diff --git a/integration_tests/rtm/test_issue_631.py b/integration_tests/rtm/test_issue_631.py new file mode 100644 index 000000000..0fe55a65c --- /dev/null +++ b/integration_tests/rtm/test_issue_631.py @@ -0,0 +1,155 @@ +import asyncio +import collections +import logging +import os +import threading +import time +import traceback +import unittest + +import pytest + +from integration_tests.env_variable_names import ( + SLACK_SDK_TEST_CLASSIC_APP_BOT_TOKEN, + SLACK_SDK_TEST_RTM_TEST_CHANNEL_ID, +) +from integration_tests.helpers import async_test, is_not_specified +from slack_sdk.rtm import RTMClient +from slack_sdk.web import WebClient + + +class TestRTMClient(unittest.TestCase): + """Runs integration tests with real Slack API + + https://github.com/slackapi/python-slack-sdk/issues/631 + """ + + def setUp(self): + if not hasattr(self, "logger"): + self.logger = logging.getLogger(__name__) + self.channel_id = os.environ[SLACK_SDK_TEST_RTM_TEST_CHANNEL_ID] + self.bot_token = os.environ[SLACK_SDK_TEST_CLASSIC_APP_BOT_TOKEN] + + def tearDown(self): + # Reset the decorators by @RTMClient.run_on + RTMClient._callbacks = collections.defaultdict(list) + # Stop the Client + if hasattr(self, "rtm_client") and not self.rtm_client._stopped: + self.rtm_client.stop() + + @pytest.mark.skipif(condition=is_not_specified(), reason="to avoid rate_limited errors") + def test_issue_631_sharing_event_loop(self): + self.success = None + self.text = "This message was sent to verify issue #631" + + self.rtm_client = RTMClient( + token=self.bot_token, + run_async=False, + loop=asyncio.new_event_loop(), # TODO: this doesn't work without this + ) + + # @RTMClient.run_on(event="message") + # def send_reply(**payload): + # self.logger.debug(payload) + # data = payload['data'] + # web_client = payload['web_client'] + # web_client._event_loop = self.loop + # # Maybe you will also need the following line uncommented + # # web_client.run_async = True + # + # if self.text in data['text']: + # channel_id = data['channel'] + # thread_ts = data['ts'] + # try: + # self.success = web_client.chat_postMessage( + # channel=channel_id, + # text="Thanks!", + # thread_ts=thread_ts + # ) + # except Exception as e: + # # slack.rtm.client:client.py:446 When calling '#send_reply()' + # # in the 'test_rtm_client' module the following error was raised: This event loop is already running + # self.logger.error(traceback.format_exc()) + # raise e + + # Solution (1) for #631 + @RTMClient.run_on(event="message") + def send_reply(**payload): + self.logger.debug(payload) + data = payload["data"] + web_client = payload["web_client"] + + try: + if "text" in data and self.text in data["text"]: + channel_id = data["channel"] + thread_ts = data["ts"] + self.success = web_client.chat_postMessage(channel=channel_id, text="Thanks!", thread_ts=thread_ts) + except Exception as e: + self.logger.error(traceback.format_exc()) + raise e + + def connect(): + self.logger.debug("Starting RTM Client...") + self.rtm_client.start() + + t = threading.Thread(target=connect) + t.daemon = True + t.start() + + try: + self.assertIsNone(self.success) + time.sleep(5) + + self.web_client = WebClient( + token=self.bot_token, + run_async=False, + ) + new_message = self.web_client.chat_postMessage(channel=self.channel_id, text=self.text) + self.assertFalse("error" in new_message) + + time.sleep(5) + self.assertIsNotNone(self.success) + finally: + t.join(0.3) + + # Solution (2) for #631 + @pytest.mark.skipif(condition=is_not_specified(), reason="this is just for reference") + @async_test + async def test_issue_631_sharing_event_loop_async(self): + self.success = None + self.text = "This message was sent to verify issue #631" + + # To make run_async=True, the test method needs to be an async function + @async_test decorator + self.rtm_client = RTMClient(token=self.bot_token, run_async=True) + self.web_client = WebClient(token=self.bot_token, run_async=True) + + @RTMClient.run_on(event="message") + async def send_reply(**payload): + self.logger.debug(payload) + data = payload["data"] + web_client = payload["web_client"] + + try: + if "text" in data and self.text in data["text"]: + channel_id = data["channel"] + thread_ts = data["ts"] + self.success = await web_client.chat_postMessage(channel=channel_id, text="Thanks!", thread_ts=thread_ts) + except Exception as e: + self.logger.error(traceback.format_exc()) + raise e + + # intentionally not waiting here + self.rtm_client.start() + + self.assertIsNone(self.success) + await asyncio.sleep(5) + + self.web_client = WebClient( + token=self.bot_token, + run_async=True, # all need to be async here + ) + new_message = await self.web_client.chat_postMessage(channel=self.channel_id, text=self.text) + self.assertFalse("error" in new_message) + + await asyncio.sleep(5) + self.assertIsNotNone(self.success) diff --git a/integration_tests/rtm/test_issue_701.py b/integration_tests/rtm/test_issue_701.py new file mode 100644 index 000000000..793bab772 --- /dev/null +++ b/integration_tests/rtm/test_issue_701.py @@ -0,0 +1,136 @@ +import asyncio +import collections +import logging +import os +import threading +import time +import unittest + +import pytest + +from integration_tests.env_variable_names import SLACK_SDK_TEST_CLASSIC_APP_BOT_TOKEN +from integration_tests.helpers import async_test, is_not_specified +from slack_sdk.rtm import RTMClient +from slack_sdk.web import WebClient + + +class TestRTMClient(unittest.TestCase): + """Runs integration tests with real Slack API + + https://github.com/slackapi/python-slack-sdk/issues/701 + """ + + def setUp(self): + self.logger = logging.getLogger(__name__) + self.bot_token = os.environ[SLACK_SDK_TEST_CLASSIC_APP_BOT_TOKEN] + + def tearDown(self): + # Reset the decorators by @RTMClient.run_on + RTMClient._callbacks = collections.defaultdict(list) + + # @pytest.mark.skipif(condition=is_not_specified(), reason="to avoid rate_limited errors") + @pytest.mark.skip() + def test_receiving_all_messages(self): + self.rtm_client = RTMClient(token=self.bot_token, loop=asyncio.new_event_loop()) + self.web_client = WebClient(token=self.bot_token) + + self.call_count = 0 + + @RTMClient.run_on(event="message") + def send_reply(**payload): + self.logger.debug(payload) + web_client, data = payload["web_client"], payload["data"] + web_client.reactions_add(channel=data["channel"], timestamp=data["ts"], name="eyes") + self.call_count += 1 + + def connect(): + self.logger.debug("Starting RTM Client...") + self.rtm_client.start() + + rtm = threading.Thread(target=connect) + rtm.daemon = True + + rtm.start() + time.sleep(3) + + total_num = 10 + + sender_completion = [] + + def sent_bulk_message(): + for i in range(total_num): + text = f"Sent by ! ({i})" + self.web_client.chat_postMessage(channel="#random", text=text) + time.sleep(0.1) + sender_completion.append(True) + + num_of_senders = 3 + senders = [] + for sender_num in range(num_of_senders): + sender = threading.Thread(target=sent_bulk_message) + sender.daemon = True + sender.start() + senders.append(sender) + + while len(sender_completion) < num_of_senders: + time.sleep(1) + + expected_call_count = total_num * num_of_senders + wait_seconds = 0 + max_wait = 20 + while self.call_count < expected_call_count and wait_seconds < max_wait: + time.sleep(1) + wait_seconds += 1 + + self.assertEqual(total_num * num_of_senders, self.call_count, "The RTM handler failed") + + @pytest.mark.skipif(condition=is_not_specified(), reason="to avoid rate_limited errors") + @async_test + async def test_receiving_all_messages_async(self): + self.rtm_client = RTMClient(token=self.bot_token, run_async=True) + self.web_client = WebClient(token=self.bot_token, run_async=False) + + self.call_count = 0 + + @RTMClient.run_on(event="message") + async def send_reply(**payload): + self.logger.debug(payload) + web_client, data = payload["web_client"], payload["data"] + await web_client.reactions_add(channel=data["channel"], timestamp=data["ts"], name="eyes") + self.call_count += 1 + + # intentionally not waiting here + self.rtm_client.start() + + await asyncio.sleep(3) + + total_num = 10 + + sender_completion = [] + + def sent_bulk_message(): + for i in range(total_num): + text = f"Sent by ! ({i})" + self.web_client.chat_postMessage(channel="#random", text=text) + time.sleep(0.1) + sender_completion.append(True) + + num_of_senders = 3 + senders = [] + for sender_num in range(num_of_senders): + sender = threading.Thread(target=sent_bulk_message) + sender.daemon = True + sender.start() + senders.append(sender) + + while len(sender_completion) < num_of_senders: + await asyncio.sleep(1) + + expected_call_count = total_num * num_of_senders + wait_seconds = 0 + max_wait = 20 + while self.call_count < expected_call_count and wait_seconds < max_wait: + await asyncio.sleep(1) + wait_seconds += 1 + + self.assertEqual(total_num * num_of_senders, self.call_count, "The RTM handler failed") diff --git a/integration_tests/rtm/test_rtm_client.py b/integration_tests/rtm/test_rtm_client.py new file mode 100644 index 000000000..b56b12ded --- /dev/null +++ b/integration_tests/rtm/test_rtm_client.py @@ -0,0 +1,90 @@ +import asyncio +import collections +import logging +import os +import threading +import time +import unittest + +from integration_tests.env_variable_names import ( + SLACK_SDK_TEST_CLASSIC_APP_BOT_TOKEN, + SLACK_SDK_TEST_RTM_TEST_CHANNEL_ID, +) +from integration_tests.helpers import async_test +from slack_sdk.rtm import RTMClient +from slack_sdk.web.legacy_client import LegacyWebClient + + +class TestRTMClient(unittest.TestCase): + """Runs integration tests with real Slack API""" + + def setUp(self): + if not hasattr(self, "logger"): + self.logger = logging.getLogger(__name__) + self.channel_id = os.environ[SLACK_SDK_TEST_RTM_TEST_CHANNEL_ID] + self.bot_token = os.environ[SLACK_SDK_TEST_CLASSIC_APP_BOT_TOKEN] + + def tearDown(self): + # Reset the decorators by @RTMClient.run_on + RTMClient._callbacks = collections.defaultdict(list) + # Stop the Client + if hasattr(self, "rtm_client") and not self.rtm_client._stopped: + self.rtm_client.stop() + + def test_basic_operations(self): + self.sent_text: str = None + self.rtm_client = RTMClient( + token=self.bot_token, + run_async=False, + loop=asyncio.new_event_loop(), # TODO: this doesn't work without this + ) + self.web_client = LegacyWebClient(token=self.bot_token) + + @RTMClient.run_on(event="message") + def send_reply(**payload): + self.logger.debug(payload) + self.sent_text = payload["data"]["text"] + + def connect(): + self.logger.debug("Starting RTM Client...") + self.rtm_client.start() + + t = threading.Thread(target=connect) + t.daemon = True + t.start() + + try: + self.assertIsNone(self.sent_text) + time.sleep(5) + + text = "This message was sent by ! (test_basic_operations)" + new_message = self.web_client.chat_postMessage(channel=self.channel_id, text=text) + self.assertFalse("error" in new_message) + + time.sleep(5) + self.assertEqual(self.sent_text, text) + finally: + t.join(0.3) + + @async_test + async def test_basic_operations_async(self): + self.sent_text: str = None + self.rtm_client = RTMClient(token=self.bot_token, run_async=True) + self.async_web_client = LegacyWebClient(token=self.bot_token, run_async=True) + + @RTMClient.run_on(event="message") + async def send_reply(**payload): + self.logger.debug(payload) + self.sent_text = payload["data"]["text"] + + # intentionally not waiting here + self.rtm_client.start() + + self.assertIsNone(self.sent_text) + await asyncio.sleep(5) + + text = "This message was sent by ! (test_basic_operations_async)" + new_message = await self.async_web_client.chat_postMessage(channel=self.channel_id, text=text) + self.assertFalse("error" in new_message) + await asyncio.sleep(5) + self.assertEqual(self.sent_text, text) diff --git a/integration_tests/samples/basic_usage/calling_any_api_methods.py b/integration_tests/samples/basic_usage/calling_any_api_methods.py new file mode 100644 index 000000000..bc545af39 --- /dev/null +++ b/integration_tests/samples/basic_usage/calling_any_api_methods.py @@ -0,0 +1,13 @@ +import logging + +logging.basicConfig(level=logging.DEBUG) + +# export SLACK_API_TOKEN=xoxb-*** +# python3 integration_tests/samples/basic_usage/calling_any_api_methods.py + +import os +from slack_sdk.web import WebClient + +client = WebClient(token=os.environ["SLACK_API_TOKEN"]) +response = client.api_call(api_method="chat.postMessage", json={"channel": "#random", "text": "Hello world!"}) +assert response["message"]["text"] == "Hello world!" diff --git a/integration_tests/samples/basic_usage/channels.py b/integration_tests/samples/basic_usage/channels.py new file mode 100644 index 000000000..f7c1c9c41 --- /dev/null +++ b/integration_tests/samples/basic_usage/channels.py @@ -0,0 +1,23 @@ +import logging + +logging.basicConfig(level=logging.DEBUG) + +# export SLACK_API_TOKEN=xoxb-*** +# python3 integration_tests/samples/basic_usage/channels.py + +import os +from slack_sdk.web import WebClient + +client = WebClient(token=os.environ["SLACK_API_TOKEN"]) + +response = client.conversations_list(exclude_archived=1) + +channel_id = response["channels"][0]["id"] + +response = client.conversations_info(channel=channel_id) + +response = client.conversations_join(channel=channel_id) + +response = client.conversations_leave(channel=channel_id) + +response = client.conversations_join(channel=channel_id) diff --git a/integration_tests/samples/basic_usage/emoji_reactions.py b/integration_tests/samples/basic_usage/emoji_reactions.py new file mode 100644 index 000000000..03f11d266 --- /dev/null +++ b/integration_tests/samples/basic_usage/emoji_reactions.py @@ -0,0 +1,27 @@ +import logging + +logging.basicConfig(level=logging.DEBUG) + +# export SLACK_API_TOKEN=xoxb-*** +# python3 integration_tests/samples/basic_usage/emoji_reactions.py + +import os +from slack_sdk.web import WebClient + +client = WebClient(token=os.environ["SLACK_API_TOKEN"]) + +if __name__ == "__main__": + channel_id = "#random" + user_id = client.users_list()["members"][0]["id"] +else: + channel_id = "C0XXXXXX" + user_id = "U0XXXXXXX" + +response = client.chat_postMessage(channel=channel_id, text="Give me some reaction!") +# Ensure the channel_id is not a name +channel_id = response["channel"] +ts = response["message"]["ts"] + +response = client.reactions_add(channel=channel_id, name="thumbsup", timestamp=ts) + +response = client.reactions_remove(channel=channel_id, name="thumbsup", timestamp=ts) diff --git a/integration_tests/samples/basic_usage/rate_limits.py b/integration_tests/samples/basic_usage/rate_limits.py new file mode 100644 index 000000000..e0d947cbf --- /dev/null +++ b/integration_tests/samples/basic_usage/rate_limits.py @@ -0,0 +1,38 @@ +import logging + +logging.basicConfig(level=logging.DEBUG) + +# export SLACK_API_TOKEN=xoxb-*** +# python3 integration_tests/samples/basic_usage/rate_limits.py + +import os +import time +from slack_sdk.web import WebClient +from slack_sdk.errors import SlackApiError + +client = WebClient(token=os.environ["SLACK_API_TOKEN"]) + + +# Simple wrapper for sending a Slack message +def send_slack_message(channel, message): + return client.chat_postMessage(channel=channel, text=message) + + +# Make the API call and save results to `response` +channel = "#random" +message = "Hello, from Python!" + +# Do until being rate limited +while True: + try: + response = send_slack_message(channel, message) + except SlackApiError as e: + if e.response["error"] == "ratelimited": + # The `Retry-After` header will tell you how long to wait before retrying + delay = int(e.response.headers["Retry-After"]) + print(f"Rate limited. Retrying in {delay} seconds") + time.sleep(delay) + response = send_slack_message(channel, message) + else: + # other errors + raise e diff --git a/integration_tests/samples/basic_usage/sending_a_message.py b/integration_tests/samples/basic_usage/sending_a_message.py new file mode 100644 index 000000000..ed55ad00b --- /dev/null +++ b/integration_tests/samples/basic_usage/sending_a_message.py @@ -0,0 +1,78 @@ +import logging + +logging.basicConfig(level=logging.DEBUG) + +# export SLACK_API_TOKEN=xoxb-*** +# python3 integration_tests/samples/basic_usage/sending_a_message.py + +import os +from slack_sdk.web import WebClient + +slack_token = os.environ["SLACK_API_TOKEN"] +client = WebClient(token=slack_token) + +if __name__ == "__main__": + channel_id = "#random" + user_id = client.users_list()["members"][0]["id"] +else: + channel_id = "C0XXXXXX" + user_id = "U0XXXXXXX" + +response = client.chat_postMessage(channel=channel_id, text="Hello from your app! :tada:") +# Ensure the channel_id is not a name +channel_id = response["channel"] + +thread_ts = response["message"]["ts"] + +response = client.chat_postEphemeral(channel=channel_id, user=user_id, text="Hello silently from your app! :tada:") + +response = client.chat_postMessage( + channel=channel_id, + blocks=[ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "Danny Torrence left the following review for your property:", + }, + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": " \n :star: \n Doors had too many axe holes, guest in room " + + "237 was far too rowdy, whole place felt stuck in the 1920s.", + }, + "accessory": { + "type": "image", + "image_url": "https://images.pexels.com/photos/750319/pexels-photo-750319.jpeg", + "alt_text": "Haunted hotel image", + }, + }, + { + "type": "section", + "fields": [{"type": "mrkdwn", "text": "*Average Rating*\n1.0"}], + }, + ], +) + +# Threading Messages +response = client.chat_postMessage(channel=channel_id, text="Hello from your app! :tada:", thread_ts=thread_ts) + +response = client.chat_postMessage( + channel=channel_id, + text="Hello from your app! :tada:", + thread_ts=thread_ts, + reply_broadcast=True, +) + +# Updating a message +response = client.chat_postMessage(channel=channel_id, text="To be modified :eyes:") +ts = response["message"]["ts"] + +response = client.chat_update(channel=channel_id, ts=ts, text="updates from your app! :tada:") + +# Deleting a message +response = client.chat_postMessage(channel=channel_id, text="To be deleted :eyes:") +ts = response["message"]["ts"] +response = client.chat_delete(channel=channel_id, ts=ts) diff --git a/integration_tests/samples/basic_usage/uploading_files.py b/integration_tests/samples/basic_usage/uploading_files.py new file mode 100644 index 000000000..7d0c828a5 --- /dev/null +++ b/integration_tests/samples/basic_usage/uploading_files.py @@ -0,0 +1,17 @@ +import logging + +logging.basicConfig(level=logging.DEBUG) + +# export SLACK_API_TOKEN=xoxb-*** +# echo 'Hello world!' > tmp.txt +# python3 integration_tests/samples/basic_usage/uploading_files.py + +import os +from slack_sdk.web import WebClient + +client = WebClient(token=os.environ["SLACK_API_TOKEN"]) + +channels = ",".join(["#random"]) +filepath = "./tmp.txt" +response = client.files_upload(channels=channels, file=filepath) +response = client.files_upload_v2(channel=response.get("file").get("channels")[0], file=filepath) diff --git a/integration_tests/samples/basic_usage/users.py b/integration_tests/samples/basic_usage/users.py new file mode 100644 index 000000000..1446b32e6 --- /dev/null +++ b/integration_tests/samples/basic_usage/users.py @@ -0,0 +1,13 @@ +import logging + +logging.basicConfig(level=logging.DEBUG) + +# export SLACK_API_TOKEN=xoxb-*** +# python3 integration_tests/samples/basic_usage/users.py + +import os +from slack_sdk.web import WebClient + +client = WebClient(token=os.environ["SLACK_API_TOKEN"]) + +response = client.users_list() diff --git a/integration_tests/samples/basic_usage/views.py b/integration_tests/samples/basic_usage/views.py new file mode 100644 index 000000000..ef791116e --- /dev/null +++ b/integration_tests/samples/basic_usage/views.py @@ -0,0 +1,106 @@ +import json +import logging + +logging.basicConfig(level=logging.DEBUG) + +# --------------------- +# Slack WebClient +# --------------------- + +import os + +from slack_sdk.web import WebClient +from slack_sdk.errors import SlackApiError +from slack_sdk.signature import SignatureVerifier + +client = WebClient(token=os.environ["SLACK_BOT_TOKEN"]) +signature_verifier = SignatureVerifier(os.environ["SLACK_SIGNING_SECRET"]) + +# --------------------- +# Flask App +# --------------------- + +# pip3 install flask +from flask import Flask, request, make_response, jsonify + +app = Flask(__name__) + + +@app.route("/slack/events", methods=["POST"]) +def slack_app(): + if not signature_verifier.is_valid_request(request.get_data(), request.headers): + return make_response("invalid request", 403) + + if "payload" in request.form: + payload = json.loads(request.form["payload"]) + + if payload["type"] == "shortcut" and payload["callback_id"] == "test-shortcut": + # Open a new modal by a global shortcut + try: + api_response = client.views_open( + trigger_id=payload["trigger_id"], + view={ + "type": "modal", + "callback_id": "modal-id", + "title": {"type": "plain_text", "text": "Awesome Modal"}, + "submit": {"type": "plain_text", "text": "Submit"}, + "close": {"type": "plain_text", "text": "Cancel"}, + "blocks": [ + { + "type": "input", + "block_id": "b-id", + "label": { + "type": "plain_text", + "text": "Input label", + }, + "element": { + "action_id": "a-id", + "type": "plain_text_input", + }, + } + ], + }, + ) + return make_response("", 200) + except SlackApiError as e: + code = e.response["error"] + return make_response(f"Failed to open a modal due to {code}", 200) + + if payload["type"] == "view_submission" and payload["view"]["callback_id"] == "modal-id": + # Handle a data submission request from the modal + submitted_data = payload["view"]["state"]["values"] + print(submitted_data) # {'b-id': {'a-id': {'type': 'plain_text_input', 'value': 'your input'}}} + return make_response( + jsonify( + { + "response_action": "update", + "view": { + "type": "modal", + "title": {"type": "plain_text", "text": "Accepted"}, + "close": {"type": "plain_text", "text": "Close"}, + "blocks": [ + { + "type": "section", + "text": { + "type": "plain_text", + "text": "Thanks for submitting the data!", + }, + } + ], + }, + } + ), + 200, + ) + + return make_response("", 404) + + +if __name__ == "__main__": + # export SLACK_SIGNING_SECRET=*** + # export SLACK_API_TOKEN=xoxb-*** + # export FLASK_ENV=development + # python3 integration_tests/samples/basic_usage/views.py + app.run("localhost", 3000) + +# ngrok http 3000 diff --git a/integration_tests/samples/basic_usage/views_2.py b/integration_tests/samples/basic_usage/views_2.py new file mode 100644 index 000000000..7ed4feea9 --- /dev/null +++ b/integration_tests/samples/basic_usage/views_2.py @@ -0,0 +1,101 @@ +import json +import logging + +logging.basicConfig(level=logging.DEBUG) + +# --------------------- +# Slack WebClient +# --------------------- + +import os + +from slack_sdk.web import WebClient +from slack_sdk.errors import SlackApiError +from slack_sdk.signature import SignatureVerifier +from slack_sdk.models.blocks import InputBlock, SectionBlock +from slack_sdk.models.blocks.block_elements import PlainTextInputElement +from slack_sdk.models.blocks.basic_components import PlainTextObject +from slack_sdk.models.views import View + +client = WebClient(token=os.environ["SLACK_BOT_TOKEN"]) +signature_verifier = SignatureVerifier(os.environ["SLACK_SIGNING_SECRET"]) + +# --------------------- +# Flask App +# --------------------- + +# pip3 install flask +from flask import Flask, request, make_response, jsonify + +app = Flask(__name__) + + +@app.route("/slack/events", methods=["POST"]) +def slack_app(): + if not signature_verifier.is_valid_request(request.get_data(), request.headers): + return make_response("invalid request", 403) + + if "payload" in request.form: + payload = json.loads(request.form["payload"]) + if payload["type"] == "shortcut" and payload["callback_id"] == "test-shortcut": + # Open a new modal by a global shortcut + try: + view = View( + type="modal", + callback_id="modal-id", + title=PlainTextObject(text="Awesome Modal"), + submit=PlainTextObject(text="Submit"), + close=PlainTextObject(text="Cancel"), + blocks=[ + InputBlock( + block_id="b-id", + label=PlainTextObject(text="Input label"), + element=PlainTextInputElement(action_id="a-id"), + ) + ], + ) + api_response = client.views_open( + trigger_id=payload["trigger_id"], + view=view, + ) + return make_response("", 200) + except SlackApiError as e: + code = e.response["error"] + return make_response(f"Failed to open a modal due to {code}", 200) + + if payload["type"] == "view_submission" and payload["view"]["callback_id"] == "modal-id": + # Handle a data submission request from the modal + submitted_data = payload["view"]["state"]["values"] + print(submitted_data) # {'b-id': {'a-id': {'type': 'plain_text_input', 'value': 'your input'}}} + return make_response( + jsonify( + { + "response_action": "update", + "view": View( + type="modal", + callback_id="modal-id", + title=PlainTextObject(text="Accepted"), + close=PlainTextObject(text="Close"), + blocks=[ + SectionBlock( + block_id="b-id", + text=PlainTextObject(text="Thanks for submitting the data!"), + ) + ], + ).to_dict(), + } + ), + 200, + ) + + return make_response("", 404) + + +if __name__ == "__main__": + # export SLACK_SIGNING_SECRET=*** + # export SLACK_API_TOKEN=xoxb-*** + # export FLASK_ENV=development + # python3 integration_tests/samples/basic_usage/views_2.py + app.run("localhost", 3000) + +# ngrok http 3000 diff --git a/integration_tests/samples/basic_usage/views_default_to_current_conversation.py b/integration_tests/samples/basic_usage/views_default_to_current_conversation.py new file mode 100644 index 000000000..13cbd710a --- /dev/null +++ b/integration_tests/samples/basic_usage/views_default_to_current_conversation.py @@ -0,0 +1,103 @@ +import json +import logging + +logging.basicConfig(level=logging.DEBUG) + +# --------------------- +# Slack WebClient +# --------------------- + +import os + +from slack_sdk.web import WebClient +from slack_sdk.errors import SlackApiError +from slack_sdk.signature import SignatureVerifier +from slack_sdk.models.blocks import InputBlock +from slack_sdk.models.blocks.block_elements import ( + ConversationMultiSelectElement, + ConversationSelectElement, +) +from slack_sdk.models.blocks.basic_components import PlainTextObject +from slack_sdk.models.views import View + +client = WebClient(token=os.environ["SLACK_BOT_TOKEN"]) +signature_verifier = SignatureVerifier(os.environ["SLACK_SIGNING_SECRET"]) + +# --------------------- +# Flask App +# --------------------- + +# pip3 install flask +from flask import Flask, request, make_response + +app = Flask(__name__) + + +def open_modal(trigger_id: str): + try: + view = View( + type="modal", + callback_id="modal-id", + title=PlainTextObject(text="Awesome Modal"), + submit=PlainTextObject(text="Submit"), + close=PlainTextObject(text="Cancel"), + blocks=[ + InputBlock( + block_id="b-id-1", + label=PlainTextObject(text="Input label"), + element=ConversationSelectElement( + action_id="a", + default_to_current_conversation=True, + ), + ), + InputBlock( + block_id="b-id-2", + label=PlainTextObject(text="Input label"), + element=ConversationMultiSelectElement( + action_id="a", + max_selected_items=2, + default_to_current_conversation=True, + ), + ), + ], + ) + api_response = client.views_open(trigger_id=trigger_id, view=view) + return make_response("", 200) + except SlackApiError as e: + code = e.response["error"] + return make_response(f"Failed to open a modal due to {code}", 200) + + +@app.route("/slack/events", methods=["POST"]) +def slack_app(): + if not signature_verifier.is_valid_request(request.get_data(), request.headers): + return make_response("invalid request", 403) + + if "command" in request.form and request.form["command"] == "/view": + # Open a new modal by a slash command + return open_modal(request.form["trigger_id"]) + + elif "payload" in request.form: + payload = json.loads(request.form["payload"]) + + if payload["type"] == "shortcut" and payload["callback_id"] == "test-shortcut": + # Open a new modal by a global shortcut + return open_modal(payload["trigger_id"]) + + if payload["type"] == "view_submission" and payload["view"]["callback_id"] == "modal-id": + # Handle a data submission request from the modal + submitted_data = payload["view"]["state"]["values"] + print(submitted_data) # {'b-id': {'a-id': {'type': 'plain_text_input', 'value': 'your input'}}} + return make_response("", 200) + + return make_response("", 404) + + +if __name__ == "__main__": + # export SLACK_SIGNING_SECRET=*** + # export SLACK_API_TOKEN=xoxb-*** + # export FLASK_ENV=development + # python3 integration_tests/samples/basic_usage/views_default_to_current_conversation.py + app.run("localhost", 3000) + +# ngrok http 3000 diff --git a/integration_tests/samples/conversations/create_private_channel.py b/integration_tests/samples/conversations/create_private_channel.py new file mode 100644 index 000000000..4ab780b2b --- /dev/null +++ b/integration_tests/samples/conversations/create_private_channel.py @@ -0,0 +1,24 @@ +import logging + +logging.basicConfig(level=logging.DEBUG) + +# export SLACK_API_TOKEN=xoxb-*** +# python3 integration_tests/samples/conversations/create_private_channel.py + +import os +from slack_sdk.web import WebClient +from time import time + +client = WebClient(token=os.environ["SLACK_API_TOKEN"]) + +channel_name = f"my-private-channel-{round(time())}" +response = client.conversations_create(name=channel_name, is_private=True) +channel_id = response["channel"]["id"] + +response = client.conversations_info(channel=channel_id, include_num_members=1) # TODO: True + +response = client.conversations_members(channel=channel_id) +user_ids = response["members"] +print(f"user_ids: {user_ids}") + +response = client.conversations_archive(channel=channel_id) diff --git a/integration_tests/samples/conversations/list_conversations.py b/integration_tests/samples/conversations/list_conversations.py new file mode 100644 index 000000000..19749f04d --- /dev/null +++ b/integration_tests/samples/conversations/list_conversations.py @@ -0,0 +1,19 @@ +import logging + +logging.basicConfig(level=logging.DEBUG) + +# export SLACK_API_TOKEN=xoxb-*** +# python3 integration_tests/samples/conversations/list_conversations.py + +import os +from slack_sdk.web import WebClient + +client = WebClient(token=os.environ["SLACK_API_TOKEN"]) + +response = client.conversations_list() + +response = client.conversations_list(types="public_channel, private_channel") + +channel_id = response["channels"][0]["id"] + +response = client.conversations_info(channel=channel_id, include_num_members=1) # TODO: True diff --git a/integration_tests/samples/conversations/open_dm.py b/integration_tests/samples/conversations/open_dm.py new file mode 100644 index 000000000..86786c885 --- /dev/null +++ b/integration_tests/samples/conversations/open_dm.py @@ -0,0 +1,25 @@ +import logging + +logging.basicConfig(level=logging.DEBUG) + +# export SLACK_API_TOKEN=xoxb-*** +# python3 integration_tests/samples/conversations/open_dm.py + +import os +from slack_sdk.web import WebClient + +client = WebClient(token=os.environ["SLACK_API_TOKEN"]) + +all_users = client.users_list(limit=100)["members"] +joinable_only = ( + lambda u: u["id"] != "USLACKBOT" + and not u["is_bot"] + and not u["is_app_user"] + and not u["deleted"] + and not u["is_restricted"] + and not u["is_ultra_restricted"] +) +users = filter(joinable_only, all_users) +user_ids = list(map(lambda u: u["id"], users)) + +response = client.conversations_open(users=user_ids) diff --git a/integration_tests/samples/issues/issue_497.py b/integration_tests/samples/issues/issue_497.py new file mode 100644 index 000000000..7741c7ecb --- /dev/null +++ b/integration_tests/samples/issues/issue_497.py @@ -0,0 +1,95 @@ +import asyncio +import logging + +logging.basicConfig(level=logging.DEBUG) + + +# --------------------- +# Flask App +# --------------------- + +# pip3 install flask +from flask import Flask, make_response + +app = Flask(__name__) +logger = logging.getLogger(__name__) + +import os + +from slack_sdk.web import WebClient +from slack_sdk.errors import SlackApiError + +singleton_client = WebClient(token=os.environ["SLACK_BOT_TOKEN"], run_async=False) + +singleton_loop = asyncio.new_event_loop() +singleton_async_client = WebClient(token=os.environ["SLACK_BOT_TOKEN"], run_async=True, loop=singleton_loop) + + +# Fixed in 2.6.0: This doesn't work +@app.route("/sync/singleton", methods=["GET"]) +def singleton(): + try: + # blocking here!!! + # as described at https://github.com/slackapi/python-slack-sdk/issues/497 + # until this completion, other simultaneous requests get "RuntimeError: This event loop is already running" + response = singleton_client.chat_postMessage( + channel="#random", + text="You used the singleton WebClient for posting this message!", + ) + return str(response) + except SlackApiError as e: + return make_response(str(e), 400) + + +@app.route("/sync/per-request", methods=["GET"]) +def per_request(): + try: + client = WebClient(token=os.environ["SLACK_BOT_TOKEN"], run_async=False) + response = client.chat_postMessage(channel="#random", text="You used a new WebClient for posting this message!") + return str(response) + except SlackApiError as e: + return make_response(str(e), 400) + + +# This doesn't work +@app.route("/async/singleton", methods=["GET"]) +def singleton_async(): + try: + future = singleton_async_client.chat_postMessage( + channel="#random", + text="You used the singleton WebClient for posting this message!", + ) + # blocking here!!! + # as described at https://github.com/slackapi/python-slack-sdk/issues/497 + # until this completion, other simultaneous requests get "RuntimeError: This event loop is already running" + response = singleton_loop.run_until_complete(future) + return str(response) + except SlackApiError as e: + return make_response(str(e), 400) + + +@app.route("/async/per-request", methods=["GET"]) +def per_request_async(): + try: + # This is not optimal and the host should have a large number of FD (File Descriptor) + loop_for_this_request = asyncio.new_event_loop() + + async_client = WebClient( + token=os.environ["SLACK_BOT_TOKEN"], + run_async=True, + loop=loop_for_this_request, + ) + future = async_client.chat_postMessage( + channel="#random", + text="You used the singleton WebClient for posting this message!", + ) + response = loop_for_this_request.run_until_complete(future) + return str(response) + except SlackApiError as e: + return make_response(str(e), 400) + + +if __name__ == "__main__": + # export FLASK_ENV=development + # python3 integration_tests/samples/issues/issue_497.py + app.run(debug=True, host="localhost", port=3000) diff --git a/integration_tests/samples/issues/issue_506.py b/integration_tests/samples/issues/issue_506.py new file mode 100644 index 000000000..b38b57ab4 --- /dev/null +++ b/integration_tests/samples/issues/issue_506.py @@ -0,0 +1,36 @@ +import logging + +logging.basicConfig(level=logging.DEBUG) + +# export SLACK_API_TOKEN=xoxb-*** +# python3 integration_tests/samples/issues/issue_506.py + +import os +from slack_sdk.rtm import RTMClient + +logger = logging.getLogger(__name__) +global_state = {} + + +@RTMClient.run_on(event="open") +def open(**payload): + web_client = payload["web_client"] + auth_result = web_client.auth_test() + global_state.update({"bot_id": auth_result["bot_id"]}) + logger.info(f"cached: {global_state}") + + +@RTMClient.run_on(event="message") +def message(**payload): + data = payload["data"] + if data.get("bot_id", None) == global_state["bot_id"]: + logger.debug("Skipped as it's me") + return + # do something here + web_client = payload["web_client"] + message = web_client.chat_postMessage(channel=data["channel"], text="What's up?") + logger.info(f"message: {message['ts']}") + + +rtm_client = RTMClient(token=os.environ["SLACK_API_TOKEN"]) +rtm_client.start() diff --git a/integration_tests/samples/issues/issue_522.py b/integration_tests/samples/issues/issue_522.py new file mode 100644 index 000000000..83a0bba9d --- /dev/null +++ b/integration_tests/samples/issues/issue_522.py @@ -0,0 +1,62 @@ +# export SLACK_SDK_TEST_CLASSIC_APP_BOT_TOKEN=xoxb-*** +# python3 integration_tests/samples/issues/issue_522.py + +import asyncio +import logging +import os +import sys + +from slack_sdk.rtm import RTMClient + +logging.basicConfig(level=logging.DEBUG) +LOGGER = logging.getLogger(__name__) + +token = os.environ["SLACK_SDK_TEST_CLASSIC_APP_BOT_TOKEN"] + + +async def sleepy_count(name, sleep_for): + for i in range(10): + await asyncio.sleep(sleep_for) + LOGGER.debug(f"{name} - slept {i + 1} times.") + + +async def slack_client_and_sleeps(): + # real-time-messaging Slack client + client = RTMClient(token=token, run_async=True) + + sleepy_count_task = asyncio.create_task(sleepy_count("first counter", 1)) + sleepy_count_task2 = asyncio.create_task(sleepy_count("second counter", 3)) + + await asyncio.gather(client.start(), sleepy_count_task, sleepy_count_task2) + + +async def slack_client(): + # real-time-messaging Slack client + client = RTMClient(token=token, run_async=True) + + await asyncio.gather(client.start()) + + +async def sleeps(): + sleepy_count_task = asyncio.create_task(sleepy_count("first counter", 1)) + sleepy_count_task2 = asyncio.create_task(sleepy_count("second counter", 3)) + + await asyncio.gather(sleepy_count_task, sleepy_count_task2) + + +if __name__ == "__main__": + LOGGER.info(f"Try: kill -2 {os.getpid()} or ctrl+c") + if len(sys.argv) > 1: + option = sys.argv[1] + if option == "1": + # sigint closes program correctly + asyncio.run(slack_client()) + elif option == "2": + # sigint closes program correctly + asyncio.run(sleeps()) + elif option == "3": + # sigint doesn't actually close properly + asyncio.run(slack_client_and_sleeps()) + else: + # sigint doesn't actually close properly + asyncio.run(slack_client_and_sleeps()) diff --git a/integration_tests/samples/issues/issue_690.py b/integration_tests/samples/issues/issue_690.py new file mode 100644 index 000000000..eadbce5f7 --- /dev/null +++ b/integration_tests/samples/issues/issue_690.py @@ -0,0 +1,39 @@ +import logging + +logging.basicConfig(level=logging.DEBUG) + +# --------------------- +# Flask App +# --------------------- + +import os + +# pip install flask +from flask import Flask, make_response, request + +app = Flask(__name__) +logger = logging.getLogger(__name__) + + +@app.route("/slack/oauth/callback", methods=["GET"]) +def endpoint(): + code = request.args.get("code") + from slack_sdk.web import WebClient + from slack_sdk.errors import SlackApiError + + try: + client = WebClient(token="") + client_id = os.environ["SLACK_CLIENT_ID"] + client_secret = os.environ["SLACK_CLIENT_SECRET"] + response = client.oauth_v2_access(client_id=client_id, client_secret=client_secret, code=code) + result = response.get("error", "success!") + return str(result) + except SlackApiError as e: + return make_response(str(e), 400) + + +if __name__ == "__main__": + # export SLACK_CLIENT_ID=111.222 + # export SLACK_CLIENT_SECRET= + # FLASK_ENV=development python integration_tests/samples/issues/issue_690.py + app.run(debug=True, host="localhost", port=3000) diff --git a/integration_tests/samples/issues/issue_714.py b/integration_tests/samples/issues/issue_714.py new file mode 100644 index 000000000..b7a7bd0c6 --- /dev/null +++ b/integration_tests/samples/issues/issue_714.py @@ -0,0 +1,43 @@ +import asyncio +import logging + +logging.basicConfig(level=logging.DEBUG) + +logger = logging.getLogger(__name__) + +import os +from slack_sdk.web import WebClient + +# export HTTPS_PROXY=http://localhost:9000 +client = WebClient(token=os.environ["SLACK_API_TOKEN"]) +response = client.auth_test() +logger.info(f"HTTPS_PROXY response: {response}") + +client = WebClient(token=os.environ["SLACK_API_TOKEN"], proxy="http://localhost:9000") +response = client.auth_test() +logger.info(f"sync response: {response}") + +client = WebClient(token=os.environ["SLACK_API_TOKEN"], proxy="localhost:9000") +response = client.auth_test() +logger.info(f"sync response: {response}") + + +async def async_call(): + client = WebClient( + token=os.environ["SLACK_API_TOKEN"], + proxy="http://localhost:9000", + run_async=True, + ) + response = await client.auth_test() + logger.info(f"async response: {response}") + + +asyncio.run(async_call()) + +# Terminal A: +# pip3 install proxy.py +# proxy --port 9000 --log-level d + +# Terminal B: +# export SLACK_API_TOKEN=xoxb-*** +# python3 integration_tests/samples/issues/issue_714.py diff --git a/integration_tests/samples/issues/issue_838.py b/integration_tests/samples/issues/issue_838.py new file mode 100644 index 000000000..0c84b59ff --- /dev/null +++ b/integration_tests/samples/issues/issue_838.py @@ -0,0 +1,50 @@ +import json +import logging + +logging.basicConfig(level=logging.DEBUG) + +# --------------------- +# Slack WebClient +# --------------------- + +import os + +from slack_sdk.web import WebClient +from slack_sdk.signature import SignatureVerifier + +app_token_client = WebClient(token=os.environ["SLACK_APP_TOKEN"]) # xapp- +signature_verifier = SignatureVerifier(os.environ["SLACK_SIGNING_SECRET"]) + +# --------------------- +# Flask App +# --------------------- + +# pip3 install flask +from flask import Flask, request, make_response + +app = Flask(__name__) + + +@app.route("/slack/events", methods=["POST"]) +def slack_app(): + request_body = request.get_data() + if not signature_verifier.is_valid_request(request_body, request.headers): + return make_response("invalid request", 403) + + if request.headers["content-type"] == "application/json": + body = json.loads(request_body) + response = app_token_client.apps_event_authorizations_list(event_context=body["event_context"]) + print(response) + return make_response("", 200) + + return make_response("", 404) + + +if __name__ == "__main__": + # export SLACK_SIGNING_SECRET=*** + # export SLACK_API_TOKEN=xoxb-*** + # export FLASK_ENV=development + # python3 integration_tests/web/test_issue_838.py + app.run("localhost", 3000) + +# ngrok http 3000 diff --git a/integration_tests/samples/issues/issue_868.py b/integration_tests/samples/issues/issue_868.py new file mode 100644 index 000000000..d88700a5d --- /dev/null +++ b/integration_tests/samples/issues/issue_868.py @@ -0,0 +1,30 @@ +import logging + +logging.basicConfig(level=logging.DEBUG) + + +def legacy(): + from slack_sdk.models.blocks import SectionBlock + from slack_sdk.models.blocks.basic_components import TextObject + + fields = [] + fields.append(TextObject(text="...", type="mrkdwn")) + block = SectionBlock(text="", fields=fields) + assert block is not None + + +from slack_sdk.models.blocks import SectionBlock, TextObject + +fields = [] +fields.append(TextObject(text="...", type="mrkdwn")) +block = SectionBlock(text="", fields=fields) +assert block is not None + +# +# pip install mypy +# mypy integration_tests/samples/issues/issue_868.py | grep integration_tests +# + +# integration_tests/samples/issues/issue_868.py:26: error: Argument "fields" to "SectionBlock" has incompatible type "List[TextObject]"; expected "Optional[List[Union[str, Dict[Any, Any], TextObject]]]" +# integration_tests/samples/issues/issue_868.py:26: note: "List" is invariant -- see http://mypy.readthedocs.io/en/latest/common_issues.html#variance +# integration_tests/samples/issues/issue_868.py:26: note: Consider using "Sequence" instead, which is covariant diff --git a/integration_tests/samples/issues/issue_926.py b/integration_tests/samples/issues/issue_926.py new file mode 100644 index 000000000..ca893fc7e --- /dev/null +++ b/integration_tests/samples/issues/issue_926.py @@ -0,0 +1,23 @@ +import asyncio +import logging +import os + +from slack_sdk.socket_mode.aiohttp import SocketModeClient + +logging.basicConfig(level=logging.DEBUG) + + +async def main(): + client = SocketModeClient(app_token=os.environ["SLACK_APP_TOKEN"]) + await client.connect() + await asyncio.sleep(3) + await client.close() + + +if __name__ == "__main__": + asyncio.run(main()) + +# The issue: +# ERROR:asyncio:Unclosed client session +# client_session: +# INFO:slack_sdk.socket_mode.aiohttp:The session has been abandoned diff --git a/integration_tests/samples/oauth/oauth_v2.py b/integration_tests/samples/oauth/oauth_v2.py new file mode 100644 index 000000000..c1c32d879 --- /dev/null +++ b/integration_tests/samples/oauth/oauth_v2.py @@ -0,0 +1,202 @@ +# --------------------- +# Flask App for Slack OAuth flow +# --------------------- +import html + +# pip3 install flask +from flask import Flask, request, make_response + +app = Flask(__name__) +app.debug = True + +import logging +import os +from slack_sdk.web import WebClient +from slack_sdk.oauth import AuthorizeUrlGenerator, RedirectUriPageRenderer +from slack_sdk.oauth.installation_store import FileInstallationStore, Installation +from slack_sdk.oauth.state_store import FileOAuthStateStore + +client_id = os.environ["SLACK_CLIENT_ID"] +client_secret = os.environ["SLACK_CLIENT_SECRET"] +scopes = ["app_mentions:read", "chat:write"] +user_scopes = ["search:read"] + +logger = logging.getLogger(__name__) + + +state_store = FileOAuthStateStore(expiration_seconds=300) +installation_store = FileInstallationStore() +authorization_url_generator = AuthorizeUrlGenerator( + client_id=client_id, + scopes=scopes, + user_scopes=user_scopes, +) +redirect_page_renderer = RedirectUriPageRenderer( + install_path="/slack/install", + redirect_uri_path="/slack/oauth_redirect", +) + + +@app.route("/slack/install", methods=["GET"]) +def oauth_start(): + state = state_store.issue() + url = authorization_url_generator.generate(state) + return ( + f'' + f'' + ) + + +@app.route("/slack/oauth_redirect", methods=["GET"]) +def oauth_callback(): + # Retrieve the auth code and state from the request params + if "code" in request.args: + state = request.args["state"] + if state_store.consume(state): + code = request.args["code"] + client = WebClient() # no prepared token needed for this app + oauth_response = client.oauth_v2_access(client_id=client_id, client_secret=client_secret, code=code) + logger.info(f"oauth.v2.access response: {oauth_response}") + + installed_enterprise = oauth_response.get("enterprise") or {} + is_enterprise_install = oauth_response.get("is_enterprise_install") + installed_team = oauth_response.get("team") or {} + installer = oauth_response.get("authed_user") or {} + incoming_webhook = oauth_response.get("incoming_webhook") or {} + + bot_token = oauth_response.get("access_token") + # NOTE: oauth.v2.access doesn't include bot_id in response + bot_id = None + enterprise_url = None + if bot_token is not None: + auth_test = client.auth_test(token=bot_token) + bot_id = auth_test["bot_id"] + if is_enterprise_install is True: + enterprise_url = auth_test.get("url") + + installation = Installation( + app_id=oauth_response.get("app_id"), + enterprise_id=installed_enterprise.get("id"), + enterprise_name=installed_enterprise.get("name"), + enterprise_url=enterprise_url, + team_id=installed_team.get("id"), + team_name=installed_team.get("name"), + bot_token=bot_token, + bot_id=bot_id, + bot_user_id=oauth_response.get("bot_user_id"), + bot_scopes=oauth_response.get("scope"), # comma-separated string + user_id=installer.get("id"), + user_token=installer.get("access_token"), + user_scopes=installer.get("scope"), # comma-separated string + incoming_webhook_url=incoming_webhook.get("url"), + incoming_webhook_channel=incoming_webhook.get("channel"), + incoming_webhook_channel_id=incoming_webhook.get("channel_id"), + incoming_webhook_configuration_url=incoming_webhook.get("configuration_url"), + is_enterprise_install=is_enterprise_install, + token_type=oauth_response.get("token_type"), + ) + installation_store.save(installation) + return redirect_page_renderer.render_success_page( + app_id=installation.app_id, + team_id=installation.team_id, + is_enterprise_install=installation.is_enterprise_install, + enterprise_url=installation.enterprise_url, + ) + else: + return redirect_page_renderer.render_failure_page("the state value is already expired") + + error = request.args["error"] if "error" in request.args else "" + return redirect_page_renderer.render_failure_page(error) + + +# --------------------- +# Flask App for Slack events +# --------------------- + +import json +from slack_sdk.errors import SlackApiError +from slack_sdk.signature import SignatureVerifier + +signing_secret = os.environ["SLACK_SIGNING_SECRET"] +signature_verifier = SignatureVerifier(signing_secret=signing_secret) + + +@app.route("/slack/events", methods=["POST"]) +def slack_app(): + data = request.get_data() + if not signature_verifier.is_valid( + body=data, + timestamp=request.headers.get("X-Slack-Request-Timestamp"), + signature=request.headers.get("X-Slack-Signature"), + ): + return make_response("invalid request", 403) + + if data and b"url_verification" in data: + body = json.loads(data) + if body.get("type") == "url_verification" and "challenge" in body: + return make_response(body["challenge"], 200) + + if "command" in request.form and request.form["command"] == "/open-modal": + try: + enterprise_id = request.form.get("enterprise_id") + team_id = request.form["team_id"] + bot = installation_store.find_bot( + enterprise_id=enterprise_id, + team_id=team_id, + ) + bot_token = bot.bot_token if bot else None + if not bot_token: + return make_response("Please install this app first!", 200) + + client = WebClient(token=bot_token) + trigger_id = request.form["trigger_id"] + response = client.views_open( + trigger_id=trigger_id, + view={ + "type": "modal", + "callback_id": "modal-id", + "title": {"type": "plain_text", "text": "Awesome Modal"}, + "submit": {"type": "plain_text", "text": "Submit"}, + "close": {"type": "plain_text", "text": "Cancel"}, + "blocks": [ + { + "type": "input", + "block_id": "b-id", + "label": { + "type": "plain_text", + "text": "Input label", + }, + "element": { + "action_id": "a-id", + "type": "plain_text_input", + }, + } + ], + }, + ) + return make_response("", 200) + except SlackApiError as e: + code = e.response["error"] + return make_response(f"Failed to open a modal due to {code}", 200) + + elif "payload" in request.form: + payload = json.loads(request.form["payload"]) + if payload["type"] == "view_submission" and payload["view"]["callback_id"] == "modal-id": + submitted_data = payload["view"]["state"]["values"] + print(submitted_data) # {'b-id': {'a-id': {'type': 'plain_text_input', 'value': 'your input'}}} + return make_response("", 200) + + return make_response("", 404) + + +if __name__ == "__main__": + # export SLACK_CLIENT_ID=123.123 + # export SLACK_CLIENT_SECRET=xxx + # export SLACK_SIGNING_SECRET=*** + # export FLASK_ENV=development + + app.run("localhost", 3000) + + # python3 integration_tests/samples/oauth/oauth_v2.py + # ngrok http 3000 + # https://{yours}.ngrok.io/slack/install diff --git a/integration_tests/samples/oauth/oauth_v2_async.py b/integration_tests/samples/oauth/oauth_v2_async.py new file mode 100644 index 000000000..588b0b5f0 --- /dev/null +++ b/integration_tests/samples/oauth/oauth_v2_async.py @@ -0,0 +1,211 @@ +# --------------------- +# Sanic App for Slack OAuth flow +# --------------------- +import html +import logging +import os +from slack_sdk.web.async_client import AsyncWebClient +from slack_sdk.oauth import AuthorizeUrlGenerator, RedirectUriPageRenderer +from slack_sdk.oauth.installation_store import FileInstallationStore, Installation +from slack_sdk.oauth.state_store import FileOAuthStateStore + +client_id = os.environ["SLACK_CLIENT_ID"] +client_secret = os.environ["SLACK_CLIENT_SECRET"] +scopes = ["app_mentions:read", "chat:write"] +user_scopes = ["search:read"] + +logger = logging.getLogger(__name__) +logging.basicConfig(level=logging.DEBUG) + +state_store = FileOAuthStateStore(expiration_seconds=300) +installation_store = FileInstallationStore() +authorization_url_generator = AuthorizeUrlGenerator( + client_id=client_id, + scopes=scopes, + user_scopes=user_scopes, +) +redirect_page_renderer = RedirectUriPageRenderer( + install_path="/slack/install", + redirect_uri_path="/slack/oauth_redirect", +) + +# https://sanicframework.org/ +from sanic import Sanic +from sanic.response import json +from sanic.request import Request +from sanic.response import HTTPResponse + +app = Sanic("my-awesome-slack-app") + + +@app.get("/slack/install") +async def oauth_start(req: Request): + state = state_store.issue() + url = authorization_url_generator.generate(state) + return HTTPResponse( + status=200, + body=f'' + f'', + ) + + +@app.get("/slack/oauth_redirect") +async def oauth_callback(req: Request): + # Retrieve the auth code and state from the request params + if "code" in req.args: + state = req.args.get("state") + if state_store.consume(state): + code = req.args.get("code") + client = AsyncWebClient() # no prepared token needed for this app + oauth_response = await client.oauth_v2_access(client_id=client_id, client_secret=client_secret, code=code) + logger.info(f"oauth.v2.access response: {oauth_response}") + + installed_enterprise = oauth_response.get("enterprise") or {} + is_enterprise_install = oauth_response.get("is_enterprise_install") + installed_team = oauth_response.get("team") or {} + installer = oauth_response.get("authed_user") or {} + incoming_webhook = oauth_response.get("incoming_webhook") or {} + bot_token = oauth_response.get("access_token") + # NOTE: oauth.v2.access doesn't include bot_id in response + bot_id = None + if bot_token is not None: + auth_test = await client.auth_test(token=bot_token) + bot_id = auth_test["bot_id"] + + installation = Installation( + app_id=oauth_response.get("app_id"), + enterprise_id=installed_enterprise.get("id"), + team_id=installed_team.get("id"), + bot_token=bot_token, + bot_id=bot_id, + bot_user_id=oauth_response.get("bot_user_id"), + bot_scopes=oauth_response.get("scope"), # comma-separated string + user_id=installer.get("id"), + user_token=installer.get("access_token"), + user_scopes=installer.get("scope"), # comma-separated string + incoming_webhook_url=incoming_webhook.get("url"), + incoming_webhook_channel_id=incoming_webhook.get("channel_id"), + incoming_webhook_configuration_url=incoming_webhook.get("configuration_url"), + is_enterprise_install=is_enterprise_install, + token_type=oauth_response.get("token_type"), + ) + installation_store.save(installation) + html = redirect_page_renderer.render_success_page( + app_id=installation.app_id, + team_id=installation.team_id, + is_enterprise_install=installation.is_enterprise_install, + enterprise_url=installation.enterprise_url, + ) + return HTTPResponse( + status=200, + headers={ + "Content-Type": "text/html; charset=utf-8", + }, + body=html, + ) + else: + html = redirect_page_renderer.render_failure_page("the state value is already expired") + return HTTPResponse( + status=400, + headers={ + "Content-Type": "text/html; charset=utf-8", + }, + body=html, + ) + + error = req.args.get("error") if "error" in req.args else "" + return HTTPResponse( + status=400, + headers={"Content-Type": "text/html; charset=utf-8"}, + body=redirect_page_renderer.render_failure_page(error), + ) + + +# --------------------- +# Sanic App for Slack events +# --------------------- + +import json +from slack_sdk.errors import SlackApiError +from slack_sdk.signature import SignatureVerifier + +signing_secret = os.environ["SLACK_SIGNING_SECRET"] +signature_verifier = SignatureVerifier(signing_secret=signing_secret) + + +@app.post("/slack/events") +async def slack_app(req: Request): + data = req.body.decode("utf-8") + if not signature_verifier.is_valid( + body=data, + timestamp=req.headers.get("X-Slack-Request-Timestamp"), + signature=req.headers.get("X-Slack-Signature"), + ): + return HTTPResponse(status=403, body="invalid request") + + if data and "url_verification" in data: + body = json.loads(data) + if body.get("type") == "url_verification" and "challenge" in body: + return HTTPResponse(status=200, body=body["challenge"]) + + if "command" in req.form and req.form.get("command") == "/open-modal": + try: + enterprise_id = req.form.get("enterprise_id") + team_id = req.form.get("team_id") + bot = installation_store.find_bot( + enterprise_id=enterprise_id, + team_id=team_id, + ) + bot_token = bot.bot_token if bot else None + if not bot_token: + return HTTPResponse(status=200, body="Please install this app first!") + + client = AsyncWebClient(token=bot_token) + await client.views_open( + trigger_id=req.form.get("trigger_id"), + view={ + "type": "modal", + "callback_id": "modal-id", + "title": {"type": "plain_text", "text": "Awesome Modal"}, + "submit": {"type": "plain_text", "text": "Submit"}, + "close": {"type": "plain_text", "text": "Cancel"}, + "blocks": [ + { + "type": "input", + "block_id": "b-id", + "label": { + "type": "plain_text", + "text": "Input label", + }, + "element": { + "action_id": "a-id", + "type": "plain_text_input", + }, + } + ], + }, + ) + return HTTPResponse(status=200, body="") + except SlackApiError as e: + code = e.response["error"] + return HTTPResponse(status=200, body=f"Failed to open a modal due to {code}") + + elif "payload" in req.form: + payload = json.loads(req.form.get("payload")) + if payload.get("type") == "view_submission" and payload.get("view").get("callback_id") == "modal-id": + submitted_data = payload.get("view").get("state").get("values") + print(submitted_data) # {'b-id': {'a-id': {'type': 'plain_text_input', 'value': 'your input'}}} + return HTTPResponse(status=200, body="") + + return HTTPResponse(status=404, body="Not found") + + +if __name__ == "__main__": + # export SLACK_CLIENT_ID=123.123 + # export SLACK_CLIENT_SECRET=xxx + # export SLACK_SIGNING_SECRET=*** + + app.run(host="0.0.0.0", port=3000) + # python3 integration_tests/samples/oauth/oauth_v2_async.py + # ngrok http 3000 + # https://{yours}.ngrok.io/slack/install diff --git a/integration_tests/samples/openid_connect/app_manifest.yml b/integration_tests/samples/openid_connect/app_manifest.yml new file mode 100644 index 000000000..12eae76fa --- /dev/null +++ b/integration_tests/samples/openid_connect/app_manifest.yml @@ -0,0 +1,22 @@ +_metadata: + major_version: 1 + minor_version: 1 +display_information: + name: openid-connect-app +features: + app_home: + home_tab_enabled: false + messages_tab_enabled: true + messages_tab_read_only_enabled: true +oauth_config: + redirect_urls: + - https://{your-domain}/slack/oauth_redirect + scopes: + user: + - openid + - email + - profile +settings: + org_deploy_enabled: false + socket_mode_enabled: false + token_rotation_enabled: false diff --git a/integration_tests/samples/openid_connect/flask_example.py b/integration_tests/samples/openid_connect/flask_example.py new file mode 100644 index 000000000..644ed31dd --- /dev/null +++ b/integration_tests/samples/openid_connect/flask_example.py @@ -0,0 +1,107 @@ +import json +import logging +import os +import jwt + +logger = logging.getLogger(__name__) +logging.basicConfig(level=logging.DEBUG) + +client_id = os.environ["SLACK_CLIENT_ID"] +client_secret = os.environ["SLACK_CLIENT_SECRET"] +redirect_uri = os.environ["SLACK_REDIRECT_URI"] +scopes = ["openid", "email", "profile"] + +from slack_sdk.web import WebClient +from slack_sdk.oauth import OpenIDConnectAuthorizeUrlGenerator, RedirectUriPageRenderer +from slack_sdk.oauth.state_store import FileOAuthStateStore + +state_store = FileOAuthStateStore(expiration_seconds=300) + +authorization_url_generator = OpenIDConnectAuthorizeUrlGenerator( + client_id=client_id, + scopes=scopes, + redirect_uri=redirect_uri, +) +redirect_page_renderer = RedirectUriPageRenderer( + install_path="/slack/install", + redirect_uri_path="/slack/oauth_redirect", +) + + +from flask import Flask, request, make_response + +app = Flask(__name__) +app.debug = True + + +@app.route("/slack/install", methods=["GET"]) +def oauth_start(): + state = state_store.issue() + url = authorization_url_generator.generate(state=state) + return ( + '' + f'Sign in with Slack' + "" + ) + + +@app.route("/slack/oauth_redirect", methods=["GET"]) +def oauth_callback(): + # Retrieve the auth code and state from the request params + if "code" in request.args: + state = request.args["state"] + if state_store.consume(state): + code = request.args["code"] + try: + token_response = WebClient().openid_connect_token( + client_id=client_id, client_secret=client_secret, code=code + ) + logger.info(f"openid.connect.token response: {token_response}") + id_token = token_response.get("id_token") + claims = jwt.decode(id_token, options={"verify_signature": False}, algorithms=["RS256"]) + logger.info(f"claims (decoded id_token): {claims}") + + user_token = token_response.get("access_token") + user_info_response = WebClient(token=user_token).openid_connect_userInfo() + logger.info(f"openid.connect.userInfo response: {user_info_response}") + return f""" + + + + + +

OpenID Connect Claims

+
{json.dumps(claims, indent=2)}
+

openid.connect.userInfo response

+
{json.dumps(user_info_response.data, indent=2)}
+ + + """ + + except Exception: + logger.exception("Failed to perform openid.connect.token API call") + return redirect_page_renderer.render_failure_page("Failed to perform openid.connect.token API call") + else: + return redirect_page_renderer.render_failure_page("The state value is already expired") + + error = request.args["error"] if "error" in request.args else "" + return redirect_page_renderer.render_failure_page(error) + + +if __name__ == "__main__": + # export SLACK_CLIENT_ID=111.222 + # export SLACK_CLIENT_SECRET=xxx + # export FLASK_ENV=development + # export SLACK_REDIRECT_URI=https://{your-domain}/slack/oauth_redirect + # python3 integration_tests/samples/openid_connect/flask_example.py + + app.run("localhost", 3000) + + # ngrok http 3000 + # https://{yours}.ngrok.io/slack/install diff --git a/integration_tests/samples/openid_connect/requirements.txt b/integration_tests/samples/openid_connect/requirements.txt new file mode 100644 index 000000000..c9aae7b20 --- /dev/null +++ b/integration_tests/samples/openid_connect/requirements.txt @@ -0,0 +1,5 @@ +slack-sdk +Flask>=2,<3 +pyjwt>=2.1,<3 +cryptography>=3.4,<4 +Sanic>=21.3 \ No newline at end of file diff --git a/integration_tests/samples/openid_connect/sanic_example.py b/integration_tests/samples/openid_connect/sanic_example.py new file mode 100644 index 000000000..a7d0f645e --- /dev/null +++ b/integration_tests/samples/openid_connect/sanic_example.py @@ -0,0 +1,128 @@ +import json +import jwt +import logging +import os + +logger = logging.getLogger(__name__) +logging.basicConfig(level=logging.DEBUG) + +client_id = os.environ["SLACK_CLIENT_ID"] +client_secret = os.environ["SLACK_CLIENT_SECRET"] +redirect_uri = os.environ["SLACK_REDIRECT_URI"] +scopes = ["openid", "email", "profile"] + +from slack_sdk.web.async_client import AsyncWebClient +from slack_sdk.oauth import OpenIDConnectAuthorizeUrlGenerator, RedirectUriPageRenderer +from slack_sdk.oauth.state_store import FileOAuthStateStore + +state_store = FileOAuthStateStore(expiration_seconds=300) +authorization_url_generator = OpenIDConnectAuthorizeUrlGenerator( + client_id=client_id, + scopes=scopes, + redirect_uri=redirect_uri, +) +redirect_page_renderer = RedirectUriPageRenderer( + install_path="/slack/install", + redirect_uri_path="/slack/oauth_redirect", +) + + +# https://sanicframework.org/ +from sanic import Sanic +from sanic.request import Request +from sanic.response import HTTPResponse + +app = Sanic("my-awesome-slack-app") + + +@app.get("/slack/install") +async def oauth_start(req: Request): + state = state_store.issue() + url = authorization_url_generator.generate(state) + response_body = ( + '' + f'Sign in with Slack' + "" + ) + return HTTPResponse( + status=200, + body=response_body, + ) + + +@app.get("/slack/oauth_redirect") +async def oauth_callback(req: Request): + # Retrieve the auth code and state from the request params + if "code" in req.args: + state = req.args.get("state") + if state_store.consume(state): + code = req.args.get("code") + try: + token_response = await AsyncWebClient().openid_connect_token( + client_id=client_id, client_secret=client_secret, code=code + ) + logger.info(f"openid.connect.token response: {token_response}") + id_token = token_response.get("id_token") + claims = jwt.decode(id_token, options={"verify_signature": False}, algorithms=["RS256"]) + logger.info(f"claims (decoded id_token): {claims}") + + user_token = token_response.get("access_token") + user_info_response = await AsyncWebClient(token=user_token).openid_connect_userInfo() + logger.info(f"openid.connect.userInfo response: {user_info_response}") + html = f""" + + + + + +

OpenID Connect Claims

+
{json.dumps(claims, indent=2)}
+

openid.connect.userInfo response

+
{json.dumps(user_info_response.data, indent=2)}
+ + + """ + return HTTPResponse( + status=200, + headers={ + "Content-Type": "text/html; charset=utf-8", + }, + body=html, + ) + + except Exception: + logger.exception("Failed to perform openid.connect.token API call") + html = redirect_page_renderer.render_failure_page("Failed to perform openid.connect.token API call") + return HTTPResponse( + status=400, + headers={"Content-Type": "text/html; charset=utf-8"}, + body=html, + ) + + else: + html = redirect_page_renderer.render_failure_page("The state value is already expired") + return HTTPResponse( + status=400, + headers={"Content-Type": "text/html; charset=utf-8"}, + body=html, + ) + + error = req.args.get("error") if "error" in req.args else "" + return HTTPResponse( + status=400, + headers={"Content-Type": "text/html; charset=utf-8"}, + body=redirect_page_renderer.render_failure_page(error), + ) + + +if __name__ == "__main__": + app.run(host="0.0.0.0", port=3000) + # python3 integration_tests/samples/openid_connect/sanic_example.py + # ngrok http 3000 + # https://{yours}.ngrok.io/slack/install diff --git a/integration_tests/samples/readme/async_function_in_framework.py b/integration_tests/samples/readme/async_function_in_framework.py new file mode 100644 index 000000000..6b317462a --- /dev/null +++ b/integration_tests/samples/readme/async_function_in_framework.py @@ -0,0 +1,49 @@ +import logging + +logging.basicConfig(level=logging.DEBUG) + +# export SLACK_API_TOKEN=xoxb-*** +# pip3 install sanic +# python3 integration_tests/samples/readme/async_function_in_framework.py + +import os +from slack_sdk.web.async_client import AsyncWebClient +from slack_sdk.errors import SlackApiError + +client = AsyncWebClient(token=os.environ["SLACK_API_TOKEN"]) + + +# Define this as an async function +async def send_to_slack(channel, text): + try: + # Don't forget to have await as the client returns asyncio.Future + response = await client.chat_postMessage(channel=channel, text=text) + assert response["message"]["text"] == text + except SlackApiError as e: + assert e.response["ok"] is False + assert e.response["error"] # str like 'invalid_auth', 'channel_not_found' + raise e + + +# https://sanicframework.org/ +from sanic import Sanic +from sanic.response import json + +app = Sanic() + + +# e.g., http://localhost:3000/?text=foo&text=bar +@app.route("/") +async def test(request): + text = "Hello World!" + if "text" in request.args: + text = "\t".join(request.args["text"]) + try: + await send_to_slack(channel="#random", text=text) + return json({"message": "Done!"}) + except SlackApiError as e: + return json({"message": f"Failed due to {e.response['error']}"}) + + +if __name__ == "__main__": + app.run(host="0.0.0.0", port=3000) diff --git a/integration_tests/samples/readme/async_script.py b/integration_tests/samples/readme/async_script.py new file mode 100644 index 000000000..88ee6b500 --- /dev/null +++ b/integration_tests/samples/readme/async_script.py @@ -0,0 +1,26 @@ +import logging + +logging.basicConfig(level=logging.DEBUG) + +# export SLACK_API_TOKEN=xoxb-*** +# python3 integration_tests/samples/readme/async_script.py + +import asyncio +import os +from slack_sdk.web.async_client import AsyncWebClient +from slack_sdk.errors import SlackApiError + +client = AsyncWebClient(token=os.environ["SLACK_API_TOKEN"]) +future = client.chat_postMessage(channel="#random", text="Hello world!") + +loop = asyncio.get_event_loop() +try: + # run_until_complete returns the Future's result, or raise its exception. + response = loop.run_until_complete(future) + assert response["message"]["text"] == "Hello world!" +except SlackApiError as e: + assert e.response["ok"] is False + assert e.response["error"] # str like 'invalid_auth', 'channel_not_found' + print(f"Got an error: {e.response['error']}") +finally: + loop.close() diff --git a/integration_tests/samples/readme/proxy.py b/integration_tests/samples/readme/proxy.py new file mode 100644 index 000000000..f667d007a --- /dev/null +++ b/integration_tests/samples/readme/proxy.py @@ -0,0 +1,19 @@ +import logging + +logging.basicConfig(level=logging.DEBUG) + +# export SLACK_API_TOKEN=xoxb-*** +# python3 integration_tests/samples/readme/proxy.py + +import os +from slack_sdk.web import WebClient +from ssl import SSLContext + +sslcert = SSLContext() +# pip3 install proxy.py +# proxy --port 9000 --log-level d +proxyinfo = "http://localhost:9000" + +client = WebClient(token=os.environ["SLACK_API_TOKEN"], ssl=sslcert, proxy=proxyinfo) +response = client.chat_postMessage(channel="#random", text="Hello World!") +print(response) diff --git a/integration_tests/samples/readme/rtm_client_basics.py b/integration_tests/samples/readme/rtm_client_basics.py new file mode 100644 index 000000000..c97df1115 --- /dev/null +++ b/integration_tests/samples/readme/rtm_client_basics.py @@ -0,0 +1,33 @@ +import logging + +logging.basicConfig(level=logging.DEBUG) + +# export SLACK_API_TOKEN=xoxb-*** +# python3 integration_tests/samples/readme/rtm_client_basics.py + +import os +from slack_sdk.rtm import RTMClient +from slack_sdk.errors import SlackApiError + + +@RTMClient.run_on(event="message") +def say_hello(**payload): + data = payload["data"] + web_client = payload["web_client"] + rtm_client = payload["rtm_client"] + if "text" in data and "Hello" in data.get("text", []): + channel_id = data["channel"] + thread_ts = data["ts"] + user = data["user"] + + try: + response = web_client.chat_postMessage(channel=channel_id, text=f"Hi <@{user}>!", thread_ts=thread_ts) + except SlackApiError as e: + # You will get a SlackApiError if "ok" is False + assert e.response["ok"] is False + assert e.response["error"] # str like 'invalid_auth', 'channel_not_found' + print(f"Got an error: {e.response['error']}") + + +rtm_client = RTMClient(token=os.environ["SLACK_API_TOKEN"]) +rtm_client.start() diff --git a/integration_tests/samples/readme/sending_messages.py b/integration_tests/samples/readme/sending_messages.py new file mode 100644 index 000000000..86728df3d --- /dev/null +++ b/integration_tests/samples/readme/sending_messages.py @@ -0,0 +1,21 @@ +import logging + +logging.basicConfig(level=logging.DEBUG) + +# export SLACK_API_TOKEN=xoxb-*** +# python3 integration_tests/samples/readme/sending_messages.py + +import os +from slack_sdk.web import WebClient +from slack_sdk.errors import SlackApiError + +client = WebClient(token=os.environ["SLACK_API_TOKEN"]) + +try: + response = client.chat_postMessage(channel="#random", text="Hello world!") + assert response["message"]["text"] == "Hello world!" +except SlackApiError as e: + # You will get a SlackApiError if "ok" is False + assert e.response["ok"] is False + assert e.response["error"] # str like 'invalid_auth', 'channel_not_found' + print(f"Got an error: {e.response['error']}") diff --git a/integration_tests/samples/readme/uploading_files.py b/integration_tests/samples/readme/uploading_files.py new file mode 100644 index 000000000..9767c1add --- /dev/null +++ b/integration_tests/samples/readme/uploading_files.py @@ -0,0 +1,23 @@ +import logging + +logging.basicConfig(level=logging.DEBUG) + +# export SLACK_API_TOKEN=xoxb-*** +# echo 'Hello world!' > tmp.txt +# python3 integration_tests/samples/readme/uploading_files.py + +import os +from slack_sdk.web import WebClient +from slack_sdk.errors import SlackApiError + +client = WebClient(token=os.environ["SLACK_API_TOKEN"]) + +try: + filepath = "./tmp.txt" + response = client.files_upload(channels="#random", file=filepath) + assert response["file"] # the uploaded file +except SlackApiError as e: + # You will get a SlackApiError if "ok" is False + assert e.response["ok"] is False + assert e.response["error"] # str like 'invalid_auth', 'channel_not_found' + print(f"Got an error: {e.response['error']}") diff --git a/integration_tests/samples/rtm_v2/rtm_v2_app.py b/integration_tests/samples/rtm_v2/rtm_v2_app.py new file mode 100644 index 000000000..f67f5e80a --- /dev/null +++ b/integration_tests/samples/rtm_v2/rtm_v2_app.py @@ -0,0 +1,33 @@ +import logging + +logging.basicConfig( + level=logging.DEBUG, + format="%(asctime)s.%(msecs)03d %(levelname)s %(filename)s (%(lineno)s): %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", +) +logger = logging.getLogger(__name__) + +import os +from slack_sdk.rtm.v2 import RTMClient +from integration_tests.env_variable_names import SLACK_SDK_TEST_CLASSIC_APP_BOT_TOKEN + +if __name__ == "__main__": + rtm = RTMClient( + token=os.environ.get(SLACK_SDK_TEST_CLASSIC_APP_BOT_TOKEN), + trace_enabled=True, + all_message_trace_enabled=True, + ) + + @rtm.on("message") + def handle(client: RTMClient, event: dict): + client.web_client.reactions_add( + channel=event["channel"], + timestamp=event["ts"], + name="eyes", + ) + + @rtm.on("*") + def handle(client: RTMClient, event: dict): + logger.info(event) + + rtm.start() diff --git a/integration_tests/samples/rtm_v2/rtm_v2_proxy_app.py b/integration_tests/samples/rtm_v2/rtm_v2_proxy_app.py new file mode 100644 index 000000000..0f8e5e8b9 --- /dev/null +++ b/integration_tests/samples/rtm_v2/rtm_v2_proxy_app.py @@ -0,0 +1,38 @@ +import logging + +logging.basicConfig( + level=logging.DEBUG, + format="%(asctime)s.%(msecs)03d %(levelname)s %(filename)s (%(lineno)s): %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", +) +logger = logging.getLogger(__name__) + +import os +from slack_sdk.rtm.v2 import RTMClient +from integration_tests.env_variable_names import SLACK_SDK_TEST_CLASSIC_APP_BOT_TOKEN + +# pip3 install proxy.py +# proxy --port 9000 --log-level d +proxy_url = "http://localhost:9000" + +if __name__ == "__main__": + rtm = RTMClient( + token=os.environ.get(SLACK_SDK_TEST_CLASSIC_APP_BOT_TOKEN), + trace_enabled=True, + all_message_trace_enabled=True, + proxy=proxy_url, + ) + + @rtm.on("message") + def handle(client: RTMClient, event: dict): + client.web_client.reactions_add( + channel=event["channel"], + timestamp=event["ts"], + name="eyes", + ) + + @rtm.on("*") + def handle(client: RTMClient, event: dict): + logger.info(event) + + rtm.start() diff --git a/integration_tests/samples/scim/search_groups.py b/integration_tests/samples/scim/search_groups.py new file mode 100644 index 000000000..1819d4ffa --- /dev/null +++ b/integration_tests/samples/scim/search_groups.py @@ -0,0 +1,12 @@ +import logging + +logging.basicConfig(level=logging.DEBUG) + +import os +from slack_sdk.scim import SCIMClient + +client = SCIMClient(token=os.environ["SLACK_SDK_TEST_GRID_ORG_ADMIN_USER_TOKEN"]) + +response = client.search_groups(start_index=1, count=2) +print("-----------------------") +print(response.groups) diff --git a/integration_tests/samples/scim/search_groups_async.py b/integration_tests/samples/scim/search_groups_async.py new file mode 100644 index 000000000..b66ca4112 --- /dev/null +++ b/integration_tests/samples/scim/search_groups_async.py @@ -0,0 +1,18 @@ +import logging + +logging.basicConfig(level=logging.DEBUG) + +import asyncio +import os +from slack_sdk.scim.async_client import AsyncSCIMClient + +client = AsyncSCIMClient(token=os.environ["SLACK_SDK_TEST_GRID_ORG_ADMIN_USER_TOKEN"]) + + +async def main(): + response = await client.search_groups(start_index=1, count=2) + print("-----------------------") + print(response.groups) + + +asyncio.run(main()) diff --git a/integration_tests/samples/scim/search_users.py b/integration_tests/samples/scim/search_users.py new file mode 100644 index 000000000..c0fb156de --- /dev/null +++ b/integration_tests/samples/scim/search_users.py @@ -0,0 +1,12 @@ +import logging + +logging.basicConfig(level=logging.DEBUG) + +import os +from slack_sdk.scim import SCIMClient + +client = SCIMClient(token=os.environ["SLACK_SDK_TEST_GRID_ORG_ADMIN_USER_TOKEN"]) + +response = client.search_users(start_index=1, count=2) +print("-----------------------") +print(response.users) diff --git a/integration_tests/samples/socket_mode/__init__.py b/integration_tests/samples/socket_mode/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/integration_tests/samples/socket_mode/aiohttp_example.py b/integration_tests/samples/socket_mode/aiohttp_example.py new file mode 100644 index 000000000..37147dd3b --- /dev/null +++ b/integration_tests/samples/socket_mode/aiohttp_example.py @@ -0,0 +1,36 @@ +import logging + +logging.basicConfig(level=logging.DEBUG) + +import asyncio +import os +from slack_sdk.web.async_client import AsyncWebClient +from slack_sdk.socket_mode.response import SocketModeResponse +from slack_sdk.socket_mode.request import SocketModeRequest +from slack_sdk.socket_mode.aiohttp import SocketModeClient + + +async def main(): + client = SocketModeClient( + app_token=os.environ.get("SLACK_SDK_TEST_SOCKET_MODE_APP_TOKEN"), + web_client=AsyncWebClient(token=os.environ.get("SLACK_SDK_TEST_SOCKET_MODE_BOT_TOKEN")), + trace_enabled=True, + ) + + async def process(client: SocketModeClient, req: SocketModeRequest): + if req.type == "events_api": + response = SocketModeResponse(envelope_id=req.envelope_id) + await client.send_socket_mode_response(response) + if req.payload["event"]["type"] == "message": + await client.web_client.reactions_add( + name="eyes", + channel=req.payload["event"]["channel"], + timestamp=req.payload["event"]["ts"], + ) + + client.socket_mode_request_listeners.append(process) + await client.connect() + await asyncio.sleep(float("inf")) + + +asyncio.run(main()) diff --git a/integration_tests/samples/socket_mode/bolt_adapter/__init__.py b/integration_tests/samples/socket_mode/bolt_adapter/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/integration_tests/samples/socket_mode/bolt_adapter/aiohttp.py b/integration_tests/samples/socket_mode/bolt_adapter/aiohttp.py new file mode 100644 index 000000000..3ea4e7695 --- /dev/null +++ b/integration_tests/samples/socket_mode/bolt_adapter/aiohttp.py @@ -0,0 +1,58 @@ +import os +from time import time +from typing import Optional + +from slack_sdk.socket_mode.aiohttp import SocketModeClient +from slack_sdk.socket_mode.request import SocketModeRequest + +from slack_bolt import App +from .async_base_handler import AsyncBaseSocketModeHandler +from .async_internals import ( + send_async_response, + run_async_bolt_app, +) +from .internals import run_bolt_app +from slack_bolt.app.async_app import AsyncApp +from slack_bolt.response import BoltResponse + + +class SocketModeHandler(AsyncBaseSocketModeHandler): + app: App # type: ignore + app_token: str + client: SocketModeClient + + def __init__( # type: ignore + self, + app: App, # type: ignore + app_token: Optional[str] = None, + ): + self.app = app + self.app_token = app_token or os.environ["SLACK_APP_TOKEN"] + self.client = SocketModeClient(app_token=self.app_token) + self.client.socket_mode_request_listeners.append(self.handle) + + async def handle(self, client: SocketModeClient, req: SocketModeRequest) -> None: + start = time() + bolt_resp: BoltResponse = run_bolt_app(self.app, req) + await send_async_response(client, req, bolt_resp, start) + + +class AsyncSocketModeHandler(AsyncBaseSocketModeHandler): + app: AsyncApp # type: ignore + app_token: str + client: SocketModeClient + + def __init__( # type: ignore + self, + app: AsyncApp, # type: ignore + app_token: Optional[str] = None, + ): + self.app = app + self.app_token = app_token or os.environ["SLACK_APP_TOKEN"] + self.client = SocketModeClient(app_token=self.app_token) + self.client.socket_mode_request_listeners.append(self.handle) + + async def handle(self, client: SocketModeClient, req: SocketModeRequest) -> None: + start = time() + bolt_resp: BoltResponse = await run_async_bolt_app(self.app, req) + await send_async_response(client, req, bolt_resp, start) diff --git a/integration_tests/samples/socket_mode/bolt_adapter/async_base_handler.py b/integration_tests/samples/socket_mode/bolt_adapter/async_base_handler.py new file mode 100644 index 000000000..36311318d --- /dev/null +++ b/integration_tests/samples/socket_mode/bolt_adapter/async_base_handler.py @@ -0,0 +1,34 @@ +import asyncio +import logging +from typing import Union + +from slack_sdk.socket_mode.async_client import AsyncBaseSocketModeClient +from slack_sdk.socket_mode.request import SocketModeRequest + +from slack_bolt import App +from slack_bolt.app.async_app import AsyncApp + + +class AsyncBaseSocketModeHandler: + app: Union[App, AsyncApp] # type: ignore + client: AsyncBaseSocketModeClient + + async def handle(self, client: AsyncBaseSocketModeClient, req: SocketModeRequest) -> None: + raise NotImplementedError() + + async def connect_async(self): + await self.client.connect() + + async def disconnect_async(self): + await self.client.disconnect() + + async def close_async(self): + await self.client.close() + + async def start_async(self): + await self.connect_async() + if self.app.logger.level > logging.INFO: + print("⚡️ Bolt app is running!") + else: + self.app.logger.info("⚡️ Bolt app is running!") + await asyncio.sleep(float("inf")) diff --git a/integration_tests/samples/socket_mode/bolt_adapter/async_internals.py b/integration_tests/samples/socket_mode/bolt_adapter/async_internals.py new file mode 100644 index 000000000..d80968050 --- /dev/null +++ b/integration_tests/samples/socket_mode/bolt_adapter/async_internals.py @@ -0,0 +1,44 @@ +import json +import logging +from time import time + +from slack_sdk.socket_mode.async_client import AsyncBaseSocketModeClient +from slack_sdk.socket_mode.request import SocketModeRequest +from slack_sdk.socket_mode.response import SocketModeResponse + +from slack_bolt.app.async_app import AsyncApp +from slack_bolt.request.async_request import AsyncBoltRequest +from slack_bolt.response import BoltResponse + + +async def run_async_bolt_app(app: AsyncApp, req: SocketModeRequest): # type: ignore + bolt_req: AsyncBoltRequest = AsyncBoltRequest(mode="socket_mode", body=req.payload) + bolt_resp: BoltResponse = await app.async_dispatch(bolt_req) + return bolt_resp + + +async def send_async_response( + client: AsyncBaseSocketModeClient, + req: SocketModeRequest, + bolt_resp: BoltResponse, + start_time: float, +): + if bolt_resp.status == 200: + content_type = bolt_resp.headers.get("content-type", [""])[0] + if bolt_resp.body is None or len(bolt_resp.body) == 0: + await client.send_socket_mode_response(SocketModeResponse(envelope_id=req.envelope_id)) + elif content_type.startswith("application/json"): + dict_body = json.loads(bolt_resp.body) + await client.send_socket_mode_response(SocketModeResponse(envelope_id=req.envelope_id, payload=dict_body)) + else: + await client.send_socket_mode_response( + SocketModeResponse( + envelope_id=req.envelope_id, + payload={"text": bolt_resp.body}, + ) + ) + if client.logger.level <= logging.DEBUG: + spent_time = int((time() - start_time) * 1000) + client.logger.debug(f"Response time: {spent_time} milliseconds") + else: + client.logger.info(f"Unsuccessful Bolt execution result (status: {bolt_resp.status}, body: {bolt_resp.body})") diff --git a/integration_tests/samples/socket_mode/bolt_adapter/base_handler.py b/integration_tests/samples/socket_mode/bolt_adapter/base_handler.py new file mode 100644 index 000000000..1cc6b2cc1 --- /dev/null +++ b/integration_tests/samples/socket_mode/bolt_adapter/base_handler.py @@ -0,0 +1,32 @@ +import logging +from threading import Event + +from slack_sdk.socket_mode.client import BaseSocketModeClient +from slack_sdk.socket_mode.request import SocketModeRequest + +from slack_bolt import App + + +class BaseSocketModeHandler: + app: App # type: ignore + client: BaseSocketModeClient + + def handle(self, client: BaseSocketModeClient, req: SocketModeRequest) -> None: + raise NotImplementedError() + + def connect(self): + self.client.connect() + + def disconnect(self): + self.client.disconnect() + + def close(self): + self.client.close() + + def start(self): + self.connect() + if self.app.logger.level > logging.INFO: + print("⚡️ Bolt app is running!") + else: + self.app.logger.info("⚡️ Bolt app is running!") + Event().wait() diff --git a/integration_tests/samples/socket_mode/bolt_adapter/builtin.py b/integration_tests/samples/socket_mode/bolt_adapter/builtin.py new file mode 100644 index 000000000..06f014f59 --- /dev/null +++ b/integration_tests/samples/socket_mode/bolt_adapter/builtin.py @@ -0,0 +1,32 @@ +import os +from time import time +from typing import Optional + +from slack_sdk.socket_mode.request import SocketModeRequest +from slack_sdk.socket_mode.builtin import SocketModeClient + +from slack_bolt import App +from .base_handler import BaseSocketModeHandler +from .internals import run_bolt_app, send_response +from slack_bolt.response import BoltResponse + + +class SocketModeHandler(BaseSocketModeHandler): + app: App # type: ignore + app_token: str + client: SocketModeClient + + def __init__( # type: ignore + self, + app: App, # type: ignore + app_token: Optional[str] = None, + ): + self.app = app + self.app_token = app_token or os.environ["SLACK_APP_TOKEN"] + self.client = SocketModeClient(app_token=self.app_token) + self.client.socket_mode_request_listeners.append(self.handle) + + def handle(self, client: SocketModeClient, req: SocketModeRequest) -> None: + start = time() + bolt_resp: BoltResponse = run_bolt_app(self.app, req) + send_response(client, req, bolt_resp, start) diff --git a/integration_tests/samples/socket_mode/bolt_adapter/internals.py b/integration_tests/samples/socket_mode/bolt_adapter/internals.py new file mode 100644 index 000000000..065bb0204 --- /dev/null +++ b/integration_tests/samples/socket_mode/bolt_adapter/internals.py @@ -0,0 +1,42 @@ +import json +import logging +from time import time + +from slack_sdk.socket_mode.client import BaseSocketModeClient +from slack_sdk.socket_mode.request import SocketModeRequest +from slack_sdk.socket_mode.response import SocketModeResponse + +from slack_bolt.app import App +from slack_bolt.request import BoltRequest +from slack_bolt.response import BoltResponse + + +def run_bolt_app(app: App, req: SocketModeRequest): # type: ignore + bolt_req: BoltRequest = BoltRequest(mode="socket_mode", body=req.payload) + bolt_resp: BoltResponse = app.dispatch(bolt_req) + return bolt_resp + + +def send_response( + client: BaseSocketModeClient, + req: SocketModeRequest, + bolt_resp: BoltResponse, + start_time: float, +): + if bolt_resp.status == 200: + content_type = bolt_resp.headers.get("content-type", [""])[0] + if bolt_resp.body is None or len(bolt_resp.body) == 0: + client.send_socket_mode_response(SocketModeResponse(envelope_id=req.envelope_id)) + elif content_type.startswith("application/json"): + dict_body = json.loads(bolt_resp.body) + client.send_socket_mode_response(SocketModeResponse(envelope_id=req.envelope_id, payload=dict_body)) + else: + client.send_socket_mode_response( + SocketModeResponse(envelope_id=req.envelope_id, payload={"text": bolt_resp.body}) + ) + + if client.logger.level <= logging.DEBUG: + spent_time = int((time() - start_time) * 1000) + client.logger.debug(f"Response time: {spent_time} milliseconds") + else: + client.logger.info(f"Unsuccessful Bolt execution result (status: {bolt_resp.status}, body: {bolt_resp.body})") diff --git a/integration_tests/samples/socket_mode/bolt_adapter/websocket_client.py b/integration_tests/samples/socket_mode/bolt_adapter/websocket_client.py new file mode 100644 index 000000000..850e534f4 --- /dev/null +++ b/integration_tests/samples/socket_mode/bolt_adapter/websocket_client.py @@ -0,0 +1,32 @@ +import os +from time import time +from typing import Optional + +from slack_sdk.socket_mode.request import SocketModeRequest +from slack_sdk.socket_mode.websocket_client import SocketModeClient + +from slack_bolt import App +from .base_handler import BaseSocketModeHandler +from .internals import run_bolt_app, send_response +from slack_bolt.response import BoltResponse + + +class SocketModeHandler(BaseSocketModeHandler): + app: App # type: ignore + app_token: str + client: SocketModeClient + + def __init__( # type: ignore + self, + app: App, # type: ignore + app_token: Optional[str] = None, + ): + self.app = app + self.app_token = app_token or os.environ["SLACK_APP_TOKEN"] + self.client = SocketModeClient(app_token=self.app_token) + self.client.socket_mode_request_listeners.append(self.handle) + + def handle(self, client: SocketModeClient, req: SocketModeRequest) -> None: + start = time() + bolt_resp: BoltResponse = run_bolt_app(self.app, req) + send_response(client, req, bolt_resp, start) diff --git a/integration_tests/samples/socket_mode/bolt_adapter/websockets.py b/integration_tests/samples/socket_mode/bolt_adapter/websockets.py new file mode 100644 index 000000000..2567014a6 --- /dev/null +++ b/integration_tests/samples/socket_mode/bolt_adapter/websockets.py @@ -0,0 +1,58 @@ +import os +from time import time +from typing import Optional + +from slack_sdk.socket_mode.websockets import SocketModeClient +from slack_sdk.socket_mode.request import SocketModeRequest + +from slack_bolt import App +from .async_base_handler import AsyncBaseSocketModeHandler +from .async_internals import ( + send_async_response, + run_async_bolt_app, +) +from .internals import run_bolt_app +from slack_bolt.app.async_app import AsyncApp +from slack_bolt.response import BoltResponse + + +class SocketModeHandler(AsyncBaseSocketModeHandler): + app: App # type: ignore + app_token: str + client: SocketModeClient + + def __init__( # type: ignore + self, + app: App, # type: ignore + app_token: Optional[str] = None, + ): + self.app = app + self.app_token = app_token or os.environ["SLACK_APP_TOKEN"] + self.client = SocketModeClient(app_token=self.app_token) + self.client.socket_mode_request_listeners.append(self.handle) + + async def handle(self, client: SocketModeClient, req: SocketModeRequest) -> None: + start = time() + bolt_resp: BoltResponse = run_bolt_app(self.app, req) + await send_async_response(client, req, bolt_resp, start) + + +class AsyncSocketModeHandler(AsyncBaseSocketModeHandler): + app: AsyncApp # type: ignore + app_token: str + client: SocketModeClient + + def __init__( # type: ignore + self, + app: AsyncApp, # type: ignore + app_token: Optional[str] = None, + ): + self.app = app + self.app_token = app_token or os.environ["SLACK_APP_TOKEN"] + self.client = SocketModeClient(app_token=self.app_token) + self.client.socket_mode_request_listeners.append(self.handle) + + async def handle(self, client: SocketModeClient, req: SocketModeRequest) -> None: + start = time() + bolt_resp: BoltResponse = await run_async_bolt_app(self.app, req) + await send_async_response(client, req, bolt_resp, start) diff --git a/integration_tests/samples/socket_mode/bolt_aiohttp_async_example.py b/integration_tests/samples/socket_mode/bolt_aiohttp_async_example.py new file mode 100644 index 000000000..9c392e35f --- /dev/null +++ b/integration_tests/samples/socket_mode/bolt_aiohttp_async_example.py @@ -0,0 +1,49 @@ +import logging + +logging.basicConfig(level=logging.DEBUG) + +import os +from slack_bolt.app.async_app import AsyncApp +from slack_bolt.context.async_context import AsyncBoltContext + +bot_token = os.environ.get("SLACK_SDK_TEST_SOCKET_MODE_BOT_TOKEN") +app = AsyncApp(signing_secret="will-be-removed-soon", token=bot_token) + + +@app.event("app_mention") +async def mention(context: AsyncBoltContext): + await context.say(":wave: Hi there!") + + +@app.event("message") +async def message(context: AsyncBoltContext, event: dict): + await context.client.reactions_add( + channel=event["channel"], + timestamp=event["ts"], + name="eyes", + ) + + +@app.command("/hello-socket-mode") +async def hello_command(ack, body): + user_id = body["user_id"] + await ack(f"Hi <@{user_id}>!") + + +async def main(): + from bolt_adapter.aiohttp import AsyncSocketModeHandler + + app_token = os.environ.get("SLACK_SDK_TEST_SOCKET_MODE_APP_TOKEN") + await AsyncSocketModeHandler(app, app_token).start_async() + + +if __name__ == "__main__": + import asyncio + + asyncio.run(main()) + + # export SLACK_SDK_TEST_SOCKET_MODE_APP_TOKEN= + # export SLACK_SDK_TEST_SOCKET_MODE_BOT_TOKEN= + # pip install .[optional] + # pip install slack_bolt + # python integration_tests/samples/socket_mode/{this file name}.py diff --git a/integration_tests/samples/socket_mode/bolt_aiohttp_example.py b/integration_tests/samples/socket_mode/bolt_aiohttp_example.py new file mode 100644 index 000000000..16d58f421 --- /dev/null +++ b/integration_tests/samples/socket_mode/bolt_aiohttp_example.py @@ -0,0 +1,50 @@ +import logging + +logging.basicConfig(level=logging.DEBUG) + +import os + +from slack_bolt.app import App +from slack_bolt.context import BoltContext + +bot_token = os.environ.get("SLACK_SDK_TEST_SOCKET_MODE_BOT_TOKEN") +app = App(signing_secret="will-be-removed-soon", token=bot_token) + + +@app.event("app_mention") +def mention(context: BoltContext): + context.say(":wave: Hi there!") + + +@app.event("message") +def message(context: BoltContext, event: dict): + context.client.reactions_add( + channel=event["channel"], + timestamp=event["ts"], + name="eyes", + ) + + +@app.command("/hello-socket-mode") +def hello_command(ack, body): + user_id = body["user_id"] + ack(f"Hi <@{user_id}>!") + + +async def main(): + from bolt_adapter.aiohttp import SocketModeHandler + + app_token = os.environ.get("SLACK_SDK_TEST_SOCKET_MODE_APP_TOKEN") + await SocketModeHandler(app, app_token).start_async() + + +if __name__ == "__main__": + import asyncio + + asyncio.run(main()) + + # export SLACK_SDK_TEST_SOCKET_MODE_APP_TOKEN= + # export SLACK_SDK_TEST_SOCKET_MODE_BOT_TOKEN= + # pip install .[optional] + # pip install slack_bolt + # python integration_tests/samples/socket_mode/{this file name}.py diff --git a/integration_tests/samples/socket_mode/bolt_builtin_example.py b/integration_tests/samples/socket_mode/bolt_builtin_example.py new file mode 100644 index 000000000..319fdefed --- /dev/null +++ b/integration_tests/samples/socket_mode/bolt_builtin_example.py @@ -0,0 +1,44 @@ +import logging + +logging.basicConfig(level=logging.DEBUG) + +import os + +from slack_bolt.app import App +from slack_bolt.context import BoltContext + +bot_token = os.environ.get("SLACK_SDK_TEST_SOCKET_MODE_BOT_TOKEN") +app = App(signing_secret="will-be-removed-soon", token=bot_token) + + +@app.event("app_mention") +def mention(context: BoltContext): + context.say(":wave: Hi there!") + + +@app.event("message") +def message(context: BoltContext, event: dict): + context.client.reactions_add( + channel=event["channel"], + timestamp=event["ts"], + name="eyes", + ) + + +@app.command("/hello-socket-mode") +def hello_command(ack, body): + user_id = body["user_id"] + ack(f"Hi <@{user_id}>!") + + +if __name__ == "__main__": + from bolt_adapter.builtin import SocketModeHandler + + app_token = os.environ.get("SLACK_SDK_TEST_SOCKET_MODE_APP_TOKEN") + SocketModeHandler(app, app_token).start() + + # export SLACK_SDK_TEST_SOCKET_MODE_APP_TOKEN= + # export SLACK_SDK_TEST_SOCKET_MODE_BOT_TOKEN= + # pip install .[optional] + # pip install slack_bolt + # python integration_tests/samples/socket_mode/{this file name}.py diff --git a/integration_tests/samples/socket_mode/bolt_oauth_aiohttp_async_example.py b/integration_tests/samples/socket_mode/bolt_oauth_aiohttp_async_example.py new file mode 100644 index 000000000..225d27e27 --- /dev/null +++ b/integration_tests/samples/socket_mode/bolt_oauth_aiohttp_async_example.py @@ -0,0 +1,62 @@ +import logging + +logging.basicConfig(level=logging.DEBUG) + +import os + +from slack_bolt.app.async_app import AsyncApp +from slack_bolt.context.async_context import AsyncBoltContext +from slack_bolt.oauth.async_oauth_settings import AsyncOAuthSettings + +app = AsyncApp( + signing_secret=os.environ["SLACK_SIGNING_SECRET"], + oauth_settings=AsyncOAuthSettings( + client_id=os.environ["SLACK_CLIENT_ID"], + client_secret=os.environ["SLACK_CLIENT_SECRET"], + scopes=os.environ["SLACK_SCOPES"].split(","), + ), +) + + +@app.event("app_mention") +async def mention(context: AsyncBoltContext): + await context.say(":wave: Hi there!") + + +@app.event("message") +async def message(context: AsyncBoltContext, event: dict): + await context.client.reactions_add( + channel=event["channel"], + timestamp=event["ts"], + name="eyes", + ) + + +@app.command("/hello-socket-mode") +async def hello_command(ack, body): + user_id = body["user_id"] + await ack(f"Hi <@{user_id}>!") + + +if __name__ == "__main__": + import asyncio + from asyncio import Future + + async def socket_mode_runner(): + from bolt_adapter.aiohttp import AsyncSocketModeHandler + + app_token = os.environ.get("SLACK_APP_TOKEN") + await AsyncSocketModeHandler(app, app_token).connect_async() + await asyncio.sleep(float("inf")) + + _: Future = asyncio.ensure_future(socket_mode_runner()) + app.start() + + # export SLACK_APP_TOKEN= + # export SLACK_SIGNING_SECRET= + # export SLACK_CLIENT_ID= + # export SLACK_CLIENT_SECRET= + # export SLACK_SCOPES= + # pip install .[optional] + # pip install slack_bolt + # python integration_tests/samples/socket_mode/{this file name}.py diff --git a/integration_tests/samples/socket_mode/bolt_oauth_aiohttp_example.py b/integration_tests/samples/socket_mode/bolt_oauth_aiohttp_example.py new file mode 100644 index 000000000..7b19d1413 --- /dev/null +++ b/integration_tests/samples/socket_mode/bolt_oauth_aiohttp_example.py @@ -0,0 +1,68 @@ +import logging + +logging.basicConfig(level=logging.DEBUG) + +import os + +from slack_bolt.app import App +from slack_bolt.context import BoltContext +from slack_bolt.oauth.oauth_settings import OAuthSettings + +app = App( + signing_secret=os.environ["SLACK_SIGNING_SECRET"], + oauth_settings=OAuthSettings( + client_id=os.environ["SLACK_CLIENT_ID"], + client_secret=os.environ["SLACK_CLIENT_SECRET"], + scopes=os.environ["SLACK_SCOPES"].split(","), + ), +) + + +@app.event("app_mention") +def mention(context: BoltContext): + context.say(":wave: Hi there!") + + +@app.event("message") +def message(context: BoltContext, event: dict): + context.client.reactions_add( + channel=event["channel"], + timestamp=event["ts"], + name="eyes", + ) + + +@app.command("/hello-socket-mode") +def hello_command(ack, body): + user_id = body["user_id"] + ack(f"Hi <@{user_id}>!") + + +if __name__ == "__main__": + + def run_socket_mode_app(): + import asyncio + from bolt_adapter.aiohttp import AsyncSocketModeHandler + + async def socket_mode_app(): + app_token = os.environ.get("SLACK_APP_TOKEN") + await AsyncSocketModeHandler(app, app_token).connect_async() + await asyncio.sleep(float("inf")) + + asyncio.run(socket_mode_app()) + + from concurrent.futures.thread import ThreadPoolExecutor + + socket_mode_thread = ThreadPoolExecutor(1) + socket_mode_thread.submit(run_socket_mode_app) + + app.start() + + # export SLACK_APP_TOKEN= + # export SLACK_SIGNING_SECRET= + # export SLACK_CLIENT_ID= + # export SLACK_CLIENT_SECRET= + # export SLACK_SCOPES= + # pip install .[optional] + # pip install slack_bolt + # python integration_tests/samples/socket_mode/{this file name}.py diff --git a/integration_tests/samples/socket_mode/bolt_oauth_builtin_example.py b/integration_tests/samples/socket_mode/bolt_oauth_builtin_example.py new file mode 100644 index 000000000..cf8574333 --- /dev/null +++ b/integration_tests/samples/socket_mode/bolt_oauth_builtin_example.py @@ -0,0 +1,56 @@ +import logging + +logging.basicConfig(level=logging.DEBUG) + +import os + +from slack_bolt.app import App +from slack_bolt.context import BoltContext +from slack_bolt.oauth.oauth_settings import OAuthSettings + +app = App( + signing_secret=os.environ["SLACK_SIGNING_SECRET"], + oauth_settings=OAuthSettings( + client_id=os.environ["SLACK_CLIENT_ID"], + client_secret=os.environ["SLACK_CLIENT_SECRET"], + scopes=os.environ["SLACK_SCOPES"].split(","), + ), +) + + +@app.event("app_mention") +def mention(context: BoltContext): + context.say(":wave: Hi there!") + + +@app.event("message") +def message(context: BoltContext, event: dict): + context.client.reactions_add( + channel=event["channel"], + timestamp=event["ts"], + name="eyes", + ) + + +@app.command("/hello-socket-mode") +def hello_command(ack, body): + user_id = body["user_id"] + ack(f"Hi <@{user_id}>!") + + +if __name__ == "__main__": + from bolt_adapter.builtin import SocketModeHandler + + app_token = os.environ.get("SLACK_APP_TOKEN") + SocketModeHandler(app, app_token).connect() + + app.start() + + # export SLACK_APP_TOKEN= + # export SLACK_SIGNING_SECRET= + # export SLACK_CLIENT_ID= + # export SLACK_CLIENT_SECRET= + # export SLACK_SCOPES= + # pip install .[optional] + # pip install slack_bolt + # python integration_tests/samples/socket_mode/{this file name}.py diff --git a/integration_tests/samples/socket_mode/bolt_oauth_websocket_client_example.py b/integration_tests/samples/socket_mode/bolt_oauth_websocket_client_example.py new file mode 100644 index 000000000..74a78d3ad --- /dev/null +++ b/integration_tests/samples/socket_mode/bolt_oauth_websocket_client_example.py @@ -0,0 +1,56 @@ +import logging + +logging.basicConfig(level=logging.DEBUG) + +import os + +from slack_bolt.app import App +from slack_bolt.context import BoltContext +from slack_bolt.oauth.oauth_settings import OAuthSettings + +app = App( + signing_secret=os.environ["SLACK_SIGNING_SECRET"], + oauth_settings=OAuthSettings( + client_id=os.environ["SLACK_CLIENT_ID"], + client_secret=os.environ["SLACK_CLIENT_SECRET"], + scopes=os.environ["SLACK_SCOPES"].split(","), + ), +) + + +@app.event("app_mention") +def mention(context: BoltContext): + context.say(":wave: Hi there!") + + +@app.event("message") +def message(context: BoltContext, event: dict): + context.client.reactions_add( + channel=event["channel"], + timestamp=event["ts"], + name="eyes", + ) + + +@app.command("/hello-socket-mode") +def hello_command(ack, body): + user_id = body["user_id"] + ack(f"Hi <@{user_id}>!") + + +if __name__ == "__main__": + from bolt_adapter.websocket_client import SocketModeHandler + + app_token = os.environ.get("SLACK_APP_TOKEN") + SocketModeHandler(app, app_token).connect() + + app.start() + + # export SLACK_APP_TOKEN= + # export SLACK_SIGNING_SECRET= + # export SLACK_CLIENT_ID= + # export SLACK_CLIENT_SECRET= + # export SLACK_SCOPES= + # pip install .[optional] + # pip install slack_bolt + # python integration_tests/samples/socket_mode/{this file name}.py diff --git a/integration_tests/samples/socket_mode/bolt_websocket_client_example.py b/integration_tests/samples/socket_mode/bolt_websocket_client_example.py new file mode 100644 index 000000000..397fc4584 --- /dev/null +++ b/integration_tests/samples/socket_mode/bolt_websocket_client_example.py @@ -0,0 +1,44 @@ +import logging + +logging.basicConfig(level=logging.DEBUG) + +import os + +from slack_bolt.app import App +from slack_bolt.context import BoltContext + +bot_token = os.environ.get("SLACK_SDK_TEST_SOCKET_MODE_BOT_TOKEN") +app = App(signing_secret="will-be-removed-soon", token=bot_token) + + +@app.event("app_mention") +def mention(context: BoltContext): + context.say(":wave: Hi there!") + + +@app.event("message") +def message(context: BoltContext, event: dict): + context.client.reactions_add( + channel=event["channel"], + timestamp=event["ts"], + name="eyes", + ) + + +@app.command("/hello-socket-mode") +def hello_command(ack, body): + user_id = body["user_id"] + ack(f"Hi <@{user_id}>!") + + +if __name__ == "__main__": + from bolt_adapter.websocket_client import SocketModeHandler + + app_token = os.environ.get("SLACK_SDK_TEST_SOCKET_MODE_APP_TOKEN") + SocketModeHandler(app, app_token).start() + + # export SLACK_SDK_TEST_SOCKET_MODE_APP_TOKEN= + # export SLACK_SDK_TEST_SOCKET_MODE_BOT_TOKEN= + # pip install .[optional] + # pip install slack_bolt + # python integration_tests/samples/socket_mode/{this file name}.py diff --git a/integration_tests/samples/socket_mode/bolt_websockets_async_example.py b/integration_tests/samples/socket_mode/bolt_websockets_async_example.py new file mode 100644 index 000000000..6db031d06 --- /dev/null +++ b/integration_tests/samples/socket_mode/bolt_websockets_async_example.py @@ -0,0 +1,49 @@ +import logging + +logging.basicConfig(level=logging.DEBUG) + +import os +from slack_bolt.app.async_app import AsyncApp +from slack_bolt.context.async_context import AsyncBoltContext + +bot_token = os.environ.get("SLACK_SDK_TEST_SOCKET_MODE_BOT_TOKEN") +app = AsyncApp(signing_secret="will-be-removed-soon", token=bot_token) + + +@app.event("app_mention") +async def mention(context: AsyncBoltContext): + await context.say(":wave: Hi there!") + + +@app.event("message") +async def message(context: AsyncBoltContext, event: dict): + await context.client.reactions_add( + channel=event["channel"], + timestamp=event["ts"], + name="eyes", + ) + + +@app.command("/hello-socket-mode") +async def hello_command(ack, body): + user_id = body["user_id"] + await ack(f"Hi <@{user_id}>!") + + +async def main(): + from bolt_adapter.websockets import AsyncSocketModeHandler + + app_token = os.environ.get("SLACK_SDK_TEST_SOCKET_MODE_APP_TOKEN") + await AsyncSocketModeHandler(app, app_token).start_async() + + +if __name__ == "__main__": + import asyncio + + asyncio.run(main()) + + # export SLACK_SDK_TEST_SOCKET_MODE_APP_TOKEN= + # export SLACK_SDK_TEST_SOCKET_MODE_BOT_TOKEN= + # pip install .[optional] + # pip install slack_bolt + # python integration_tests/samples/socket_mode/{this file name}.py diff --git a/integration_tests/samples/socket_mode/bolt_websockets_example.py b/integration_tests/samples/socket_mode/bolt_websockets_example.py new file mode 100644 index 000000000..04d8f7cc6 --- /dev/null +++ b/integration_tests/samples/socket_mode/bolt_websockets_example.py @@ -0,0 +1,50 @@ +import logging + +logging.basicConfig(level=logging.DEBUG) + +import os + +from slack_bolt.app import App +from slack_bolt.context import BoltContext + +bot_token = os.environ.get("SLACK_SDK_TEST_SOCKET_MODE_BOT_TOKEN") +app = App(signing_secret="will-be-removed-soon", token=bot_token) + + +@app.event("app_mention") +def mention(context: BoltContext): + context.say(":wave: Hi there!") + + +@app.event("message") +def message(context: BoltContext, event: dict): + context.client.reactions_add( + channel=event["channel"], + timestamp=event["ts"], + name="eyes", + ) + + +@app.command("/hello-socket-mode") +def hello_command(ack, body): + user_id = body["user_id"] + ack(f"Hi <@{user_id}>!") + + +async def main(): + from bolt_adapter.websockets import SocketModeHandler + + app_token = os.environ.get("SLACK_SDK_TEST_SOCKET_MODE_APP_TOKEN") + await SocketModeHandler(app, app_token).start_async() + + +if __name__ == "__main__": + import asyncio + + asyncio.run(main()) + + # export SLACK_SDK_TEST_SOCKET_MODE_APP_TOKEN= + # export SLACK_SDK_TEST_SOCKET_MODE_BOT_TOKEN= + # pip install .[optional] + # pip install slack_bolt + # python integration_tests/samples/socket_mode/{this file name}.py diff --git a/integration_tests/samples/socket_mode/builtin_example.py b/integration_tests/samples/socket_mode/builtin_example.py new file mode 100644 index 000000000..d4104238d --- /dev/null +++ b/integration_tests/samples/socket_mode/builtin_example.py @@ -0,0 +1,37 @@ +import logging + +logging.basicConfig( + level=logging.DEBUG, + format="%(asctime)s.%(msecs)03d %(levelname)s %(pathname)s (%(lineno)s): %(message)s", + datefmt="%Y-%m-%d %H:%M:%S", +) + +import os +from threading import Event +from slack_sdk.web import WebClient +from slack_sdk.socket_mode.response import SocketModeResponse +from slack_sdk.socket_mode.request import SocketModeRequest +from slack_sdk.socket_mode import SocketModeClient + +client = SocketModeClient( + app_token=os.environ.get("SLACK_SDK_TEST_SOCKET_MODE_APP_TOKEN"), + web_client=WebClient(token=os.environ.get("SLACK_SDK_TEST_SOCKET_MODE_BOT_TOKEN")), + trace_enabled=True, + all_message_trace_enabled=True, +) + +if __name__ == "__main__": + + def process(client: SocketModeClient, req: SocketModeRequest): + if req.type == "events_api": + response = SocketModeResponse(envelope_id=req.envelope_id) + client.send_socket_mode_response(response) + client.web_client.reactions_add( + name="eyes", + channel=req.payload["event"]["channel"], + timestamp=req.payload["event"]["ts"], + ) + + client.socket_mode_request_listeners.append(process) + client.connect() + Event().wait() diff --git a/integration_tests/samples/socket_mode/builtin_proxy_auth_example.py b/integration_tests/samples/socket_mode/builtin_proxy_auth_example.py new file mode 100644 index 000000000..be65ddba2 --- /dev/null +++ b/integration_tests/samples/socket_mode/builtin_proxy_auth_example.py @@ -0,0 +1,47 @@ +import logging + +logging.basicConfig( + level=logging.DEBUG, + # format="%(asctime)s.%(msecs)03d %(levelname)s %(pathname)s (%(lineno)s): %(message)s", + # datefmt="%Y-%m-%d %H:%M:%S", +) + +import os +from threading import Event +from slack_sdk.web import WebClient +from slack_sdk.socket_mode.response import SocketModeResponse +from slack_sdk.socket_mode.request import SocketModeRequest +from slack_sdk.socket_mode import SocketModeClient + +# https://github.com/seratch/my-proxy-server +# go build && my-proxy-server -a -u user -p pass/word +proxy_url = "http://user:pass%2Fword@localhost:9000" + +client = SocketModeClient( + app_token=os.environ.get("SLACK_SDK_TEST_SOCKET_MODE_APP_TOKEN"), + web_client=WebClient( + token=os.environ.get("SLACK_SDK_TEST_SOCKET_MODE_BOT_TOKEN"), + proxy=proxy_url, + ), + proxy=proxy_url, + # proxy="http://localhost:9000", + # proxy_headers={"Proxy-Authorization": "Basic dXNlcjpwYXNz"}, + trace_enabled=True, + all_message_trace_enabled=True, +) + +if __name__ == "__main__": + + def process(client: SocketModeClient, req: SocketModeRequest): + if req.type == "events_api": + response = SocketModeResponse(envelope_id=req.envelope_id) + client.send_socket_mode_response(response) + client.web_client.reactions_add( + name="eyes", + channel=req.payload["event"]["channel"], + timestamp=req.payload["event"]["ts"], + ) + + client.socket_mode_request_listeners.append(process) + client.connect() + Event().wait() diff --git a/integration_tests/samples/socket_mode/builtin_proxy_env_variable_example.py b/integration_tests/samples/socket_mode/builtin_proxy_env_variable_example.py new file mode 100644 index 000000000..f564756e1 --- /dev/null +++ b/integration_tests/samples/socket_mode/builtin_proxy_env_variable_example.py @@ -0,0 +1,44 @@ +import logging + +logging.basicConfig( + level=logging.DEBUG, + # format="%(asctime)s.%(msecs)03d %(levelname)s %(pathname)s (%(lineno)s): %(message)s", + # datefmt="%Y-%m-%d %H:%M:%S", +) + +import os +from threading import Event +from slack_sdk.web import WebClient +from slack_sdk.socket_mode.response import SocketModeResponse +from slack_sdk.socket_mode.request import SocketModeRequest +from slack_sdk.socket_mode import SocketModeClient + +# pip3 install proxy.py +# proxy --port 9000 --log-level d +os.environ["HTTPS_PROXY"] = "http://localhost:9000" + +client = SocketModeClient( + app_token=os.environ.get("SLACK_SDK_TEST_SOCKET_MODE_APP_TOKEN"), + web_client=WebClient( + token=os.environ.get("SLACK_SDK_TEST_SOCKET_MODE_BOT_TOKEN"), + ), + proxy_headers={"Proxy-Authorization": "Basic dXNlcjpwYXNz"}, + trace_enabled=True, + all_message_trace_enabled=True, +) + +if __name__ == "__main__": + + def process(client: SocketModeClient, req: SocketModeRequest): + if req.type == "events_api": + response = SocketModeResponse(envelope_id=req.envelope_id) + client.send_socket_mode_response(response) + client.web_client.reactions_add( + name="eyes", + channel=req.payload["event"]["channel"], + timestamp=req.payload["event"]["ts"], + ) + + client.socket_mode_request_listeners.append(process) + client.connect() + Event().wait() diff --git a/integration_tests/samples/socket_mode/builtin_proxy_example.py b/integration_tests/samples/socket_mode/builtin_proxy_example.py new file mode 100644 index 000000000..3160d7733 --- /dev/null +++ b/integration_tests/samples/socket_mode/builtin_proxy_example.py @@ -0,0 +1,45 @@ +import logging + +logging.basicConfig( + level=logging.DEBUG, + # format="%(asctime)s.%(msecs)03d %(levelname)s %(pathname)s (%(lineno)s): %(message)s", + # datefmt="%Y-%m-%d %H:%M:%S", +) + +import os +from threading import Event +from slack_sdk.web import WebClient +from slack_sdk.socket_mode.response import SocketModeResponse +from slack_sdk.socket_mode.request import SocketModeRequest +from slack_sdk.socket_mode import SocketModeClient + +# pip3 install proxy.py +# proxy --port 9000 --log-level d +proxy_url = "http://localhost:9000" + +client = SocketModeClient( + app_token=os.environ.get("SLACK_SDK_TEST_SOCKET_MODE_APP_TOKEN"), + web_client=WebClient( + token=os.environ.get("SLACK_SDK_TEST_SOCKET_MODE_BOT_TOKEN"), + proxy=proxy_url, + ), + proxy=proxy_url, + trace_enabled=True, + all_message_trace_enabled=True, +) + +if __name__ == "__main__": + + def process(client: SocketModeClient, req: SocketModeRequest): + if req.type == "events_api": + response = SocketModeResponse(envelope_id=req.envelope_id) + client.send_socket_mode_response(response) + client.web_client.reactions_add( + name="eyes", + channel=req.payload["event"]["channel"], + timestamp=req.payload["event"]["ts"], + ) + + client.socket_mode_request_listeners.append(process) + client.connect() + Event().wait() diff --git a/integration_tests/samples/socket_mode/websocket_client_example.py b/integration_tests/samples/socket_mode/websocket_client_example.py new file mode 100644 index 000000000..d14834efb --- /dev/null +++ b/integration_tests/samples/socket_mode/websocket_client_example.py @@ -0,0 +1,32 @@ +import logging + +logging.basicConfig(level=logging.DEBUG) + +import os +from threading import Event +from slack_sdk.web import WebClient +from slack_sdk.socket_mode.response import SocketModeResponse +from slack_sdk.socket_mode.request import SocketModeRequest +from slack_sdk.socket_mode.websocket_client import SocketModeClient + +client = SocketModeClient( + app_token=os.environ.get("SLACK_SDK_TEST_SOCKET_MODE_APP_TOKEN"), + web_client=WebClient(token=os.environ.get("SLACK_SDK_TEST_SOCKET_MODE_BOT_TOKEN")), + trace_enabled=True, +) + +if __name__ == "__main__": + + def process(client: SocketModeClient, req: SocketModeRequest): + if req.type == "events_api": + response = SocketModeResponse(envelope_id=req.envelope_id) + client.send_socket_mode_response(response) + client.web_client.reactions_add( + name="eyes", + channel=req.payload["event"]["channel"], + timestamp=req.payload["event"]["ts"], + ) + + client.socket_mode_request_listeners.append(process) + client.connect() + Event().wait() diff --git a/integration_tests/samples/socket_mode/websocket_client_proxy_example.py b/integration_tests/samples/socket_mode/websocket_client_proxy_example.py new file mode 100644 index 000000000..20126e3d9 --- /dev/null +++ b/integration_tests/samples/socket_mode/websocket_client_proxy_example.py @@ -0,0 +1,42 @@ +import logging + +logging.basicConfig(level=logging.DEBUG) + +import os +from threading import Event +from slack_sdk.web import WebClient +from slack_sdk.socket_mode.response import SocketModeResponse +from slack_sdk.socket_mode.request import SocketModeRequest +from slack_sdk.socket_mode.websocket_client import SocketModeClient + +client = SocketModeClient( + app_token=os.environ.get("SLACK_SDK_TEST_SOCKET_MODE_APP_TOKEN"), + web_client=WebClient( + token=os.environ.get("SLACK_SDK_TEST_SOCKET_MODE_BOT_TOKEN"), + # pip3 install proxy.py + # proxy --port 9000 --log-level d + proxy="http://localhost:9000", + ), + trace_enabled=True, + # pip3 install proxy.py + # proxy --port 9000 --log-level d + http_proxy_host="localhost", + http_proxy_port=9000, + http_proxy_auth=("user", "pass"), +) + +if __name__ == "__main__": + + def process(client: SocketModeClient, req: SocketModeRequest): + if req.type == "events_api": + response = SocketModeResponse(envelope_id=req.envelope_id) + client.send_socket_mode_response(response) + client.web_client.reactions_add( + name="eyes", + channel=req.payload["event"]["channel"], + timestamp=req.payload["event"]["ts"], + ) + + client.socket_mode_request_listeners.append(process) + client.connect() + Event().wait() diff --git a/integration_tests/samples/socket_mode/websockets_example.py b/integration_tests/samples/socket_mode/websockets_example.py new file mode 100644 index 000000000..6129a8a42 --- /dev/null +++ b/integration_tests/samples/socket_mode/websockets_example.py @@ -0,0 +1,35 @@ +import logging + +logging.basicConfig(level=logging.DEBUG) + +import asyncio +import os +from slack_sdk.web.async_client import AsyncWebClient +from slack_sdk.socket_mode.response import SocketModeResponse +from slack_sdk.socket_mode.request import SocketModeRequest +from slack_sdk.socket_mode.websockets import SocketModeClient + + +async def main(): + client = SocketModeClient( + app_token=os.environ.get("SLACK_SDK_TEST_SOCKET_MODE_APP_TOKEN"), + web_client=AsyncWebClient(token=os.environ.get("SLACK_SDK_TEST_SOCKET_MODE_BOT_TOKEN")), + ) + + async def process(client: SocketModeClient, req: SocketModeRequest): + if req.type == "events_api": + response = SocketModeResponse(envelope_id=req.envelope_id) + await client.send_socket_mode_response(response) + + await client.web_client.reactions_add( + name="eyes", + channel=req.payload["event"]["channel"], + timestamp=req.payload["event"]["ts"], + ) + + client.socket_mode_request_listeners.append(process) + await client.connect() + await asyncio.sleep(float("inf")) + + +asyncio.run(main()) diff --git a/integration_tests/samples/token_rotation/.gitignore b/integration_tests/samples/token_rotation/.gitignore new file mode 100644 index 000000000..e6905a239 --- /dev/null +++ b/integration_tests/samples/token_rotation/.gitignore @@ -0,0 +1 @@ +.env* \ No newline at end of file diff --git a/integration_tests/samples/token_rotation/oauth.py b/integration_tests/samples/token_rotation/oauth.py new file mode 100644 index 000000000..4fb977244 --- /dev/null +++ b/integration_tests/samples/token_rotation/oauth.py @@ -0,0 +1,261 @@ +# --------------------- +# Flask App for Slack OAuth flow +# --------------------- + +# pip3 install flask +from typing import Optional + +from integration_tests.samples.token_rotation.util import ( + parse_body, + extract_enterprise_id, + extract_user_id, + extract_team_id, + extract_is_enterprise_install, + extract_content_type, +) + +import logging +import os +from slack_sdk.web import WebClient +from slack_sdk.oauth.token_rotation import TokenRotator +from slack_sdk.oauth import AuthorizeUrlGenerator, RedirectUriPageRenderer +from slack_sdk.oauth.installation_store import FileInstallationStore, Installation +from slack_sdk.oauth.state_store import FileOAuthStateStore + +client_id = os.environ["SLACK_CLIENT_ID"] +client_secret = os.environ["SLACK_CLIENT_SECRET"] +scopes = ["app_mentions:read", "chat:write", "commands"] +user_scopes = ["search:read"] + +logger = logging.getLogger(__name__) +logging.basicConfig(level=logging.DEBUG) + +state_store = FileOAuthStateStore(expiration_seconds=300) +installation_store = FileInstallationStore() +token_rotator = TokenRotator( + client_id=client_id, + client_secret=client_secret, +) + +# --------------------- +# Flask App for Slack events +# --------------------- + +import json +from slack_sdk.errors import SlackApiError +from slack_sdk.signature import SignatureVerifier + +signing_secret = os.environ["SLACK_SIGNING_SECRET"] +signature_verifier = SignatureVerifier(signing_secret=signing_secret) + + +def rotate_tokens( + enterprise_id: Optional[str] = None, + team_id: Optional[str] = None, + user_id: Optional[str] = None, + is_enterprise_install: Optional[bool] = None, +): + installation = installation_store.find_installation( + enterprise_id=enterprise_id, + team_id=team_id, + user_id=user_id, + is_enterprise_install=is_enterprise_install, + ) + if installation is not None: + updated_installation = token_rotator.perform_token_rotation( + installation=installation, + minutes_before_expiration=60 * 24 * 365, # one year for testing + ) + if updated_installation is not None: + installation_store.save(updated_installation) + + +from flask import Flask, request, make_response + +app = Flask(__name__) +app.debug = True + + +@app.route("/slack/events", methods=["POST"]) +def slack_app(): + if not signature_verifier.is_valid( + body=request.get_data(), + timestamp=request.headers.get("X-Slack-Request-Timestamp"), + signature=request.headers.get("X-Slack-Signature"), + ): + return make_response("invalid request", 403) + + raw_body = request.data.decode("utf-8") + body = parse_body(body=raw_body, content_type=extract_content_type(request.headers)) + rotate_tokens( + enterprise_id=extract_enterprise_id(body), + team_id=extract_team_id(body), + user_id=extract_user_id(body), + is_enterprise_install=extract_is_enterprise_install(body), + ) + + if "command" in request.form and request.form["command"] == "/token-rotation-modal": + try: + enterprise_id = request.form.get("enterprise_id") + team_id = request.form["team_id"] + bot = installation_store.find_bot( + enterprise_id=enterprise_id, + team_id=team_id, + ) + bot_token = bot.bot_token if bot else None + if not bot_token: + return make_response("Please install this app first!", 200) + + client = WebClient(token=bot_token) + trigger_id = request.form["trigger_id"] + response = client.views_open( + trigger_id=trigger_id, + view={ + "type": "modal", + "callback_id": "modal-id", + "title": {"type": "plain_text", "text": "Awesome Modal"}, + "submit": {"type": "plain_text", "text": "Submit"}, + "close": {"type": "plain_text", "text": "Cancel"}, + "blocks": [ + { + "type": "input", + "block_id": "b-id", + "label": { + "type": "plain_text", + "text": "Input label", + }, + "element": { + "action_id": "a-id", + "type": "plain_text_input", + }, + } + ], + }, + ) + return make_response("", 200) + except SlackApiError as e: + code = e.response["error"] + return make_response(f"Failed to open a modal due to {code}", 200) + + elif "payload" in request.form: + payload = json.loads(request.form["payload"]) + if payload["type"] == "view_submission" and payload["view"]["callback_id"] == "modal-id": + submitted_data = payload["view"]["state"]["values"] + print(submitted_data) # {'b-id': {'a-id': {'type': 'plain_text_input', 'value': 'your input'}}} + return make_response("", 200) + + else: + if raw_body.startswith("{"): + event_payload = json.loads(raw_body) + logger.info(f"Events API payload: {event_payload}") + if event_payload.get("type") == "url_verification": + return make_response(event_payload.get("challenge"), 200) + return make_response("", 200) + + return make_response("", 404) + + +# --------------------- +# Flask App for Slack OAuth flow +# --------------------- + +authorization_url_generator = AuthorizeUrlGenerator( + client_id=client_id, + scopes=scopes, + user_scopes=user_scopes, +) +redirect_page_renderer = RedirectUriPageRenderer( + install_path="/slack/install", + redirect_uri_path="/slack/oauth_redirect", +) + + +@app.route("/slack/install", methods=["GET"]) +def oauth_start(): + state = state_store.issue() + url = authorization_url_generator.generate(state) + return ( + '' + f'' + f'' + "" + ) + + +@app.route("/slack/oauth_redirect", methods=["GET"]) +def oauth_callback(): + # Retrieve the auth code and state from the request params + if "code" in request.args: + state = request.args["state"] + if state_store.consume(state): + code = request.args["code"] + client = WebClient() # no prepared token needed for this app + oauth_response = client.oauth_v2_access(client_id=client_id, client_secret=client_secret, code=code) + logger.info(f"oauth.v2.access response: {oauth_response}") + + installed_enterprise = oauth_response.get("enterprise", {}) + is_enterprise_install = oauth_response.get("is_enterprise_install") + installed_team = oauth_response.get("team", {}) + installer = oauth_response.get("authed_user", {}) + incoming_webhook = oauth_response.get("incoming_webhook", {}) + + bot_token = oauth_response.get("access_token") + # NOTE: oauth.v2.access doesn't include bot_id in response + bot_id = None + enterprise_url = None + if bot_token is not None: + auth_test = client.auth_test(token=bot_token) + bot_id = auth_test["bot_id"] + if is_enterprise_install is True: + enterprise_url = auth_test.get("url") + + installation = Installation( + app_id=oauth_response.get("app_id"), + enterprise_id=installed_enterprise.get("id"), + enterprise_name=installed_enterprise.get("name"), + enterprise_url=enterprise_url, + team_id=installed_team.get("id"), + team_name=installed_team.get("name"), + bot_token=bot_token, + bot_id=bot_id, + bot_user_id=oauth_response.get("bot_user_id"), + bot_scopes=oauth_response.get("scope"), # comma-separated string + bot_refresh_token=oauth_response.get("refresh_token"), + bot_token_expires_in=oauth_response.get("expires_in"), + user_id=installer.get("id"), + user_token=installer.get("access_token"), + user_scopes=installer.get("scope"), # comma-separated string + user_refresh_token=installer.get("refresh_token"), + user_token_expires_in=installer.get("expires_in"), + incoming_webhook_url=incoming_webhook.get("url"), + incoming_webhook_channel=incoming_webhook.get("channel"), + incoming_webhook_channel_id=incoming_webhook.get("channel_id"), + incoming_webhook_configuration_url=incoming_webhook.get("configuration_url"), + is_enterprise_install=is_enterprise_install, + token_type=oauth_response.get("token_type"), + ) + installation_store.save(installation) + return redirect_page_renderer.render_success_page( + app_id=installation.app_id, + team_id=installation.team_id, + is_enterprise_install=installation.is_enterprise_install, + enterprise_url=installation.enterprise_url, + ) + else: + return redirect_page_renderer.render_failure_page("the state value is already expired") + + error = request.args["error"] if "error" in request.args else "" + return redirect_page_renderer.render_failure_page(error) + + +if __name__ == "__main__": + # export SLACK_CLIENT_ID=123.123 + # export SLACK_CLIENT_SECRET=xxx + # export SLACK_SIGNING_SECRET=*** + # export FLASK_ENV=development + + app.run("localhost", 3000) + + # python3 integration_tests/samples/token_rotation/oauth.py + # ngrok http 3000 + # https://{yours}.ngrok.io/slack/oauth/start diff --git a/integration_tests/samples/token_rotation/oauth_async.py b/integration_tests/samples/token_rotation/oauth_async.py new file mode 100644 index 000000000..319c718f9 --- /dev/null +++ b/integration_tests/samples/token_rotation/oauth_async.py @@ -0,0 +1,265 @@ +# --------------------- +# Sanic App for Slack OAuth flow +# --------------------- + +from typing import Optional + +from integration_tests.samples.token_rotation.util import ( + parse_body, + extract_enterprise_id, + extract_user_id, + extract_team_id, + extract_is_enterprise_install, + extract_content_type, +) + +import logging +import os +from slack_sdk.web.async_client import AsyncWebClient +from slack_sdk.oauth.token_rotation.async_rotator import AsyncTokenRotator +from slack_sdk.oauth import AuthorizeUrlGenerator, RedirectUriPageRenderer +from slack_sdk.oauth.installation_store import FileInstallationStore, Installation +from slack_sdk.oauth.state_store import FileOAuthStateStore + +client_id = os.environ["SLACK_CLIENT_ID"] +client_secret = os.environ["SLACK_CLIENT_SECRET"] +scopes = ["app_mentions:read", "chat:write", "commands"] +user_scopes = ["search:read"] + +logger = logging.getLogger(__name__) +logging.basicConfig(level=logging.DEBUG) + +state_store = FileOAuthStateStore(expiration_seconds=300) +installation_store = FileInstallationStore() +token_rotator = AsyncTokenRotator( + client_id=client_id, + client_secret=client_secret, +) + +# --------------------- +# Sanic App for Slack events +# --------------------- + +import json +from slack_sdk.errors import SlackApiError +from slack_sdk.signature import SignatureVerifier + +signing_secret = os.environ["SLACK_SIGNING_SECRET"] +signature_verifier = SignatureVerifier(signing_secret=signing_secret) + + +async def rotate_tokens( + enterprise_id: Optional[str] = None, + team_id: Optional[str] = None, + user_id: Optional[str] = None, + is_enterprise_install: Optional[bool] = None, +): + installation = await installation_store.async_find_installation( + enterprise_id=enterprise_id, + team_id=team_id, + user_id=user_id, + is_enterprise_install=is_enterprise_install, + ) + if installation is not None: + updated_installation = await token_rotator.perform_token_rotation( + installation=installation, + minutes_before_expiration=60 * 24 * 365, # one year for testing + ) + if updated_installation is not None: + await installation_store.async_save(updated_installation) + + +# https://sanicframework.org/ +from sanic import Sanic +from sanic.request import Request +from sanic.response import HTTPResponse + +app = Sanic("my-awesome-slack-app") + + +@app.post("/slack/events") +async def slack_app(req: Request): + if not signature_verifier.is_valid( + body=req.body.decode("utf-8"), + timestamp=req.headers.get("X-Slack-Request-Timestamp"), + signature=req.headers.get("X-Slack-Signature"), + ): + return HTTPResponse(status=403, body="invalid request") + + raw_body = req.body.decode("utf-8") + body = parse_body(body=raw_body, content_type=extract_content_type(req.headers)) + await rotate_tokens( + enterprise_id=extract_enterprise_id(body), + team_id=extract_team_id(body), + user_id=extract_user_id(body), + is_enterprise_install=extract_is_enterprise_install(body), + ) + + if "command" in req.form and req.form.get("command") == "/token-rotation-modal": + try: + enterprise_id = req.form.get("enterprise_id") + team_id = req.form.get("team_id") + bot = await installation_store.async_find_bot( + enterprise_id=enterprise_id, + team_id=team_id, + ) + bot_token = bot.bot_token if bot else None + if not bot_token: + return HTTPResponse(status=200, body="Please install this app first!") + + client = AsyncWebClient(token=bot_token) + await client.views_open( + trigger_id=req.form.get("trigger_id"), + view={ + "type": "modal", + "callback_id": "modal-id", + "title": {"type": "plain_text", "text": "Awesome Modal"}, + "submit": {"type": "plain_text", "text": "Submit"}, + "close": {"type": "plain_text", "text": "Cancel"}, + "blocks": [ + { + "type": "input", + "block_id": "b-id", + "label": { + "type": "plain_text", + "text": "Input label", + }, + "element": { + "action_id": "a-id", + "type": "plain_text_input", + }, + } + ], + }, + ) + return HTTPResponse(status=200, body="") + except SlackApiError as e: + code = e.response["error"] + return HTTPResponse(status=200, body=f"Failed to open a modal due to {code}") + + elif "payload" in req.form: + payload = json.loads(req.form.get("payload")) + if payload.get("type") == "view_submission" and payload.get("view").get("callback_id") == "modal-id": + submitted_data = payload.get("view").get("state").get("values") + print(submitted_data) # {'b-id': {'a-id': {'type': 'plain_text_input', 'value': 'your input'}}} + return HTTPResponse(status=200, body="") + + else: + if raw_body.startswith("{"): + event_payload = json.loads(raw_body) + if event_payload.get("type") == "url_verification": + return HTTPResponse(status=200, body=event_payload.get("challenge")) + return HTTPResponse(status=200, body="") + + return HTTPResponse(status=404, body="Not found") + + +# --------------------- +# Sanic App for Slack OAuth flow +# --------------------- + +authorization_url_generator = AuthorizeUrlGenerator( + client_id=client_id, + scopes=scopes, + user_scopes=user_scopes, +) +redirect_page_renderer = RedirectUriPageRenderer( + install_path="/slack/install", + redirect_uri_path="/slack/oauth_redirect", +) + + +@app.get("/slack/install") +async def oauth_start(req: Request): + state = state_store.issue() + url = authorization_url_generator.generate(state) + response_body = ( + '' + f'' + f'' + "" + ) + return HTTPResponse( + status=200, + body=response_body, + ) + + +@app.get("/slack/oauth_redirect") +async def oauth_callback(req: Request): + # Retrieve the auth code and state from the request params + if "code" in req.args: + state = req.args.get("state") + if state_store.consume(state): + code = req.args.get("code") + client = AsyncWebClient() # no prepared token needed for this app + oauth_response = await client.oauth_v2_access(client_id=client_id, client_secret=client_secret, code=code) + logger.info(f"oauth.v2.access response: {oauth_response}") + + installed_enterprise = oauth_response.get("enterprise") or {} + installed_team = oauth_response.get("team") or {} + installer = oauth_response.get("authed_user") or {} + incoming_webhook = oauth_response.get("incoming_webhook") or {} + bot_token = oauth_response.get("access_token") + # NOTE: oauth.v2.access doesn't include bot_id in response + bot_id = None + if bot_token is not None: + auth_test = await client.auth_test(token=bot_token) + bot_id = auth_test["bot_id"] + + installation = Installation( + app_id=oauth_response.get("app_id"), + enterprise_id=installed_enterprise.get("id"), + team_id=installed_team.get("id"), + bot_token=bot_token, + bot_id=bot_id, + bot_user_id=oauth_response.get("bot_user_id"), + bot_scopes=oauth_response.get("scope"), # comma-separated string + bot_refresh_token=oauth_response.get("refresh_token"), + bot_token_expires_in=oauth_response.get("expires_in"), + user_id=installer.get("id"), + user_token=installer.get("access_token"), + user_scopes=installer.get("scope"), # comma-separated string + user_refresh_token=installer.get("refresh_token"), + user_token_expires_in=installer.get("expires_in"), + incoming_webhook_url=incoming_webhook.get("url"), + incoming_webhook_channel_id=incoming_webhook.get("channel_id"), + incoming_webhook_configuration_url=incoming_webhook.get("configuration_url"), + ) + await installation_store.async_save(installation) + html = redirect_page_renderer.render_success_page( + app_id=installation.app_id, + team_id=installation.team_id, + is_enterprise_install=installation.is_enterprise_install, + enterprise_url=installation.enterprise_url, + ) + return HTTPResponse( + status=200, + headers={ + "Content-Type": "text/html; charset=utf-8", + }, + body=html, + ) + else: + html = redirect_page_renderer.render_failure_page("the state value is already expired") + return HTTPResponse( + status=400, + headers={ + "Content-Type": "text/html; charset=utf-8", + }, + body=html, + ) + + error = req.args.get("error") if "error" in req.args else "" + return HTTPResponse( + status=400, + headers={"Content-Type": "text/html; charset=utf-8"}, + body=redirect_page_renderer.render_failure_page(error), + ) + + +if __name__ == "__main__": + app.run(host="0.0.0.0", port=3000) + # python3 integration_tests/samples/token_rotation/oauth_async.py + # ngrok http 3000 + # https://{yours}.ngrok.io/slack/install diff --git a/integration_tests/samples/token_rotation/oauth_sqlalchemy.py b/integration_tests/samples/token_rotation/oauth_sqlalchemy.py new file mode 100644 index 000000000..df52beac7 --- /dev/null +++ b/integration_tests/samples/token_rotation/oauth_sqlalchemy.py @@ -0,0 +1,290 @@ +# --------------------- +# Flask App for Slack OAuth flow +# --------------------- + +# pip3 install flask +from typing import Optional + +from integration_tests.samples.token_rotation.util import ( + parse_body, + extract_enterprise_id, + extract_user_id, + extract_team_id, + extract_is_enterprise_install, + extract_content_type, +) + +import logging +import os + +from slack_sdk.oauth.installation_store.sqlalchemy import SQLAlchemyInstallationStore +from slack_sdk.web import WebClient +from slack_sdk.oauth.token_rotation import TokenRotator +from slack_sdk.oauth import AuthorizeUrlGenerator, RedirectUriPageRenderer +from slack_sdk.oauth.installation_store import Installation +from slack_sdk.oauth.state_store.sqlalchemy import SQLAlchemyOAuthStateStore + +client_id = os.environ["SLACK_CLIENT_ID"] +client_secret = os.environ["SLACK_CLIENT_SECRET"] +scopes = ["app_mentions:read", "chat:write", "commands"] +user_scopes = ["search:read"] + +logging.basicConfig(level=logging.DEBUG) +logger = logging.getLogger(__name__) +logging.getLogger("sqlalchemy.engine").setLevel(logging.INFO) + +import sqlalchemy +from sqlalchemy.engine import Engine + +database_url = "sqlite:///slackapp.db" +# database_url = "postgresql://localhost/slackapp" # pip install psycopg2 +engine: Engine = sqlalchemy.create_engine(database_url) + +installation_store = SQLAlchemyInstallationStore( + client_id=client_id, + engine=engine, + logger=logger, +) +token_rotator = TokenRotator( + client_id=client_id, + client_secret=client_secret, +) + +state_store = SQLAlchemyOAuthStateStore( + engine=engine, + logger=logger, + expiration_seconds=300, +) + +try: + engine.execute("select count(*) from slack_bots") +except Exception as e: + installation_store.metadata.create_all(engine) + +try: + engine.execute("select count(*) from slack_oauth_states") +except Exception as e: + state_store.metadata.create_all(engine) + +# --------------------- +# Flask App for Slack events +# --------------------- + +import json +from slack_sdk.errors import SlackApiError +from slack_sdk.signature import SignatureVerifier + +signing_secret = os.environ["SLACK_SIGNING_SECRET"] +signature_verifier = SignatureVerifier(signing_secret=signing_secret) + + +def rotate_tokens( + enterprise_id: Optional[str] = None, + team_id: Optional[str] = None, + user_id: Optional[str] = None, + is_enterprise_install: Optional[bool] = None, +): + installation = installation_store.find_installation( + enterprise_id=enterprise_id, + team_id=team_id, + user_id=user_id, + is_enterprise_install=is_enterprise_install, + ) + if installation is not None: + updated_installation = token_rotator.perform_token_rotation( + installation=installation, + minutes_before_expiration=60 * 24 * 365, # one year for testing + ) + if updated_installation is not None: + installation_store.save(updated_installation) + + +from flask import Flask, request, make_response + +app = Flask(__name__) +app.debug = True + + +@app.route("/slack/events", methods=["POST"]) +def slack_app(): + if not signature_verifier.is_valid( + body=request.get_data(), + timestamp=request.headers.get("X-Slack-Request-Timestamp"), + signature=request.headers.get("X-Slack-Signature"), + ): + return make_response("invalid request", 403) + + raw_body = request.data.decode("utf-8") + body = parse_body(body=raw_body, content_type=extract_content_type(request.headers)) + rotate_tokens( + enterprise_id=extract_enterprise_id(body), + team_id=extract_team_id(body), + user_id=extract_user_id(body), + is_enterprise_install=extract_is_enterprise_install(body), + ) + + if "command" in request.form and request.form["command"] == "/token-rotation-modal": + try: + enterprise_id = request.form.get("enterprise_id") + team_id = request.form["team_id"] + bot = installation_store.find_bot( + enterprise_id=enterprise_id, + team_id=team_id, + ) + bot_token = bot.bot_token if bot else None + if not bot_token: + return make_response("Please install this app first!", 200) + + client = WebClient(token=bot_token) + trigger_id = request.form["trigger_id"] + response = client.views_open( + trigger_id=trigger_id, + view={ + "type": "modal", + "callback_id": "modal-id", + "title": {"type": "plain_text", "text": "Awesome Modal"}, + "submit": {"type": "plain_text", "text": "Submit"}, + "close": {"type": "plain_text", "text": "Cancel"}, + "blocks": [ + { + "type": "input", + "block_id": "b-id", + "label": { + "type": "plain_text", + "text": "Input label", + }, + "element": { + "action_id": "a-id", + "type": "plain_text_input", + }, + } + ], + }, + ) + return make_response("", 200) + except SlackApiError as e: + code = e.response["error"] + return make_response(f"Failed to open a modal due to {code}", 200) + + elif "payload" in request.form: + payload = json.loads(request.form["payload"]) + if payload["type"] == "view_submission" and payload["view"]["callback_id"] == "modal-id": + submitted_data = payload["view"]["state"]["values"] + print(submitted_data) # {'b-id': {'a-id': {'type': 'plain_text_input', 'value': 'your input'}}} + return make_response("", 200) + + else: + if raw_body.startswith("{"): + event_payload = json.loads(raw_body) + logger.info(f"Events API payload: {event_payload}") + if event_payload.get("type") == "url_verification": + return make_response(event_payload.get("challenge"), 200) + return make_response("", 200) + + return make_response("", 404) + + +# --------------------- +# Flask App for Slack OAuth flow +# --------------------- + +authorization_url_generator = AuthorizeUrlGenerator( + client_id=client_id, + scopes=scopes, + user_scopes=user_scopes, +) +redirect_page_renderer = RedirectUriPageRenderer( + install_path="/slack/install", + redirect_uri_path="/slack/oauth_redirect", +) + + +@app.route("/slack/install", methods=["GET"]) +def oauth_start(): + state = state_store.issue() + url = authorization_url_generator.generate(state) + return ( + '' + f'' + f'' + "" + ) + + +@app.route("/slack/oauth_redirect", methods=["GET"]) +def oauth_callback(): + # Retrieve the auth code and state from the request params + if "code" in request.args: + state = request.args["state"] + if state_store.consume(state): + code = request.args["code"] + client = WebClient() # no prepared token needed for this app + oauth_response = client.oauth_v2_access(client_id=client_id, client_secret=client_secret, code=code) + logger.info(f"oauth.v2.access response: {oauth_response}") + + installed_enterprise = oauth_response.get("enterprise", {}) + is_enterprise_install = oauth_response.get("is_enterprise_install") + installed_team = oauth_response.get("team", {}) + installer = oauth_response.get("authed_user", {}) + incoming_webhook = oauth_response.get("incoming_webhook", {}) + + bot_token = oauth_response.get("access_token") + # NOTE: oauth.v2.access doesn't include bot_id in response + bot_id = None + enterprise_url = None + if bot_token is not None: + auth_test = client.auth_test(token=bot_token) + bot_id = auth_test["bot_id"] + if is_enterprise_install is True: + enterprise_url = auth_test.get("url") + + installation = Installation( + app_id=oauth_response.get("app_id"), + enterprise_id=installed_enterprise.get("id"), + enterprise_name=installed_enterprise.get("name"), + enterprise_url=enterprise_url, + team_id=installed_team.get("id"), + team_name=installed_team.get("name"), + bot_token=bot_token, + bot_id=bot_id, + bot_user_id=oauth_response.get("bot_user_id"), + bot_scopes=oauth_response.get("scope"), # comma-separated string + bot_refresh_token=oauth_response.get("refresh_token"), + bot_token_expires_in=oauth_response.get("expires_in"), + user_id=installer.get("id"), + user_token=installer.get("access_token"), + user_scopes=installer.get("scope"), # comma-separated string + user_refresh_token=installer.get("refresh_token"), + user_token_expires_in=installer.get("expires_in"), + incoming_webhook_url=incoming_webhook.get("url"), + incoming_webhook_channel=incoming_webhook.get("channel"), + incoming_webhook_channel_id=incoming_webhook.get("channel_id"), + incoming_webhook_configuration_url=incoming_webhook.get("configuration_url"), + is_enterprise_install=is_enterprise_install, + token_type=oauth_response.get("token_type"), + ) + installation_store.save(installation) + return redirect_page_renderer.render_success_page( + app_id=installation.app_id, + team_id=installation.team_id, + is_enterprise_install=installation.is_enterprise_install, + enterprise_url=installation.enterprise_url, + ) + else: + return redirect_page_renderer.render_failure_page("the state value is already expired") + + error = request.args["error"] if "error" in request.args else "" + return redirect_page_renderer.render_failure_page(error) + + +if __name__ == "__main__": + # export SLACK_CLIENT_ID=123.123 + # export SLACK_CLIENT_SECRET=xxx + # export SLACK_SIGNING_SECRET=*** + # export FLASK_ENV=development + + app.run("localhost", 3000) + + # python3 integration_tests/samples/token_rotation/oauth.py + # ngrok http 3000 + # https://{yours}.ngrok.io/slack/oauth/start diff --git a/integration_tests/samples/token_rotation/oauth_sqlite3.py b/integration_tests/samples/token_rotation/oauth_sqlite3.py new file mode 100644 index 000000000..7b7acd989 --- /dev/null +++ b/integration_tests/samples/token_rotation/oauth_sqlite3.py @@ -0,0 +1,283 @@ +# --------------------- +# Flask App for Slack OAuth flow +# --------------------- + +# pip3 install flask +from typing import Optional + +from integration_tests.samples.token_rotation.util import ( + parse_body, + extract_enterprise_id, + extract_user_id, + extract_team_id, + extract_is_enterprise_install, + extract_content_type, +) + +import logging +import os + +from slack_sdk.oauth.installation_store.sqlite3 import SQLite3InstallationStore +from slack_sdk.web import WebClient +from slack_sdk.oauth.token_rotation import TokenRotator +from slack_sdk.oauth import AuthorizeUrlGenerator, RedirectUriPageRenderer +from slack_sdk.oauth.installation_store import Installation +from slack_sdk.oauth.state_store.sqlite3 import SQLite3OAuthStateStore + +client_id = os.environ["SLACK_CLIENT_ID"] +client_secret = os.environ["SLACK_CLIENT_SECRET"] +scopes = ["app_mentions:read", "chat:write", "commands"] +user_scopes = ["search:read"] + +logging.basicConfig(level=logging.DEBUG) +logger = logging.getLogger(__name__) +logging.getLogger("sqlalchemy.engine").setLevel(logging.INFO) + +import sqlalchemy +from sqlalchemy.engine import Engine + +database_url = "sqlite:///slackapp.db" +# database_url = "postgresql://localhost/slackapp" # pip install psycopg2 +engine: Engine = sqlalchemy.create_engine(database_url) + +installation_store = SQLite3InstallationStore( + database="test.db", + client_id=client_id, + logger=logger, +) +installation_store.init() + +token_rotator = TokenRotator( + client_id=client_id, + client_secret=client_secret, +) + +state_store = SQLite3OAuthStateStore( + database="test.db", + logger=logger, + expiration_seconds=300, +) +state_store.init() + +# --------------------- +# Flask App for Slack events +# --------------------- + +import json +from slack_sdk.errors import SlackApiError +from slack_sdk.signature import SignatureVerifier + +signing_secret = os.environ["SLACK_SIGNING_SECRET"] +signature_verifier = SignatureVerifier(signing_secret=signing_secret) + + +def rotate_tokens( + enterprise_id: Optional[str] = None, + team_id: Optional[str] = None, + user_id: Optional[str] = None, + is_enterprise_install: Optional[bool] = None, +): + installation = installation_store.find_installation( + enterprise_id=enterprise_id, + team_id=team_id, + user_id=user_id, + is_enterprise_install=is_enterprise_install, + ) + if installation is not None: + updated_installation = token_rotator.perform_token_rotation( + installation=installation, + minutes_before_expiration=60 * 24 * 365, # one year for testing + ) + if updated_installation is not None: + installation_store.save(updated_installation) + + +from flask import Flask, request, make_response + +app = Flask(__name__) +app.debug = True + + +@app.route("/slack/events", methods=["POST"]) +def slack_app(): + if not signature_verifier.is_valid( + body=request.get_data(), + timestamp=request.headers.get("X-Slack-Request-Timestamp"), + signature=request.headers.get("X-Slack-Signature"), + ): + return make_response("invalid request", 403) + + raw_body = request.data.decode("utf-8") + body = parse_body(body=raw_body, content_type=extract_content_type(request.headers)) + rotate_tokens( + enterprise_id=extract_enterprise_id(body), + team_id=extract_team_id(body), + user_id=extract_user_id(body), + is_enterprise_install=extract_is_enterprise_install(body), + ) + + if "command" in request.form and request.form["command"] == "/token-rotation-modal": + try: + enterprise_id = request.form.get("enterprise_id") + team_id = request.form["team_id"] + bot = installation_store.find_bot( + enterprise_id=enterprise_id, + team_id=team_id, + ) + bot_token = bot.bot_token if bot else None + if not bot_token: + return make_response("Please install this app first!", 200) + + client = WebClient(token=bot_token) + trigger_id = request.form["trigger_id"] + response = client.views_open( + trigger_id=trigger_id, + view={ + "type": "modal", + "callback_id": "modal-id", + "title": {"type": "plain_text", "text": "Awesome Modal"}, + "submit": {"type": "plain_text", "text": "Submit"}, + "close": {"type": "plain_text", "text": "Cancel"}, + "blocks": [ + { + "type": "input", + "block_id": "b-id", + "label": { + "type": "plain_text", + "text": "Input label", + }, + "element": { + "action_id": "a-id", + "type": "plain_text_input", + }, + } + ], + }, + ) + return make_response("", 200) + except SlackApiError as e: + code = e.response["error"] + return make_response(f"Failed to open a modal due to {code}", 200) + + elif "payload" in request.form: + payload = json.loads(request.form["payload"]) + if payload["type"] == "view_submission" and payload["view"]["callback_id"] == "modal-id": + submitted_data = payload["view"]["state"]["values"] + print(submitted_data) # {'b-id': {'a-id': {'type': 'plain_text_input', 'value': 'your input'}}} + return make_response("", 200) + + else: + if raw_body.startswith("{"): + event_payload = json.loads(raw_body) + logger.info(f"Events API payload: {event_payload}") + if event_payload.get("type") == "url_verification": + return make_response(event_payload.get("challenge"), 200) + return make_response("", 200) + + return make_response("", 404) + + +# --------------------- +# Flask App for Slack OAuth flow +# --------------------- + +authorization_url_generator = AuthorizeUrlGenerator( + client_id=client_id, + scopes=scopes, + user_scopes=user_scopes, +) +redirect_page_renderer = RedirectUriPageRenderer( + install_path="/slack/install", + redirect_uri_path="/slack/oauth_redirect", +) + + +@app.route("/slack/install", methods=["GET"]) +def oauth_start(): + state = state_store.issue() + url = authorization_url_generator.generate(state) + return ( + '' + f'' + f'' + "" + ) + + +@app.route("/slack/oauth_redirect", methods=["GET"]) +def oauth_callback(): + # Retrieve the auth code and state from the request params + if "code" in request.args: + state = request.args["state"] + if state_store.consume(state): + code = request.args["code"] + client = WebClient() # no prepared token needed for this app + oauth_response = client.oauth_v2_access(client_id=client_id, client_secret=client_secret, code=code) + logger.info(f"oauth.v2.access response: {oauth_response}") + + installed_enterprise = oauth_response.get("enterprise", {}) + is_enterprise_install = oauth_response.get("is_enterprise_install") + installed_team = oauth_response.get("team", {}) + installer = oauth_response.get("authed_user", {}) + incoming_webhook = oauth_response.get("incoming_webhook", {}) + + bot_token = oauth_response.get("access_token") + # NOTE: oauth.v2.access doesn't include bot_id in response + bot_id = None + enterprise_url = None + if bot_token is not None: + auth_test = client.auth_test(token=bot_token) + bot_id = auth_test["bot_id"] + if is_enterprise_install is True: + enterprise_url = auth_test.get("url") + + installation = Installation( + app_id=oauth_response.get("app_id"), + enterprise_id=installed_enterprise.get("id"), + enterprise_name=installed_enterprise.get("name"), + enterprise_url=enterprise_url, + team_id=installed_team.get("id"), + team_name=installed_team.get("name"), + bot_token=bot_token, + bot_id=bot_id, + bot_user_id=oauth_response.get("bot_user_id"), + bot_scopes=oauth_response.get("scope"), # comma-separated string + bot_refresh_token=oauth_response.get("refresh_token"), + bot_token_expires_in=oauth_response.get("expires_in"), + user_id=installer.get("id"), + user_token=installer.get("access_token"), + user_scopes=installer.get("scope"), # comma-separated string + user_refresh_token=installer.get("refresh_token"), + user_token_expires_in=installer.get("expires_in"), + incoming_webhook_url=incoming_webhook.get("url"), + incoming_webhook_channel=incoming_webhook.get("channel"), + incoming_webhook_channel_id=incoming_webhook.get("channel_id"), + incoming_webhook_configuration_url=incoming_webhook.get("configuration_url"), + is_enterprise_install=is_enterprise_install, + token_type=oauth_response.get("token_type"), + ) + installation_store.save(installation) + return redirect_page_renderer.render_success_page( + app_id=installation.app_id, + team_id=installation.team_id, + is_enterprise_install=installation.is_enterprise_install, + enterprise_url=installation.enterprise_url, + ) + else: + return redirect_page_renderer.render_failure_page("the state value is already expired") + + error = request.args["error"] if "error" in request.args else "" + return redirect_page_renderer.render_failure_page(error) + + +if __name__ == "__main__": + # export SLACK_CLIENT_ID=123.123 + # export SLACK_CLIENT_SECRET=xxx + # export SLACK_SIGNING_SECRET=*** + # export FLASK_ENV=development + + app.run("localhost", 3000) + + # python3 integration_tests/samples/token_rotation/oauth_sqlite3.py + # ngrok http 3000 + # https://{yours}.ngrok.io/slack/oauth/start diff --git a/integration_tests/samples/token_rotation/util.py b/integration_tests/samples/token_rotation/util.py new file mode 100644 index 000000000..f0d201199 --- /dev/null +++ b/integration_tests/samples/token_rotation/util.py @@ -0,0 +1,88 @@ +import json +from typing import Optional, Dict, Any, Sequence +from urllib.parse import parse_qsl + + +def parse_body(body: str, content_type: Optional[str]) -> Dict[str, Any]: + if not body: + return {} + if (content_type is not None and content_type == "application/json") or body.startswith("{"): + return json.loads(body) + else: + if "payload" in body: # This is not JSON format yet + params = dict(parse_qsl(body)) + if params.get("payload") is not None: + return json.loads(params.get("payload")) + else: + return {} + else: + return dict(parse_qsl(body)) + + +def extract_is_enterprise_install(payload: Dict[str, Any]) -> Optional[bool]: + if "is_enterprise_install" in payload: + is_enterprise_install = payload.get("is_enterprise_install") + return is_enterprise_install is not None and (is_enterprise_install is True or is_enterprise_install == "true") + return False + + +def extract_enterprise_id(payload: Dict[str, Any]) -> Optional[str]: + if payload.get("enterprise") is not None: + org = payload.get("enterprise") + if isinstance(org, str): + return org + elif "id" in org: + return org.get("id") # type: ignore + if payload.get("authorizations") is not None and len(payload["authorizations"]) > 0: + # To make Events API handling functioning also for shared channels, + # we should use .authorizations[0].enterprise_id over .enterprise_id + return extract_enterprise_id(payload["authorizations"][0]) + if "enterprise_id" in payload: + return payload.get("enterprise_id") + if payload.get("team") is not None and "enterprise_id" in payload["team"]: + # In the case where the type is view_submission + return payload["team"].get("enterprise_id") + if payload.get("event") is not None: + return extract_enterprise_id(payload["event"]) + return None + + +def extract_team_id(payload: Dict[str, Any]) -> Optional[str]: + if payload.get("team") is not None: + team = payload.get("team") + if isinstance(team, str): + return team + elif team and "id" in team: + return team.get("id") + if payload.get("authorizations") is not None and len(payload["authorizations"]) > 0: + # To make Events API handling functioning also for shared channels, + # we should use .authorizations[0].team_id over .team_id + return extract_team_id(payload["authorizations"][0]) + if "team_id" in payload: + return payload.get("team_id") + if payload.get("event") is not None: + return extract_team_id(payload["event"]) + if payload.get("user") is not None: + return payload.get("user")["team_id"] + return None + + +def extract_user_id(payload: Dict[str, Any]) -> Optional[str]: + if payload.get("user") is not None: + user = payload.get("user") + if isinstance(user, str): + return user + elif "id" in user: + return user.get("id") # type: ignore + if "user_id" in payload: + return payload.get("user_id") + if payload.get("event") is not None: + return extract_user_id(payload["event"]) + return None + + +def extract_content_type(headers: Dict[str, Sequence[str]]) -> Optional[str]: + content_type: Optional[str] = headers.get("content-type", [None])[0] + if content_type: + return content_type.split(";")[0] + return None diff --git a/integration_tests/samples/workflows/steps_from_apps.py b/integration_tests/samples/workflows/steps_from_apps.py new file mode 100644 index 000000000..f77d6187a --- /dev/null +++ b/integration_tests/samples/workflows/steps_from_apps.py @@ -0,0 +1,177 @@ +import logging + +logging.basicConfig(level=logging.DEBUG) + +# --------------------- +# Flask App for Slack OAuth flow +# --------------------- + +import os +import json +from slack_sdk.web import WebClient +from slack_sdk.signature import SignatureVerifier + +logger = logging.getLogger(__name__) +signature_verifier = SignatureVerifier(os.environ["SLACK_SIGNING_SECRET"]) +client = WebClient(token=os.environ["SLACK_BOT_TOKEN"]) + +# --------------------- +# Flask App for Slack events +# --------------------- + +from concurrent.futures.thread import ThreadPoolExecutor + +executor = ThreadPoolExecutor(max_workers=5) + +# pip3 install flask +from flask import Flask, request, make_response + +app = Flask(__name__) +app.debug = True + + +@app.route("/slack/events", methods=["POST"]) +def slack_app(): + request_body = request.get_data() + if not signature_verifier.is_valid_request(request_body, request.headers): + return make_response("invalid request", 403) + + if request.headers["content-type"] == "application/json": + # Events API + body = json.loads(request_body) + if body["event"]["type"] == "workflow_step_execute": + step = body["event"]["workflow_step"] + + def handle_step(): + try: + client.workflows_stepCompleted( + workflow_step_execute_id=step["workflow_step_execute_id"], + outputs={ + "taskName": step["inputs"]["taskName"]["value"], + "taskDescription": step["inputs"]["taskDescription"]["value"], + "taskAuthorEmail": step["inputs"]["taskAuthorEmail"]["value"], + }, + ) + except Exception as err: + client.workflows_stepFailed( + workflow_step_execute_id=step["workflow_step_execute_id"], + error={ + "message": f"Something went wrong! ({err})", + }, + ) + + executor.submit(handle_step) + return make_response("", 200) + + elif "payload" in request.form: + # Action / View Submission + body = json.loads(request.form["payload"]) + + if body["type"] == "workflow_step_edit": + new_modal = client.views_open( + trigger_id=body["trigger_id"], + view={ + "type": "workflow_step", + "callback_id": "copy_review_view", + "blocks": [ + { + "type": "section", + "block_id": "intro-section", + "text": { + "type": "plain_text", + "text": "Create a task in one of the listed projects. The link to the task and other details will be available as variable data in later steps.", + }, + }, + { + "type": "input", + "block_id": "task_name_input", + "element": { + "type": "plain_text_input", + "action_id": "task_name", + "placeholder": { + "type": "plain_text", + "text": "Write a task name", + }, + }, + "label": {"type": "plain_text", "text": "Task name"}, + }, + { + "type": "input", + "block_id": "task_description_input", + "element": { + "type": "plain_text_input", + "action_id": "task_description", + "placeholder": { + "type": "plain_text", + "text": "Write a description for your task", + }, + }, + "label": {"type": "plain_text", "text": "Task description"}, + }, + { + "type": "input", + "block_id": "task_author_input", + "element": { + "type": "plain_text_input", + "action_id": "task_author", + "placeholder": { + "type": "plain_text", + "text": "Write a task name", + }, + }, + "label": {"type": "plain_text", "text": "Task author"}, + }, + ], + }, + ) + return make_response("", 200) + + if body["type"] == "view_submission" and body["view"]["callback_id"] == "copy_review_view": + state_values = body["view"]["state"]["values"] + + client.workflows_updateStep( + workflow_step_edit_id=body["workflow_step"]["workflow_step_edit_id"], + inputs={ + "taskName": { + "value": state_values["task_name_input"]["task_name"]["value"], + }, + "taskDescription": { + "value": state_values["task_description_input"]["task_description"]["value"], + }, + "taskAuthorEmail": { + "value": state_values["task_author_input"]["task_author"]["value"], + }, + }, + outputs=[ + { + "name": "taskName", + "type": "text", + "label": "Task Name", + }, + { + "name": "taskDescription", + "type": "text", + "label": "Task Description", + }, + { + "name": "taskAuthorEmail", + "type": "text", + "label": "Task Author Email", + }, + ], + ) + return make_response("", 200) + + return make_response("", 404) + + +if __name__ == "__main__": + # export SLACK_BOT_TOKEN=*** + # export SLACK_SIGNING_SECRET=*** + # export FLASK_ENV=development + + app.run("localhost", 3000) + + # python3 integration_tests/samples/workflows/steps_from_apps.py + # ngrok http 3000 + # POST https://{yours}.ngrok.io/slack/events diff --git a/integration_tests/scim/test_scim_client_read.py b/integration_tests/scim/test_scim_client_read.py new file mode 100644 index 000000000..700dbceef --- /dev/null +++ b/integration_tests/scim/test_scim_client_read.py @@ -0,0 +1,74 @@ +import logging +import os +import unittest + +from integration_tests.env_variable_names import ( + SLACK_SDK_TEST_GRID_ORG_ADMIN_USER_TOKEN, +) +from slack_sdk.scim import SCIMClient, SCIMResponse + + +class TestSCIMClient(unittest.TestCase): + def setUp(self): + self.logger = logging.getLogger(__name__) + self.bot_token = os.environ[SLACK_SDK_TEST_GRID_ORG_ADMIN_USER_TOKEN] + self.client: SCIMClient = SCIMClient(token=self.bot_token) + + def tearDown(self): + pass + + def test_api_call(self): + response: SCIMResponse = self.client.api_call( + http_verb="GET", path="Users", query_params={"startIndex": 1, "count": 1} + ) + self.assertIsNotNone(response) + + self.logger.info(response.snake_cased_body) + self.assertEqual(response.snake_cased_body["start_index"], 1) + self.assertIsNotNone(response.snake_cased_body["resources"][0]["id"]) + + def test_lookup_users(self): + search_result = self.client.search_users(start_index=1, count=1) + self.assertIsNotNone(search_result) + + self.logger.info(search_result.snake_cased_body) + self.assertEqual(search_result.snake_cased_body["start_index"], 1) + self.assertIsNotNone(search_result.snake_cased_body["resources"][0]["id"]) + self.assertEqual( + search_result.users[0].id, + search_result.snake_cased_body["resources"][0]["id"], + ) + + read_result = self.client.read_user(search_result.users[0].id) + self.assertIsNotNone(read_result) + self.logger.info(read_result.snake_cased_body) + self.assertEqual(read_result.user.id, search_result.users[0].id) + + def test_lookup_users_error(self): + # error + error_result = self.client.search_users(start_index=1, count=1, filter="foo") + self.assertEqual(error_result.errors.code, 400) + self.assertEqual(error_result.errors.description, "no_filters (is_aggregate_call=1)") + + def test_lookup_groups(self): + search_result = self.client.search_groups(start_index=1, count=1) + self.assertIsNotNone(search_result) + + self.logger.info(search_result.snake_cased_body) + self.assertEqual(search_result.snake_cased_body["start_index"], 1) + self.assertIsNotNone(search_result.snake_cased_body["resources"][0]["id"]) + self.assertEqual( + search_result.groups[0].id, + search_result.snake_cased_body["resources"][0]["id"], + ) + + read_result = self.client.read_group(search_result.groups[0].id) + self.assertIsNotNone(read_result) + self.logger.info(read_result.snake_cased_body) + self.assertEqual(read_result.group.id, search_result.groups[0].id) + + def test_lookup_groups_error(self): + # error + error_result = self.client.search_groups(start_index=1, count=-1, filter="foo") + self.assertEqual(error_result.errors.code, 400) + self.assertEqual(error_result.errors.description, "no_filters (is_aggregate_call=1)") diff --git a/integration_tests/scim/test_scim_client_write.py b/integration_tests/scim/test_scim_client_write.py new file mode 100644 index 000000000..bb598e003 --- /dev/null +++ b/integration_tests/scim/test_scim_client_write.py @@ -0,0 +1,125 @@ +import logging +import os +import time +import unittest + +from integration_tests.env_variable_names import ( + SLACK_SDK_TEST_GRID_ORG_ADMIN_USER_TOKEN, +) +from slack_sdk.scim import SCIMClient +from slack_sdk.scim.v1.group import Group, GroupMember +from slack_sdk.scim.v1.user import User, UserName, UserEmail + + +class TestSCIMClient(unittest.TestCase): + def setUp(self): + self.logger = logging.getLogger(__name__) + self.bot_token = os.environ[SLACK_SDK_TEST_GRID_ORG_ADMIN_USER_TOKEN] + self.client: SCIMClient = SCIMClient(token=self.bot_token) + + def tearDown(self): + pass + + def test_user_crud(self): + now = str(time.time())[:10] + user = User( + user_name=f"user_{now}", + name=UserName(given_name="Kaz", family_name="Sera"), + emails=[UserEmail(value=f"seratch+{now}@example.com")], + schemas=[ + "urn:scim:schemas:core:1.0", + # "urn:scim:schemas:extension:enterprise:1.0", + # "urn:scim:schemas:extension:slack:guest:1.0" + ], + # additional_fields={ + # "urn:scim:schemas:extension:slack:guest:1.0": { + # "type": "multi", + # "expiration": "2022-11-30T23:59:59Z" + # } + # } + ) + creation = self.client.create_user(user) + self.assertEqual(creation.status_code, 201) + + patch_result = self.client.patch_user( + id=creation.user.id, + partial_user=User( + user_name=f"user_{now}_2", + name=UserName(given_name="Kazuhiro", family_name="Sera"), + ), + ) + self.assertEqual(patch_result.status_code, 200) + + # Patch using dict + # snake_cased keys will be automatically converted to camelCase + patch_result_2 = self.client.patch_user( + id=creation.user.id, + partial_user={ + "user_name": f"user_{now}_3", + "name": { + "given_name": "Kaz", + "family_name": "Sera", + }, + }, + ) + self.assertEqual(patch_result_2.status_code, 200) + self.assertEqual(patch_result_2.user.user_name, f"user_{now}_3") + self.assertEqual(patch_result_2.user.name.given_name, "Kaz") + + # using camelCase also works + patch_result_3 = self.client.patch_user( + id=creation.user.id, + partial_user={ + "userName": f"user_{now}_4", + "name": { + "givenName": "Kazuhiro", + "familyName": "Sera", + }, + }, + ) + self.assertEqual(patch_result_3.status_code, 200) + self.assertEqual(patch_result_3.user.user_name, f"user_{now}_4") + self.assertEqual(patch_result_3.user.name.given_name, "Kazuhiro") + + updated_user = creation.user + updated_user.name = UserName(given_name="Foo", family_name="Bar") + update_result = self.client.update_user(user=updated_user) + self.assertEqual(update_result.status_code, 200) + + delete_result = self.client.delete_user(updated_user.id) + self.assertEqual(delete_result.status_code, 200) + + def test_group_crud(self): + now = str(time.time())[:10] + + user = User( + user_name=f"user_{now}", + name=UserName(given_name="Kaz", family_name="Sera"), + emails=[UserEmail(value=f"seratch+{now}@example.com")], + schemas=["urn:scim:schemas:core:1.0"], + ) + user_creation = self.client.create_user(user) + group = Group( + display_name=f"TestGroup_{now}", + members=[GroupMember(value=user_creation.user.id)], + ) + creation = self.client.create_group(group) + self.assertEqual(creation.status_code, 201) + + group = creation.group + + patch_result = self.client.patch_group( + id=group.id, + partial_group=Group( + display_name=f"Test Group{now}_2", + ), + ) + self.assertEqual(patch_result.status_code, 204) + + updated_group = group + updated_group.display_name = f"Test Group{now}_3" + update_result = self.client.update_group(updated_group) + self.assertEqual(update_result.status_code, 200) + + delete_result = self.client.delete_group(updated_group.id) + self.assertEqual(delete_result.status_code, 204) diff --git a/integration_tests/web/test_admin_analytics.py b/integration_tests/web/test_admin_analytics.py new file mode 100644 index 000000000..7b309d959 --- /dev/null +++ b/integration_tests/web/test_admin_analytics.py @@ -0,0 +1,114 @@ +import logging +import os +import unittest + +from integration_tests.env_variable_names import ( + SLACK_SDK_TEST_GRID_ORG_ADMIN_USER_TOKEN, +) +from integration_tests.helpers import async_test +from slack_sdk.errors import SlackApiError +from slack_sdk.web.legacy_client import LegacyWebClient +from slack_sdk.web import WebClient +from slack_sdk.web.async_client import AsyncWebClient + + +class TestWebClient(unittest.TestCase): + """Runs integration tests with real Slack API""" + + def setUp(self): + self.logger = logging.getLogger(__name__) + self.org_admin_token = os.environ[SLACK_SDK_TEST_GRID_ORG_ADMIN_USER_TOKEN] + self.legacy_client: LegacyWebClient = LegacyWebClient(token=self.org_admin_token) + self.sync_client: WebClient = WebClient(token=self.org_admin_token) + self.async_client: WebClient = AsyncWebClient(token=self.org_admin_token) + + def tearDown(self): + pass + + def test_sync(self): + client = self.sync_client + + response = client.admin_analytics_getFile(date="2022-10-20", type="member") + self.assertTrue(isinstance(response.data, bytes)) + self.assertIsNotNone(response.data) + + def test_sync_error(self): + client = self.sync_client + + try: + client.admin_analytics_getFile(date="2035-12-31", type="member") + except SlackApiError as e: + self.assertFalse(e.response["ok"]) + self.assertEqual("file_not_yet_available", e.response["error"]) + + def test_sync_public_channel(self): + client = self.sync_client + + response = client.admin_analytics_getFile(date="2022-10-20", type="public_channel") + self.assertTrue(isinstance(response.data, bytes)) + self.assertIsNotNone(response.data) + + def test_sync_public_channel_medata_only(self): + client = self.sync_client + + response = client.admin_analytics_getFile(type="public_channel", metadata_only=True) + self.assertTrue(isinstance(response.data, bytes)) + self.assertIsNotNone(response.data) + + @async_test + async def test_async(self): + client = self.async_client + + response = await client.admin_analytics_getFile(date="2022-10-20", type="member") + self.assertTrue(isinstance(response.data, bytes)) + self.assertIsNotNone(response.data) + + @async_test + async def test_async_error(self): + client = self.async_client + + try: + await client.admin_analytics_getFile(date="2035-12-31", type="member") + except SlackApiError as e: + self.assertFalse(e.response["ok"]) + self.assertEqual("file_not_yet_available", e.response["error"]) + + @async_test + async def test_async_public_channel(self): + client = self.async_client + + response = await client.admin_analytics_getFile(date="2022-10-20", type="public_channel") + self.assertTrue(isinstance(response.data, bytes)) + self.assertIsNotNone(response.data) + + @async_test + async def test_async_public_channel_metadata_only(self): + client = self.async_client + + response = await client.admin_analytics_getFile( + type="public_channel", + metadata_only=True, + ) + self.assertTrue(isinstance(response.data, bytes)) + self.assertIsNotNone(response.data) + + def test_legacy(self): + client = self.legacy_client + + response = client.admin_analytics_getFile(date="2022-10-20", type="member") + self.assertTrue(isinstance(response.data, bytes)) + self.assertIsNotNone(response.data) + + def test_legacy_public_channel(self): + client = self.legacy_client + + response = client.admin_analytics_getFile(date="2022-10-20", type="public_channel") + self.assertTrue(isinstance(response.data, bytes)) + self.assertIsNotNone(response.data) + + def test_legacy_public_channel_metadata_only(self): + client = self.legacy_client + + response = client.admin_analytics_getFile(type="public_channel", metadata_only=True) + self.assertTrue(isinstance(response.data, bytes)) + self.assertIsNotNone(response.data) diff --git a/integration_tests/web/test_admin_auth_policy.py b/integration_tests/web/test_admin_auth_policy.py new file mode 100644 index 000000000..d83ad8c82 --- /dev/null +++ b/integration_tests/web/test_admin_auth_policy.py @@ -0,0 +1,70 @@ +import logging +import os +import unittest + +from integration_tests.env_variable_names import ( + SLACK_SDK_TEST_GRID_ORG_ADMIN_USER_TOKEN, + SLACK_SDK_TEST_GRID_USER_ID_ADMIN_AUTH, +) +from integration_tests.helpers import async_test +from slack_sdk.web import WebClient +from slack_sdk.web.async_client import AsyncWebClient + + +class TestWebClient(unittest.TestCase): + """Runs integration tests with real Slack API""" + + def setUp(self): + self.logger = logging.getLogger(__name__) + self.org_admin_token = os.environ[SLACK_SDK_TEST_GRID_ORG_ADMIN_USER_TOKEN] + self.sync_client: WebClient = WebClient(token=self.org_admin_token) + self.async_client: AsyncWebClient = AsyncWebClient(token=self.org_admin_token) + self.user_ids = [os.environ[SLACK_SDK_TEST_GRID_USER_ID_ADMIN_AUTH]] + + def tearDown(self): + pass + + def test_sync(self): + client = self.sync_client + + list = client.admin_auth_policy_getEntities(policy_name="email_password", limit=3) + self.assertIsNotNone(list) + + assignment = client.admin_auth_policy_assignEntities( + entity_ids=self.user_ids, + policy_name="email_password", + entity_type="USER", + ) + self.assertIsNotNone(assignment) + self.assertEqual(list["entity_total_count"] + 1, assignment["entity_total_count"]) + + removal = client.admin_auth_policy_removeEntities( + entity_ids=self.user_ids, + policy_name="email_password", + entity_type="USER", + ) + self.assertIsNotNone(removal) + self.assertEqual(list["entity_total_count"], removal["entity_total_count"]) + + @async_test + async def test_async(self): + client = self.async_client + + list = await client.admin_auth_policy_getEntities(policy_name="email_password", limit=3) + self.assertIsNotNone(list) + + assignment = await client.admin_auth_policy_assignEntities( + entity_ids=self.user_ids, + policy_name="email_password", + entity_type="USER", + ) + self.assertIsNotNone(assignment) + self.assertEqual(list["entity_total_count"] + 1, assignment["entity_total_count"]) + + removal = await client.admin_auth_policy_removeEntities( + entity_ids=self.user_ids, + policy_name="email_password", + entity_type="USER", + ) + self.assertIsNotNone(removal) + self.assertEqual(list["entity_total_count"], removal["entity_total_count"]) diff --git a/integration_tests/web/test_admin_barriers.py b/integration_tests/web/test_admin_barriers.py new file mode 100644 index 000000000..b23591597 --- /dev/null +++ b/integration_tests/web/test_admin_barriers.py @@ -0,0 +1,77 @@ +import logging +import os +import unittest + +from integration_tests.env_variable_names import ( + SLACK_SDK_TEST_GRID_ORG_ADMIN_USER_TOKEN, + SLACK_SDK_TEST_GRID_IDP_USERGROUP_ID, + SLACK_SDK_TEST_GRID_IDP_USERGROUP_ID_2, +) +from integration_tests.helpers import async_test +from slack_sdk.web import WebClient +from slack_sdk.web.async_client import AsyncWebClient + + +class TestWebClient(unittest.TestCase): + """Runs integration tests with real Slack API""" + + def setUp(self): + self.logger = logging.getLogger(__name__) + self.org_admin_token = os.environ[SLACK_SDK_TEST_GRID_ORG_ADMIN_USER_TOKEN] + self.sync_client: WebClient = WebClient(token=self.org_admin_token) + self.async_client: AsyncWebClient = AsyncWebClient(token=self.org_admin_token) + + self.idp_usergroup_id1 = os.environ[SLACK_SDK_TEST_GRID_IDP_USERGROUP_ID] + self.idp_usergroup_id2 = os.environ[SLACK_SDK_TEST_GRID_IDP_USERGROUP_ID_2] + + def tearDown(self): + pass + + def test_sync(self): + client = self.sync_client + + list = client.admin_barriers_list(limit=1000) + self.assertIsNotNone(list) + + for barrier in list["barriers"]: + client.admin_barriers_delete(barrier_id=barrier["id"]) + + creation = client.admin_barriers_create( + primary_usergroup_id=self.idp_usergroup_id1, + barriered_from_usergroup_ids=[self.idp_usergroup_id2], + restricted_subjects=["call", "im", "mpim"], + ) + self.assertIsNotNone(creation) + + modification = client.admin_barriers_update( + barrier_id=creation["barrier"]["id"], + primary_usergroup_id=self.idp_usergroup_id2, + barriered_from_usergroup_ids=[self.idp_usergroup_id1], + restricted_subjects=["call", "im", "mpim"], + ) + self.assertIsNotNone(modification) + + @async_test + async def test_async(self): + client = self.async_client + + list = await client.admin_barriers_list(limit=1000) + self.assertIsNotNone(list) + + for barrier in list["barriers"]: + await client.admin_barriers_delete(barrier_id=barrier["id"]) + + creation = await client.admin_barriers_create( + primary_usergroup_id=self.idp_usergroup_id1, + barriered_from_usergroup_ids=[self.idp_usergroup_id2], + restricted_subjects=["call", "im", "mpim"], + ) + self.assertIsNotNone(creation) + + modification = await client.admin_barriers_update( + barrier_id=creation["barrier"]["id"], + primary_usergroup_id=self.idp_usergroup_id2, + barriered_from_usergroup_ids=[self.idp_usergroup_id1], + restricted_subjects=["call", "im", "mpim"], + ) + self.assertIsNotNone(modification) diff --git a/integration_tests/web/test_admin_conversations.py b/integration_tests/web/test_admin_conversations.py new file mode 100644 index 000000000..95fc0060c --- /dev/null +++ b/integration_tests/web/test_admin_conversations.py @@ -0,0 +1,220 @@ +import asyncio +import logging +import os +import time +import unittest + +from integration_tests.env_variable_names import ( + SLACK_SDK_TEST_GRID_ORG_ADMIN_USER_TOKEN, + SLACK_SDK_TEST_GRID_IDP_USERGROUP_ID, + SLACK_SDK_TEST_GRID_TEAM_ID, + SLACK_SDK_TEST_GRID_USER_ID, +) +from integration_tests.helpers import async_test +from slack_sdk.web import WebClient +from slack_sdk.web.async_client import AsyncWebClient + + +class TestWebClient(unittest.TestCase): + """Runs integration tests with real Slack API""" + + # TODO: admin_conversations_disconnectShared - not_allowed_token_type + # TODO: admin_conversations_ekm_listOriginalConnectedChannelInfo - enable the feature + + def setUp(self): + self.logger = logging.getLogger(__name__) + self.org_admin_token = os.environ[SLACK_SDK_TEST_GRID_ORG_ADMIN_USER_TOKEN] + self.sync_client: WebClient = WebClient(token=self.org_admin_token) + self.async_client: AsyncWebClient = AsyncWebClient(token=self.org_admin_token) + + self.team_id = os.environ[SLACK_SDK_TEST_GRID_TEAM_ID] + self.idp_group_id = os.environ[SLACK_SDK_TEST_GRID_IDP_USERGROUP_ID] + self.user_id = os.environ[SLACK_SDK_TEST_GRID_USER_ID] + self.channel_name = f"test-channel-{int(round(time.time() * 1000))}" + self.channel_rename = f"test-channel-renamed-{int(round(time.time() * 1000))}" + + def tearDown(self): + pass + + def test_sync(self): + client = self.sync_client + + conv_creation = client.admin_conversations_create( + is_private=False, + name=self.channel_name, + team_id=self.team_id, + ) + self.assertIsNotNone(conv_creation) + created_channel_id = conv_creation.data["channel_id"] + + self.assertIsNotNone(client.admin_conversations_lookup(last_message_activity_before=100, team_ids=[self.team_id])) + + self.assertIsNotNone( + client.admin_conversations_invite( + channel_id=created_channel_id, + user_ids=[self.user_id], + ) + ) + self.assertIsNotNone( + client.admin_conversations_archive( + channel_id=created_channel_id, + ) + ) + self.assertIsNotNone( + client.admin_conversations_unarchive( + channel_id=created_channel_id, + ) + ) + self.assertIsNotNone( + client.admin_conversations_rename( + channel_id=created_channel_id, + name=self.channel_rename, + ) + ) + search_result = client.admin_conversations_search( + limit=1, + sort="member_count", + sort_dir="desc", + ) + self.assertIsNotNone(search_result.data["next_cursor"]) + self.assertIsNotNone(search_result.data["conversations"]) + + self.assertIsNotNone( + client.admin_conversations_getConversationPrefs( + channel_id=created_channel_id, + ) + ) + self.assertIsNotNone( + client.admin_conversations_setConversationPrefs( + channel_id=created_channel_id, + prefs={}, + ) + ) + + self.assertIsNotNone( + client.admin_conversations_getTeams( + channel_id=created_channel_id, + ) + ) + self.assertIsNotNone( + client.admin_conversations_setTeams( + team_id=self.team_id, + channel_id=created_channel_id, + org_channel=True, + ) + ) + time.sleep(2) # To avoid channel_not_found + self.assertIsNotNone( + client.admin_conversations_convertToPrivate( + channel_id=created_channel_id, + ) + ) + time.sleep(2) # To avoid internal_error + self.assertIsNotNone( + client.admin_conversations_convertToPublic( + channel_id=created_channel_id, + ) + ) + time.sleep(2) # To avoid internal_error + self.assertIsNotNone( + client.admin_conversations_archive( + channel_id=created_channel_id, + ) + ) + time.sleep(2) # To avoid internal_error + self.assertIsNotNone( + client.admin_conversations_delete( + channel_id=created_channel_id, + ) + ) + + @async_test + async def test_async(self): + # await asyncio.sleep(seconds) are included to avoid rate limiting errors + + client = self.async_client + + conv_creation = await client.admin_conversations_create( + is_private=False, + name=self.channel_name, + team_id=self.team_id, + ) + self.assertIsNotNone(conv_creation) + created_channel_id = conv_creation.data["channel_id"] + + self.assertIsNotNone( + await client.admin_conversations_lookup(last_message_activity_before=100, team_ids=[self.team_id]) + ) + + self.assertIsNotNone( + await client.admin_conversations_invite( + channel_id=created_channel_id, + user_ids=[self.user_id], + ) + ) + self.assertIsNotNone( + await client.admin_conversations_archive( + channel_id=created_channel_id, + ) + ) + self.assertIsNotNone( + await client.admin_conversations_unarchive( + channel_id=created_channel_id, + ) + ) + self.assertIsNotNone( + await client.admin_conversations_rename( + channel_id=created_channel_id, + name=self.channel_rename, + ) + ) + self.assertIsNotNone(await client.admin_conversations_search()) + + self.assertIsNotNone( + await client.admin_conversations_getConversationPrefs( + channel_id=created_channel_id, + ) + ) + self.assertIsNotNone( + await client.admin_conversations_setConversationPrefs( + channel_id=created_channel_id, + prefs={}, + ) + ) + + self.assertIsNotNone( + await client.admin_conversations_getTeams( + channel_id=created_channel_id, + ) + ) + self.assertIsNotNone( + await client.admin_conversations_setTeams( + team_id=self.team_id, + channel_id=created_channel_id, + org_channel=True, + ) + ) + await asyncio.sleep(2) # To avoid channel_not_found + self.assertIsNotNone( + await client.admin_conversations_convertToPrivate( + channel_id=created_channel_id, + ) + ) + await asyncio.sleep(2) # To avoid internal_error + self.assertIsNotNone( + await client.admin_conversations_convertToPublic( + channel_id=created_channel_id, + ) + ) + await asyncio.sleep(2) # To avoid internal_error + self.assertIsNotNone( + await client.admin_conversations_archive( + channel_id=created_channel_id, + ) + ) + await asyncio.sleep(2) # To avoid internal_error + self.assertIsNotNone( + await client.admin_conversations_delete( + channel_id=created_channel_id, + ) + ) diff --git a/integration_tests/web/test_admin_conversations_bulk.py b/integration_tests/web/test_admin_conversations_bulk.py new file mode 100644 index 000000000..20981d5b7 --- /dev/null +++ b/integration_tests/web/test_admin_conversations_bulk.py @@ -0,0 +1,148 @@ +import asyncio +import logging +import os +import time +import unittest + +from integration_tests.env_variable_names import ( + SLACK_SDK_TEST_GRID_ORG_ADMIN_USER_TOKEN, + SLACK_SDK_TEST_GRID_TEAM_ID_2, + SLACK_SDK_TEST_GRID_TEAM_ID, +) +from integration_tests.helpers import async_test +from slack_sdk.web import WebClient, SlackResponse +from slack_sdk.errors import SlackApiError +from slack_sdk.web.async_client import AsyncWebClient + + +class TestWebClient(unittest.TestCase): + """Runs integration tests with real Slack API""" + + def setUp(self): + self.logger = logging.getLogger(__name__) + self.org_admin_token = os.environ[SLACK_SDK_TEST_GRID_ORG_ADMIN_USER_TOKEN] + self.sync_client: WebClient = WebClient(token=self.org_admin_token) + self.async_client: AsyncWebClient = AsyncWebClient(token=self.org_admin_token) + + self.team_id = os.environ[SLACK_SDK_TEST_GRID_TEAM_ID] + self.team_id_2 = os.environ[SLACK_SDK_TEST_GRID_TEAM_ID_2] + self.channel_name = f"test-channel-{int(round(time.time() * 1000))}" + + def tearDown(self): + pass + + def test_sync_move(self): + client = self.sync_client + + conv_creation = client.admin_conversations_create( + is_private=False, + name=self.channel_name, + team_id=self.team_id, + ) + self.assertIsNotNone(conv_creation) + created_channel_id = conv_creation.data["channel_id"] + + self.assertIsNotNone( + _get_bulk_response( + client.admin_conversations_bulkMove, + channel_ids=[created_channel_id], + target_team_id=self.team_id_2, + ) + ) + + def test_sync(self): + client = self.sync_client + + conv_creation = client.admin_conversations_create( + is_private=False, + name=self.channel_name, + team_id=self.team_id, + ) + self.assertIsNotNone(conv_creation) + created_channel_id = conv_creation.data["channel_id"] + + self.assertIsNotNone( + _get_bulk_response( + client.admin_conversations_bulkArchive, + channel_ids=[created_channel_id], + ) + ) + + self.assertIsNotNone( + _get_bulk_response( + client.admin_conversations_bulkDelete, + channel_ids=[created_channel_id], + ) + ) + + @async_test + async def test_async_move(self): + client = self.async_client + + conv_creation = await client.admin_conversations_create( + is_private=False, + name=self.channel_name, + team_id=self.team_id, + ) + self.assertIsNotNone(conv_creation) + created_channel_id = conv_creation.data["channel_id"] + + self.assertIsNotNone( + await _get_async_bulk_response( + client.admin_conversations_bulkMove, + channel_ids=[created_channel_id], + target_team_id=self.team_id_2, + ) + ) + + @async_test + async def test_async(self): + client = self.async_client + + conv_creation = await client.admin_conversations_create( + is_private=False, + name=self.channel_name, + team_id=self.team_id, + ) + self.assertIsNotNone(conv_creation) + created_channel_id = conv_creation.data["channel_id"] + + self.assertIsNotNone( + await _get_async_bulk_response( + client.admin_conversations_bulkArchive, + channel_ids=[created_channel_id], + ) + ) + + self.assertIsNotNone( + await _get_async_bulk_response( + client.admin_conversations_bulkDelete, + channel_ids=[created_channel_id], + ) + ) + + +async def _get_async_bulk_response(method, **kwargs) -> SlackResponse: + while True: + try: + return await method(**kwargs) + except SlackApiError as e: + if not _action_in_progress(e.response): + raise e + await asyncio.sleep(3) + + +def _get_bulk_response(method, **kwargs) -> SlackResponse: + while True: + try: + return method(**kwargs) + except SlackApiError as e: + if not _action_in_progress(e.response): + raise e + time.sleep(3) + + +def _action_in_progress(response: SlackResponse) -> bool: + if response.data.get("error", "") == "action_already_in_progress": + return True + return False diff --git a/integration_tests/web/test_admin_conversations_restrictAccess.py b/integration_tests/web/test_admin_conversations_restrictAccess.py new file mode 100644 index 000000000..97915abe5 --- /dev/null +++ b/integration_tests/web/test_admin_conversations_restrictAccess.py @@ -0,0 +1,101 @@ +import asyncio +import logging +import os +import time +import unittest + +from integration_tests.env_variable_names import ( + SLACK_SDK_TEST_GRID_WORKSPACE_ADMIN_USER_TOKEN, + SLACK_SDK_TEST_GRID_ORG_ADMIN_USER_TOKEN, + SLACK_SDK_TEST_GRID_IDP_USERGROUP_ID, + SLACK_SDK_TEST_GRID_TEAM_ID, +) +from integration_tests.helpers import async_test +from slack_sdk.web import WebClient +from slack_sdk.web.async_client import AsyncWebClient + + +class TestWebClient(unittest.TestCase): + """Runs integration tests with real Slack API""" + + def setUp(self): + self.logger = logging.getLogger(__name__) + self.org_admin_token = os.environ[SLACK_SDK_TEST_GRID_ORG_ADMIN_USER_TOKEN] + self.sync_client: WebClient = WebClient(token=self.org_admin_token) + self.async_client: AsyncWebClient = AsyncWebClient(token=self.org_admin_token) + + self.team_id = os.environ[SLACK_SDK_TEST_GRID_TEAM_ID] + self.idp_group_id = os.environ[SLACK_SDK_TEST_GRID_IDP_USERGROUP_ID] + + if not hasattr(self, "channel_id"): + team_admin_token = os.environ[SLACK_SDK_TEST_GRID_WORKSPACE_ADMIN_USER_TOKEN] + client = WebClient(token=team_admin_token) + # Only fetching private channels since admin.conversations.restrictAccess methods + # do not work for public channels + convs = client.conversations_list(exclude_archived=True, limit=100, types="private_channel") + self.channel_id = next( + (c["id"] for c in convs["channels"] if c["name"] != "general" and not c["is_ext_shared"]), + None, + ) + if self.channel_id is None: + millis = int(round(time.time() * 1000)) + channel_name = f"private-test-channel-{millis}" + self.channel_id = client.conversations_create( + name=channel_name, + is_private=True, + )[ + "channel" + ]["id"] + + def tearDown(self): + pass + + def test_sync(self): + # time.sleep(seconds) are included to avoid rate limiting errors + client = self.sync_client + + add_group = client.admin_conversations_restrictAccess_addGroup( + channel_id=self.channel_id, group_id=self.idp_group_id, team_id=self.team_id + ) + self.assertIsNotNone(add_group) + # To avoid rate limiting errors + time.sleep(10) + + list_groups = client.admin_conversations_restrictAccess_listGroups(team_id=self.team_id, channel_id=self.channel_id) + self.assertIsNotNone(list_groups) + # To avoid rate limiting errors + time.sleep(10) + + remove_group = client.admin_conversations_restrictAccess_removeGroup( + channel_id=self.channel_id, group_id=self.idp_group_id, team_id=self.team_id + ) + self.assertIsNotNone(remove_group) + # To avoid rate limiting errors + time.sleep(20) + + @async_test + async def test_async(self): + # await asyncio.sleep(seconds) are included to avoid rate limiting errors + + client = self.async_client + + add_group = await client.admin_conversations_restrictAccess_addGroup( + channel_id=self.channel_id, group_id=self.idp_group_id, team_id=self.team_id + ) + self.assertIsNotNone(add_group) + # To avoid rate limiting errors + await asyncio.sleep(10) + + list_groups = await client.admin_conversations_restrictAccess_listGroups( + team_id=self.team_id, channel_id=self.channel_id + ) + self.assertIsNotNone(list_groups) + # To avoid rate limiting errors + await asyncio.sleep(10) + + remove_group = await client.admin_conversations_restrictAccess_removeGroup( + channel_id=self.channel_id, group_id=self.idp_group_id, team_id=self.team_id + ) + self.assertIsNotNone(remove_group) + # To avoid rate limiting errors + await asyncio.sleep(20) diff --git a/integration_tests/web/test_admin_conversations_retention.py b/integration_tests/web/test_admin_conversations_retention.py new file mode 100644 index 000000000..b2ceaadc0 --- /dev/null +++ b/integration_tests/web/test_admin_conversations_retention.py @@ -0,0 +1,102 @@ +import asyncio +import logging +import os +import time +import unittest + +from integration_tests.env_variable_names import ( + SLACK_SDK_TEST_GRID_ORG_ADMIN_USER_TOKEN, + SLACK_SDK_TEST_GRID_TEAM_ID, +) +from integration_tests.helpers import async_test +from slack_sdk.web import WebClient +from slack_sdk.web.async_client import AsyncWebClient + + +class TestWebClient(unittest.TestCase): + """Runs integration tests with real Slack API""" + + def setUp(self): + self.logger = logging.getLogger(__name__) + self.org_admin_token = os.environ[SLACK_SDK_TEST_GRID_ORG_ADMIN_USER_TOKEN] + self.team_id = os.environ[SLACK_SDK_TEST_GRID_TEAM_ID] + self.sync_client: WebClient = WebClient(token=self.org_admin_token) + self.async_client: AsyncWebClient = AsyncWebClient(token=self.org_admin_token) + + self.channel_name = f"test-channel-{int(round(time.time() * 1000))}" + + def tearDown(self): + pass + + def test_sync(self): + client = self.sync_client + + conv_creation = client.admin_conversations_create( + is_private=False, + name=self.channel_name, + team_id=self.team_id, + ) + self.assertIsNotNone(conv_creation) + created_channel_id = conv_creation.data["channel_id"] + + self.assertIsNotNone( + client.admin_conversations_setCustomRetention( + channel_id=created_channel_id, + duration_days=365, + ) + ) + self.assertIsNotNone( + client.admin_conversations_getCustomRetention( + channel_id=created_channel_id, + ) + ) + self.assertIsNotNone( + client.admin_conversations_removeCustomRetention( + channel_id=created_channel_id, + ) + ) + + time.sleep(2) # To avoid internal_error + self.assertIsNotNone( + client.admin_conversations_delete( + channel_id=created_channel_id, + ) + ) + + @async_test + async def test_async(self): + # await asyncio.sleep(seconds) are included to avoid rate limiting errors + + client = self.async_client + + conv_creation = await client.admin_conversations_create( + is_private=False, + name=self.channel_name, + team_id=self.team_id, + ) + self.assertIsNotNone(conv_creation) + created_channel_id = conv_creation.data["channel_id"] + + self.assertIsNotNone( + await client.admin_conversations_setCustomRetention( + channel_id=created_channel_id, + duration_days=365, + ) + ) + self.assertIsNotNone( + await client.admin_conversations_getCustomRetention( + channel_id=created_channel_id, + ) + ) + self.assertIsNotNone( + await client.admin_conversations_removeCustomRetention( + channel_id=created_channel_id, + ) + ) + + await asyncio.sleep(2) # To avoid internal_error + self.assertIsNotNone( + await client.admin_conversations_delete( + channel_id=created_channel_id, + ) + ) diff --git a/integration_tests/web/test_admin_rate_limit_retries.py b/integration_tests/web/test_admin_rate_limit_retries.py new file mode 100644 index 000000000..86260720a --- /dev/null +++ b/integration_tests/web/test_admin_rate_limit_retries.py @@ -0,0 +1,42 @@ +import logging +import os +import unittest + +import pytest + +from integration_tests.env_variable_names import ( + SLACK_SDK_TEST_GRID_ORG_ADMIN_USER_TOKEN, +) +from integration_tests.helpers import async_test, is_not_specified +from slack_sdk.http_retry import RateLimitErrorRetryHandler +from slack_sdk.http_retry.builtin_async_handlers import AsyncRateLimitErrorRetryHandler +from slack_sdk.web import WebClient +from slack_sdk.web.async_client import AsyncWebClient + + +class TestWebClient(unittest.TestCase): + """Runs integration tests with real Slack API""" + + def setUp(self): + self.logger = logging.getLogger(__name__) + self.org_admin_token = os.environ[SLACK_SDK_TEST_GRID_ORG_ADMIN_USER_TOKEN] + self.sync_client: WebClient = WebClient(token=self.org_admin_token) + self.sync_client.retry_handlers.append(RateLimitErrorRetryHandler(max_retry_count=2)) + self.async_client: AsyncWebClient = AsyncWebClient(token=self.org_admin_token) + self.async_client.retry_handlers.append(AsyncRateLimitErrorRetryHandler(max_retry_count=2)) + + def tearDown(self): + pass + + @pytest.mark.skipif(condition=is_not_specified(), reason="execution can take long") + def test_sync(self): + client = self.sync_client + for response in client.admin_users_session_list(limit=1): + self.assertIsNotNone(response.get("active_sessions")) + + @pytest.mark.skipif(condition=is_not_specified(), reason="execution can take long") + @async_test + async def test_async(self): + client = self.async_client + async for response in await client.admin_users_session_list(limit=1): + self.assertIsNotNone(response.get("active_sessions")) diff --git a/integration_tests/web/test_admin_roles.py b/integration_tests/web/test_admin_roles.py new file mode 100644 index 000000000..c9b79b01f --- /dev/null +++ b/integration_tests/web/test_admin_roles.py @@ -0,0 +1,38 @@ +import asyncio +import logging +import os +import time +import unittest + +from integration_tests.env_variable_names import ( + SLACK_SDK_TEST_GRID_ORG_ADMIN_USER_TOKEN, + SLACK_SDK_TEST_GRID_IDP_USERGROUP_ID, + SLACK_SDK_TEST_GRID_TEAM_ID, + SLACK_SDK_TEST_GRID_USER_ID, +) +from integration_tests.helpers import async_test +from slack_sdk.web import WebClient +from slack_sdk.web.async_client import AsyncWebClient + + +class TestWebClient(unittest.TestCase): + """Runs integration tests with real Slack API""" + + def setUp(self): + self.org_admin_token = os.environ[SLACK_SDK_TEST_GRID_ORG_ADMIN_USER_TOKEN] + + def tearDown(self): + pass + + def test_sync(self): + client: WebClient = WebClient(token=self.org_admin_token) + list_response = client.admin_roles_listAssignments(role_ids=["Rl0A"], limit=3, sort_dir="DESC") + self.assertGreater(len(list_response.get("role_assignments", [])), 0) + # TODO tests for add/remove + + @async_test + async def test_async(self): + client: AsyncWebClient = AsyncWebClient(token=self.org_admin_token) + list_response = await client.admin_roles_listAssignments(role_ids=["Rl0A"], limit=3, sort_dir="DESC") + self.assertGreater(len(list_response.get("role_assignments", [])), 0) + # TODO tests for add/remove diff --git a/integration_tests/web/test_admin_usergroups.py b/integration_tests/web/test_admin_usergroups.py new file mode 100644 index 000000000..2a845dc4d --- /dev/null +++ b/integration_tests/web/test_admin_usergroups.py @@ -0,0 +1,93 @@ +import asyncio +import logging +import os +import unittest + +from integration_tests.env_variable_names import ( + SLACK_SDK_TEST_GRID_WORKSPACE_ADMIN_USER_TOKEN, + SLACK_SDK_TEST_GRID_ORG_ADMIN_USER_TOKEN, + SLACK_SDK_TEST_GRID_IDP_USERGROUP_ID, + SLACK_SDK_TEST_GRID_TEAM_ID, +) +from integration_tests.helpers import async_test +from slack_sdk.web import WebClient +from slack_sdk.web.async_client import AsyncWebClient + + +class TestWebClient(unittest.TestCase): + """Runs integration tests with real Slack API""" + + def setUp(self): + self.logger = logging.getLogger(__name__) + self.org_admin_token = os.environ[SLACK_SDK_TEST_GRID_ORG_ADMIN_USER_TOKEN] + self.sync_client: WebClient = WebClient(token=self.org_admin_token) + self.async_client: AsyncWebClient = AsyncWebClient(token=self.org_admin_token) + + self.team_id = os.environ[SLACK_SDK_TEST_GRID_TEAM_ID] + self.idp_usergroup_id = os.environ[SLACK_SDK_TEST_GRID_IDP_USERGROUP_ID] + + if not hasattr(self, "channel_ids"): + team_admin_token = os.environ[SLACK_SDK_TEST_GRID_WORKSPACE_ADMIN_USER_TOKEN] + client = WebClient(token=team_admin_token) + convs = client.conversations_list(exclude_archived=True, limit=100) + self.channel_ids = [c["id"] for c in convs["channels"] if c["name"] == "general"] + + def tearDown(self): + pass + + def test_sync(self): + client = self.sync_client + + list_channels = client.admin_usergroups_listChannels( + team_id=self.team_id, + usergroup_id=self.idp_usergroup_id, + ) + self.assertIsNotNone(list_channels) + + add_teams = client.admin_usergroups_addTeams( + usergroup_id=self.idp_usergroup_id, + team_ids=self.team_id, + ) + self.assertIsNotNone(add_teams) + + add_channels = client.admin_usergroups_addChannels( + team_id=self.team_id, + usergroup_id=self.idp_usergroup_id, + channel_ids=self.channel_ids, + ) + self.assertIsNotNone(add_channels) + + remove_channels = client.admin_usergroups_removeChannels( + usergroup_id=self.idp_usergroup_id, + channel_ids=self.channel_ids, + ) + self.assertIsNotNone(remove_channels) + + @async_test + async def test_async(self): + client = self.async_client + + list_channels = await client.admin_usergroups_listChannels( + team_id=self.team_id, + usergroup_id=self.idp_usergroup_id, + ) + self.assertIsNotNone(list_channels) + + add_teams = await client.admin_usergroups_addTeams( + usergroup_id=self.idp_usergroup_id, + team_ids=self.team_id, + ) + self.assertIsNotNone(add_teams) + + add_channels = await client.admin_usergroups_addChannels( + team_id=self.team_id, + usergroup_id=self.idp_usergroup_id, + channel_ids=self.channel_ids, + ) + self.assertIsNotNone(add_channels) + + remove_channels = await client.admin_usergroups_removeChannels( + usergroup_id=self.idp_usergroup_id, + channel_ids=self.channel_ids, + ) + self.assertIsNotNone(remove_channels) diff --git a/integration_tests/web/test_admin_users.py b/integration_tests/web/test_admin_users.py new file mode 100644 index 000000000..317354a51 --- /dev/null +++ b/integration_tests/web/test_admin_users.py @@ -0,0 +1,49 @@ +import asyncio +import logging +import os +import unittest + +from integration_tests.env_variable_names import ( + SLACK_SDK_TEST_GRID_WORKSPACE_ADMIN_USER_TOKEN, + SLACK_SDK_TEST_GRID_ORG_ADMIN_USER_TOKEN, + SLACK_SDK_TEST_GRID_IDP_USERGROUP_ID, + SLACK_SDK_TEST_GRID_TEAM_ID, +) +from integration_tests.helpers import async_test +from slack_sdk.web import WebClient +from slack_sdk.web.async_client import AsyncWebClient + + +class TestWebClient(unittest.TestCase): + """Runs integration tests with real Slack API""" + + def setUp(self): + self.logger = logging.getLogger(__name__) + self.org_admin_token = os.environ[SLACK_SDK_TEST_GRID_ORG_ADMIN_USER_TOKEN] + self.sync_client: WebClient = WebClient(token=self.org_admin_token) + self.async_client: AsyncWebClient = AsyncWebClient(token=self.org_admin_token) + + self.team_id = os.environ[SLACK_SDK_TEST_GRID_TEAM_ID] + self.idp_usergroup_id = os.environ[SLACK_SDK_TEST_GRID_IDP_USERGROUP_ID] + + if not hasattr(self, "channel_ids"): + team_admin_token = os.environ[SLACK_SDK_TEST_GRID_WORKSPACE_ADMIN_USER_TOKEN] + client = WebClient(token=team_admin_token) + convs = client.conversations_list(exclude_archived=True, limit=100) + self.channel_ids = [c["id"] for c in convs["channels"] if c["name"] == "general"] + + def tearDown(self): + pass + + def test_sync(self): + client = self.sync_client + + response = client.admin_users_session_list() + self.assertIsNotNone(response["active_sessions"]) + + @async_test + async def test_async(self): + client = self.async_client + + response = await client.admin_users_session_list() + self.assertIsNotNone(response["active_sessions"]) diff --git a/integration_tests/web/test_admin_users_session.py b/integration_tests/web/test_admin_users_session.py new file mode 100644 index 000000000..6dfa285c2 --- /dev/null +++ b/integration_tests/web/test_admin_users_session.py @@ -0,0 +1,43 @@ +import os +import unittest + +from integration_tests.env_variable_names import ( + SLACK_SDK_TEST_GRID_WORKSPACE_ADMIN_USER_TOKEN, + SLACK_SDK_TEST_GRID_ORG_ADMIN_USER_TOKEN, +) +from slack_sdk.web import WebClient + + +class TestWebClient(unittest.TestCase): + def setUp(self): + self.org_admin_token = os.environ[SLACK_SDK_TEST_GRID_ORG_ADMIN_USER_TOKEN] + self.client: WebClient = WebClient(token=self.org_admin_token) + + if not hasattr(self, "user_ids"): + team_admin_token = os.environ[SLACK_SDK_TEST_GRID_WORKSPACE_ADMIN_USER_TOKEN] + client = WebClient(token=team_admin_token) + users = client.users_list(exclude_archived=True, limit=50) + self.user_ids = [ + u["id"] + for u in users["members"] + if not u["is_bot"] + and not u["deleted"] + and not u["is_app_user"] + and not u["is_owner"] + and not u.get("is_stranger") + ][:3] + + def tearDown(self): + pass + + def test_reset(self): + response = self.client.admin_users_session_reset(user_id=self.user_ids[0]) + self.assertIsNone(response.get("error")) + + def test_resetBulk(self): + response = self.client.admin_users_session_resetBulk(user_ids=self.user_ids) + self.assertIsNone(response.get("error")) + + def test_resetBulk_str(self): + response = self.client.admin_users_session_resetBulk(user_ids=",".join(self.user_ids)) + self.assertIsNone(response.get("error")) diff --git a/integration_tests/web/test_admin_users_session_settings.py b/integration_tests/web/test_admin_users_session_settings.py new file mode 100644 index 000000000..0054ef3be --- /dev/null +++ b/integration_tests/web/test_admin_users_session_settings.py @@ -0,0 +1,60 @@ +import logging +import os +import unittest + +from integration_tests.env_variable_names import ( + SLACK_SDK_TEST_GRID_WORKSPACE_ADMIN_USER_TOKEN, + SLACK_SDK_TEST_GRID_ORG_ADMIN_USER_TOKEN, + SLACK_SDK_TEST_GRID_IDP_USERGROUP_ID, + SLACK_SDK_TEST_GRID_TEAM_ID, +) +from integration_tests.helpers import async_test +from slack_sdk.web import WebClient +from slack_sdk.web.async_client import AsyncWebClient + + +class TestWebClient(unittest.TestCase): + """Runs integration tests with real Slack API""" + + def setUp(self): + self.logger = logging.getLogger(__name__) + self.org_admin_token = os.environ[SLACK_SDK_TEST_GRID_ORG_ADMIN_USER_TOKEN] + self.sync_client: WebClient = WebClient(token=self.org_admin_token) + self.async_client: AsyncWebClient = AsyncWebClient(token=self.org_admin_token) + + self.team_id = os.environ[SLACK_SDK_TEST_GRID_TEAM_ID] + self.idp_usergroup_id = os.environ[SLACK_SDK_TEST_GRID_IDP_USERGROUP_ID] + + if not hasattr(self, "user_ids"): + team_admin_token = os.environ[SLACK_SDK_TEST_GRID_WORKSPACE_ADMIN_USER_TOKEN] + client = WebClient(token=team_admin_token) + users = client.users_list(exclude_archived=True, limit=50) + self.user_ids = [ + u["id"] + for u in users["members"] + if not u["is_bot"] + and not u["deleted"] + and not u["is_app_user"] + and not u["is_owner"] + and not u.get("is_stranger") + ][:3] + + def tearDown(self): + pass + + def test_sync(self): + client = self.sync_client + + response = client.admin_users_session_getSettings(user_ids=self.user_ids) + self.assertIsNotNone(response["session_settings"]) + client.admin_users_session_setSettings(user_ids=self.user_ids, duration=60 * 60 * 24 * 30) + client.admin_users_session_clearSettings(user_ids=self.user_ids) + + @async_test + async def test_async(self): + client = self.async_client + + response = await client.admin_users_session_getSettings(user_ids=self.user_ids) + self.assertIsNotNone(response["session_settings"]) + await client.admin_users_session_setSettings(user_ids=self.user_ids, duration=60 * 60 * 24 * 30) + await client.admin_users_session_clearSettings(user_ids=self.user_ids) diff --git a/integration_tests/web/test_admin_users_unsupportedVersions_export.py b/integration_tests/web/test_admin_users_unsupportedVersions_export.py new file mode 100644 index 000000000..2282ce427 --- /dev/null +++ b/integration_tests/web/test_admin_users_unsupportedVersions_export.py @@ -0,0 +1,35 @@ +import os +import unittest +import time + +from integration_tests.env_variable_names import ( + SLACK_SDK_TEST_GRID_ORG_ADMIN_USER_TOKEN, +) +from slack_sdk.web import WebClient + + +class TestWebClient(unittest.TestCase): + def setUp(self): + self.org_admin_token = os.environ[SLACK_SDK_TEST_GRID_ORG_ADMIN_USER_TOKEN] + self.client: WebClient = WebClient(token=self.org_admin_token) + + def tearDown(self): + pass + + def test_no_args(self): + response = self.client.admin_users_unsupportedVersions_export() + self.assertIsNone(response.get("error")) + + def test_full_args(self): + response = self.client.admin_users_unsupportedVersions_export( + date_end_of_support=int(round(time.time())) + 60 * 60 * 24 * 120, + date_sessions_started=0, + ) + self.assertIsNone(response.get("error")) + + def test_full_args_str(self): + response = self.client.admin_users_unsupportedVersions_export( + date_end_of_support=str(int(round(time.time())) + 60 * 60 * 24 * 120), + date_sessions_started="0", + ) + self.assertIsNone(response.get("error")) diff --git a/integration_tests/web/test_app_manifest.py b/integration_tests/web/test_app_manifest.py new file mode 100644 index 000000000..10e2ec4f0 --- /dev/null +++ b/integration_tests/web/test_app_manifest.py @@ -0,0 +1,160 @@ +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/integration_tests/web/test_async_web_client.py b/integration_tests/web/test_async_web_client.py new file mode 100644 index 000000000..6785c59c9 --- /dev/null +++ b/integration_tests/web/test_async_web_client.py @@ -0,0 +1,176 @@ +import logging +import os +import unittest + +from integration_tests.env_variable_names import ( + SLACK_SDK_TEST_BOT_TOKEN, + SLACK_SDK_TEST_WEB_TEST_CHANNEL_ID, +) +from integration_tests.helpers import async_test +from slack_sdk.web.async_client import AsyncWebClient +from slack_sdk.web.async_base_client import AsyncSlackResponse + + +class TestAsyncWebClient(unittest.TestCase): + """Runs integration tests with real Slack API""" + + def setUp(self): + if not hasattr(self, "logger"): + self.logger = logging.getLogger(__name__) + self.bot_token = os.environ[SLACK_SDK_TEST_BOT_TOKEN] + self.async_client: AsyncWebClient = AsyncWebClient(token=self.bot_token) + self.channel_id = os.environ[SLACK_SDK_TEST_WEB_TEST_CHANNEL_ID] + + def tearDown(self): + pass + + # ------------------------- + # api.test + + @async_test + async def test_api_test_async(self): + response: AsyncSlackResponse = await self.async_client.api_test(foo="bar") + self.assertEqual(response["args"]["foo"], "bar") + + # ------------------------- + # auth.test + + @async_test + async def test_auth_test_async(self): + response: AsyncSlackResponse = await self.async_client.auth_test() + self.verify_auth_test_response(response) + + def verify_auth_test_response(self, response): + self.assertIsNotNone(response["url"]) + self.assertIsNotNone(response["user"]) + self.assertIsNotNone(response["user_id"]) + self.assertIsNotNone(response["team"]) + self.assertIsNotNone(response["team_id"]) + self.assertIsNotNone(response["bot_id"]) + + # ------------------------- + # basic metadata retrieval + + @async_test + async def test_metadata_retrieval_async(self): + client = self.async_client + auth = await client.auth_test() + self.assertIsNotNone(auth) + bot = await client.bots_info(bot=auth["bot_id"]) + self.assertIsNotNone(bot) + + # ------------------------- + # basic chat operations + + @async_test + async def test_basic_chat_operations_async(self): + client = self.async_client + + auth = await client.auth_test() + self.assertIsNotNone(auth) + url = auth["url"] + + channel = self.channel_id + message = ( + "This message was posted by ! " + + "(integration_tests/test_web_client.py #test_chat_operations)" + ) + new_message: AsyncSlackResponse = await client.chat_postMessage(channel=channel, text=message) + self.assertEqual(new_message["message"]["text"], message) + ts = new_message["ts"] + + permalink = await client.chat_getPermalink(channel=channel, message_ts=ts) + self.assertIsNotNone(permalink) + self.assertRegex( + permalink["permalink"], + f"{url}archives/{channel}/.+", + ) + + new_reaction = await client.reactions_add(channel=channel, timestamp=ts, name="eyes") + self.assertIsNotNone(new_reaction) + + reactions = await client.reactions_get(channel=channel, timestamp=ts) + self.assertIsNotNone(reactions) + + reaction_removal = await client.reactions_remove(channel=channel, timestamp=ts, name="eyes") + self.assertIsNotNone(reaction_removal) + + thread_reply = await client.chat_postMessage(channel=channel, thread_ts=ts, text="threading...") + self.assertIsNotNone(thread_reply) + + modification = await client.chat_update(channel=channel, ts=ts, text="Is this intentional?") + self.assertIsNotNone(modification) + + reply_deletion = await client.chat_delete(channel=channel, ts=thread_reply["ts"]) + self.assertIsNotNone(reply_deletion) + message_deletion = await client.chat_delete(channel=channel, ts=ts) + self.assertIsNotNone(message_deletion) + + # ------------------------- + # file operations + + @async_test + async def test_uploading_text_files_async(self): + client = self.async_client + file, filename = __file__, os.path.basename(__file__) + upload = await client.files_upload( + channels=self.channel_id, + title="Good Old Slack Logo", + filename=filename, + file=file, + ) + self.assertIsNotNone(upload) + + deletion = await client.files_delete(file=upload["file"]["id"]) + self.assertIsNotNone(deletion) + + @async_test + async def test_uploading_binary_files_async(self): + client = self.async_client + current_dir = os.path.dirname(__file__) + file = f"{current_dir}/../../tests/data/slack_logo.png" + upload = await client.files_upload( + channels=self.channel_id, + title="Good Old Slack Logo", + filename="slack_logo.png", + file=file, + ) + self.assertIsNotNone(upload) + + deletion = await client.files_delete(file=upload["file"]["id"]) + self.assertIsNotNone(deletion) + + @async_test + async def test_uploading_file_with_token_param_async(self): + client = AsyncWebClient() + current_dir = os.path.dirname(__file__) + file = f"{current_dir}/../../tests/data/slack_logo.png" + upload = await client.files_upload( + token=self.bot_token, + channels=self.channel_id, + title="Good Old Slack Logo", + filename="slack_logo.png", + file=file, + ) + self.assertIsNotNone(upload) + + deletion = await client.files_delete( + token=self.bot_token, + file=upload["file"]["id"], + ) + self.assertIsNotNone(deletion) + + # ------------------------- + # pagination + + @async_test + async def test_pagination_with_iterator_async(self): + client = self.async_client + fetched_count = 0 + # AsyncSlackResponse is an iterator that fetches next if next_cursor is not "" + async for response in await client.conversations_list(limit=1, exclude_archived=1, types="public_channel"): + fetched_count += len(response["channels"]) + if fetched_count > 1: + break + + self.assertGreater(fetched_count, 1) diff --git a/integration_tests/web/test_bookmarks.py b/integration_tests/web/test_bookmarks.py new file mode 100644 index 000000000..8216055ad --- /dev/null +++ b/integration_tests/web/test_bookmarks.py @@ -0,0 +1,63 @@ +import logging +import os +import unittest + +from integration_tests.env_variable_names import ( + SLACK_SDK_TEST_BOT_TOKEN, + SLACK_SDK_TEST_WEB_TEST_CHANNEL_ID, +) +from slack_sdk.web import WebClient +from slack_sdk.web.async_client import AsyncWebClient + + +class TestBookmarks(unittest.TestCase): + """Runs integration tests with real Slack API testing the bookmarks.* APIs""" + + def setUp(self): + if not hasattr(self, "logger"): + self.logger = logging.getLogger(__name__) + self.bot_token = os.environ[SLACK_SDK_TEST_BOT_TOKEN] + self.async_client: AsyncWebClient = AsyncWebClient(token=self.bot_token) + self.sync_client: WebClient = WebClient(token=self.bot_token) + self.channel_id = os.environ[SLACK_SDK_TEST_WEB_TEST_CHANNEL_ID] + + def tearDown(self): + pass + + def test_adding_listing_editing_removing_bookmark(self): + client = self.sync_client + # create a new bookmark + bookmark = client.bookmarks_add( + channel_id=self.channel_id, + title="slack!", + type="link", + link="https://slack.com", + ) + self.assertIsNotNone(bookmark) + bookmark_id = bookmark["bookmark"]["id"] + # make sure we find the bookmark we just added + all_bookmarks = client.bookmarks_list(channel_id=self.channel_id) + self.assertIsNotNone(all_bookmarks) + self.assertIsNotNone(next((b for b in all_bookmarks["bookmarks"] if b["id"] == bookmark_id), None)) + # edit the bookmark + bookmark = client.bookmarks_edit( + bookmark_id=bookmark_id, + channel_id=self.channel_id, + title="slack api!", + type="link", + link="https://api.slack.com", + ) + self.assertIsNotNone(bookmark) + # make sure we find the edited bookmark we just added + all_bookmarks = client.bookmarks_list(channel_id=self.channel_id) + self.assertIsNotNone(all_bookmarks) + edited_bookmark = next((b for b in all_bookmarks["bookmarks"] if b["id"] == bookmark_id), None) + self.assertIsNotNone(edited_bookmark) + self.assertEqual(edited_bookmark["title"], "slack api!") + # remove the bookmark + removed_bookmark = client.bookmarks_remove(bookmark_id=bookmark_id, channel_id=self.channel_id) + self.assertIsNotNone(removed_bookmark) + # make sure we cannot find the bookmark we just removed + all_bookmarks = client.bookmarks_list(channel_id=self.channel_id) + self.assertIsNotNone(all_bookmarks) + self.assertIsNone(next((b for b in all_bookmarks if b["id"] == bookmark_id), None)) diff --git a/integration_tests/web/test_calls.py b/integration_tests/web/test_calls.py new file mode 100644 index 000000000..d285c03f0 --- /dev/null +++ b/integration_tests/web/test_calls.py @@ -0,0 +1,149 @@ +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_sdk.web import WebClient +from slack_sdk.models.blocks import CallBlock +from slack_sdk.web.async_client import AsyncWebClient + + +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: AsyncWebClient = AsyncWebClient(token=self.bot_token) + + 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) + + participants_removal = client.calls_participants_remove( + 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(participants_removal) + + 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) diff --git a/integration_tests/web/test_canvases.py b/integration_tests/web/test_canvases.py new file mode 100644 index 000000000..75e970f0f --- /dev/null +++ b/integration_tests/web/test_canvases.py @@ -0,0 +1,165 @@ +import logging +import os +import time +import unittest + +from integration_tests.env_variable_names import SLACK_SDK_TEST_BOT_TOKEN +from integration_tests.helpers import async_test +from slack_sdk.web import WebClient +from slack_sdk.web.async_client import AsyncWebClient + + +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: AsyncWebClient = AsyncWebClient(token=self.bot_token) + + def tearDown(self): + pass + + def test_sync(self): + client = self.sync_client + + # Channel canvas + new_channel = client.conversations_create(name=f"test-{str(time.time()).replace('.', '-')}") + channel_id = new_channel["channel"]["id"] + channel_canvas = client.conversations_canvases_create( + channel_id=channel_id, + document_content={ + "type": "markdown", + "markdown": """# My canvas + +## Hey + +What's up? + +- foo +- bar +""", + }, + ) + self.assertIsNone(channel_canvas.get("error")) + + # Standalone canvas + standalone_canvas = client.canvases_create( + title="My canvas", + document_content={ + "type": "markdown", + "markdown": """# My canvas + +## Hey + +What's up? + +- foo +- bar +""", + }, + ) + self.assertIsNone(standalone_canvas.get("error")) + canvas_id = standalone_canvas.get("canvas_id") + + sections = client.canvases_sections_lookup(canvas_id=canvas_id, criteria={"contains_text": "Hey"}) + section_id = sections["sections"][0]["id"] + + edit = client.canvases_edit( + canvas_id=canvas_id, + changes=[ + { + "operation": "replace", + "section_id": section_id, + "document_content": {"type": "markdown", "markdown": "## Hey Hey"}, + } + ], + ) + self.assertIsNone(edit.get("error")) + + user_id = client.auth_test()["user_id"] + access_set = client.canvases_access_set( + canvas_id=canvas_id, + access_level="write", + user_ids=[user_id], + ) + self.assertIsNone(access_set.get("error")) + + access_delete = client.canvases_access_delete(canvas_id=canvas_id, user_ids=[user_id]) + self.assertIsNone(access_delete.get("error")) + + delete = client.canvases_delete(canvas_id=canvas_id) + self.assertIsNone(delete.get("error")) + + @async_test + async def test_async(self): + client = self.async_client + + # Channel canvas + new_channel = await client.conversations_create(name=f"test-{str(time.time()).replace('.', '-')}") + channel_id = new_channel["channel"]["id"] + channel_canvas = await client.conversations_canvases_create( + channel_id=channel_id, + document_content={ + "type": "markdown", + "markdown": """# My canvas + +## Hey + +What's up? + +- foo +- bar +""", + }, + ) + self.assertIsNone(channel_canvas.get("error")) + + # Standalone canvas + standalone_canvas = await client.canvases_create( + title="My canvas", + document_content={ + "type": "markdown", + "markdown": """# My canvas + +## Hey + +What's up? + +- foo +- bar +""", + }, + ) + self.assertIsNone(standalone_canvas.get("error")) + canvas_id = standalone_canvas.get("canvas_id") + + sections = await client.canvases_sections_lookup(canvas_id=canvas_id, criteria={"contains_text": "Hey"}) + section_id = sections["sections"][0]["id"] + + edit = await client.canvases_edit( + canvas_id=canvas_id, + changes=[ + { + "operation": "replace", + "section_id": section_id, + "document_content": {"type": "markdown", "markdown": "## Hey Hey"}, + } + ], + ) + self.assertIsNone(edit.get("error")) + + user_id = (await client.auth_test())["user_id"] + access_set = await client.canvases_access_set( + canvas_id=canvas_id, + access_level="write", + user_ids=[user_id], + ) + self.assertIsNone(access_set.get("error")) + + access_delete = await client.canvases_access_delete(canvas_id=canvas_id, user_ids=[user_id]) + self.assertIsNone(access_delete.get("error")) + + delete = await client.canvases_delete(canvas_id=canvas_id) + self.assertIsNone(delete.get("error")) diff --git a/integration_tests/web/test_conversations_connect.py b/integration_tests/web/test_conversations_connect.py new file mode 100644 index 000000000..1d988946c --- /dev/null +++ b/integration_tests/web/test_conversations_connect.py @@ -0,0 +1,162 @@ +import logging +import os +import time +from typing import Optional +import unittest + +from slack_sdk.web.slack_response import SlackResponse +from slack_sdk.errors import SlackApiError +from integration_tests.env_variable_names import ( + SLACK_SDK_TEST_CONNECT_INVITE_SENDER_BOT_TOKEN, + SLACK_SDK_TEST_CONNECT_INVITE_RECEIVER_BOT_TOKEN, + SLACK_SDK_TEST_CONNECT_INVITE_RECEIVER_BOT_USER_ID, +) +from integration_tests.helpers import async_test +from slack_sdk.web import WebClient +from slack_sdk.web.async_client import AsyncWebClient + + +class TestWebClient(unittest.TestCase): + """Runs integration tests with Slack API for conversations.* endpoints + To run, we use two workspace-level bot tokens, + one for the inviting workspace(list and send invites) another for the recipient + workspace (accept and approve) sent invites. Before being able to run this test suite, + we also need to have manually created a slack connect shared channel and added + these two bots as members first. See: https://docs.slack.dev/apis/slack-connect/ + + In addition to conversations.connect:* scopes, your sender bot token should have channels:manage scopes. + """ + + def setUp(self): + self.logger = logging.getLogger(__name__) + self.sender_bot_token = os.environ[SLACK_SDK_TEST_CONNECT_INVITE_SENDER_BOT_TOKEN] + self.receiver_bot_token = os.environ[SLACK_SDK_TEST_CONNECT_INVITE_RECEIVER_BOT_TOKEN] + self.sender_sync_client: WebClient = WebClient(token=self.sender_bot_token) + self.sender_async_client: AsyncWebClient = AsyncWebClient(token=self.sender_bot_token) + self.receiver_sync_client: WebClient = WebClient(token=self.receiver_bot_token) + self.receiver_async_client: AsyncWebClient = AsyncWebClient(token=self.receiver_bot_token) + + def tearDown(self): + pass + + def test_sync(self): + sender = self.sender_sync_client + receiver = self.receiver_sync_client + channel_id: Optional[str] = None + + try: + auth_test: SlackResponse = receiver.auth_test() + self.assertIsNotNone(auth_test["team_id"]) + connect_team_id = auth_test["team_id"] + + # list senders pending connect invites + connect_invites: SlackResponse = sender.conversations_listConnectInvites() + self.assertIsNotNone(connect_invites["invites"]) + + # creates channel in sender workspace to share + unique_channel_name = str(int(time.time())) + "-shared" + new_channel: SlackResponse = sender.conversations_create(name=unique_channel_name) + self.assertIsNotNone(new_channel["channel"]) + self.assertIsNotNone(new_channel["channel"]["id"]) + channel_id = new_channel["channel"]["id"] + + # send an invite for sender's intended shared channel to receiver's bot user id + invite: SlackResponse = sender.conversations_inviteShared( + channel=new_channel["channel"]["id"], + user_ids=os.environ[SLACK_SDK_TEST_CONNECT_INVITE_RECEIVER_BOT_USER_ID], + ) + self.assertIsNotNone(invite["invite_id"]) + + # receiver accept conversations invite via invite id + accepted: SlackResponse = receiver.conversations_acceptSharedInvite( + channel_name=unique_channel_name, + invite_id=invite["invite_id"], + ) + self.assertIsNone(accepted["error"]) + + # receiver attempt to approve invite already accepted by an admin level token should fail + self.assertRaises( + SlackApiError, + receiver.conversations_approveSharedInvite, + invite_id=invite["invite_id"], + ) + + sender_approval = sender.conversations_approveSharedInvite( + invite_id=invite["invite_id"], team_id=connect_team_id + ) + self.assertIsNone(sender_approval["error"]) + + downgrade = sender.conversations_externalInvitePermissions_set( + channel=channel_id, target_team=connect_team_id, action="downgrade" + ) + self.assertIsNone(downgrade["error"]) + + upgrade = sender.conversations_externalInvitePermissions_set( + channel=channel_id, target_team=connect_team_id, action="upgrade" + ) + self.assertIsNone(upgrade["error"]) + finally: + if channel_id is not None: + # clean up created channel + delete_channel: SlackResponse = sender.conversations_archive(channel=new_channel["channel"]["id"]) + self.assertIsNotNone(delete_channel) + + @async_test + async def test_async(self): + sender = self.sender_async_client + receiver = self.receiver_async_client + channel_id: Optional[str] = None + + try: + auth_test: SlackResponse = await receiver.auth_test() + self.assertIsNotNone(auth_test["team_id"]) + connect_team_id = auth_test["team_id"] + + # list senders pending connect invites + connect_invites: SlackResponse = await sender.conversations_listConnectInvites() + self.assertIsNotNone(connect_invites["invites"]) + + # creates channel in sender workspace to share + unique_channel_name = str(int(time.time())) + "-shared" + new_channel: SlackResponse = await sender.conversations_create(name=unique_channel_name) + self.assertIsNotNone(new_channel["channel"]) + self.assertIsNotNone(new_channel["channel"]["id"]) + channel_id = new_channel["channel"]["id"] + + # send an invite for sender's intended shared channel to receiver's bot user id + invite: SlackResponse = await sender.conversations_inviteShared( + channel=new_channel["channel"]["id"], + user_ids=os.environ[SLACK_SDK_TEST_CONNECT_INVITE_RECEIVER_BOT_USER_ID], + ) + self.assertIsNotNone(invite["invite_id"]) + + # receiver accept conversations invite via invite id + accepted: SlackResponse = await receiver.conversations_acceptSharedInvite( + channel_name=unique_channel_name, + invite_id=invite["invite_id"], + ) + self.assertIsNone(accepted["error"]) + + # receiver attempt to approve invite already accepted by an admin level token should fail + with self.assertRaises(SlackApiError): + await receiver.conversations_approveSharedInvite(invite_id=invite["invite_id"]) + + sender_approval = await sender.conversations_approveSharedInvite( + invite_id=invite["invite_id"], team_id=connect_team_id + ) + self.assertIsNone(sender_approval["error"]) + + downgrade = await sender.conversations_externalInvitePermissions_set( + channel=channel_id, target_team=connect_team_id, action="downgrade" + ) + self.assertIsNone(downgrade["error"]) + + upgrade = await sender.conversations_externalInvitePermissions_set( + channel=channel_id, target_team=connect_team_id, action="upgrade" + ) + self.assertIsNone(upgrade["error"]) + finally: + if channel_id is not None: + # clean up created channel + delete_channel: SlackResponse = await sender.conversations_archive(channel=new_channel["channel"]["id"]) + self.assertIsNotNone(delete_channel) diff --git a/integration_tests/web/test_files_upload_v2.py b/integration_tests/web/test_files_upload_v2.py new file mode 100644 index 000000000..4100051b4 --- /dev/null +++ b/integration_tests/web/test_files_upload_v2.py @@ -0,0 +1,224 @@ +import logging +import os +from pathlib import Path +import unittest +from io import BytesIO + +import pytest + +from integration_tests.env_variable_names import ( + SLACK_SDK_TEST_BOT_TOKEN, + SLACK_SDK_TEST_WEB_TEST_CHANNEL_ID, +) +from integration_tests.helpers import async_test +from slack_sdk.errors import SlackRequestError +from slack_sdk.web import WebClient +from slack_sdk.web.async_client import AsyncWebClient +from slack_sdk.web.legacy_client import LegacyWebClient + + +class TestWebClient_FilesUploads_V2(unittest.TestCase): + """Runs integration tests with real Slack API""" + + def setUp(self): + if not hasattr(self, "logger"): + self.logger = logging.getLogger(__name__) + self.bot_token = os.environ[SLACK_SDK_TEST_BOT_TOKEN] + self.async_client: AsyncWebClient = AsyncWebClient(token=self.bot_token) + self.sync_client: WebClient = WebClient(token=self.bot_token) + self.legacy_client: WebClient = LegacyWebClient(token=self.bot_token) + self.legacy_client_async: WebClient = LegacyWebClient(token=self.bot_token, run_async=True) + self.channel_id = os.environ[SLACK_SDK_TEST_WEB_TEST_CHANNEL_ID] + + def tearDown(self): + pass + + # ------------------------- + # file operations + + def test_uploading_text_files(self): + client = self.sync_client + file = __file__ + upload = client.files_upload_v2( + channels=self.channel_id, + file=file, + title="Test code", + ) + self.assertIsNotNone(upload) + self.assertIsNotNone(upload.get("files")[0].get("id")) + self.assertIsNotNone(upload.get("files")[0].get("title")) + + def test_uploading_bytes_io(self): + client = self.sync_client + upload = client.files_upload_v2( + channels=self.channel_id, + file=BytesIO(bytearray("This is a test!", "utf-8")), + filename="test.txt", + title="Test code", + ) + self.assertIsNotNone(upload) + self.assertIsNotNone(upload.get("files")[0].get("id")) + self.assertIsNotNone(upload.get("files")[0].get("title")) + + def test_uploading_text_files_path(self): + client = self.sync_client + file = __file__ + upload = client.files_upload_v2( + channel=self.channel_id, + file=Path(file), + title="Test code", + ) + self.assertIsNotNone(upload) + self.assertIsNotNone(upload.get("files")[0].get("id")) + self.assertIsNotNone(upload.get("files")[0].get("title")) + + def test_uploading_multiple_files(self): + client = self.sync_client + file = __file__ + upload = client.files_upload_v2( + file_uploads=[ + { + "file": file, + "title": "Test code", + }, + { + "content": "Hi there!", + "title": "Text data", + "filename": "hi-there.txt", + }, + ], + channel=self.channel_id, + initial_comment="Here are files :wave:", + ) + self.assertIsNotNone(upload) + self.assertIsNotNone(upload.get("files")[0].get("id")) + self.assertIsNotNone(upload.get("files")[0].get("title")) + + @async_test + async def test_uploading_text_files_async(self): + client = self.async_client + file, filename = __file__, os.path.basename(__file__) + upload = await client.files_upload_v2( + channels=self.channel_id, + title="Good Old Slack Logo", + filename=filename, + file=file, + ) + self.assertIsNotNone(upload) + self.assertIsNotNone(upload.get("files")[0].get("id")) + self.assertIsNotNone(upload.get("files")[0].get("title")) + + deletion = await client.files_delete(file=upload["file"]["id"]) + self.assertIsNotNone(deletion) + + @async_test + async def test_uploading_text_files_legacy_async(self): + client = self.legacy_client_async + file, filename = __file__, os.path.basename(__file__) + try: + await client.files_upload_v2( + channels=self.channel_id, + title="Good Old Slack Logo", + filename=filename, + file=file, + ) + pytest.fail("Raising SlackRequestError is expected here") + except SlackRequestError: + pass + + def test_uploading_binary_files(self): + client = self.sync_client + current_dir = os.path.dirname(__file__) + file = f"{current_dir}/../../tests/data/slack_logo.png" + upload = client.files_upload_v2( + channels=self.channel_id, + title="Good Old Slack Logo", + filename="slack_logo.png", + file=file, + ) + self.assertIsNotNone(upload) + self.assertIsNotNone(upload.get("files")[0].get("id")) + self.assertIsNotNone(upload.get("files")[0].get("title")) + + deletion = client.files_delete(file=upload["file"]["id"]) + self.assertIsNotNone(deletion) + + def test_uploading_binary_files_as_content(self): + client = self.sync_client + current_dir = os.path.dirname(__file__) + file = f"{current_dir}/../../tests/data/slack_logo.png" + with open(file, "rb") as f: + content = f.read() + upload = client.files_upload_v2( + channels=self.channel_id, + title="Good Old Slack Logo", + filename="slack_logo.png", + content=content, + ) + self.assertIsNotNone(upload) + self.assertIsNotNone(upload.get("files")[0].get("id")) + self.assertIsNotNone(upload.get("files")[0].get("title")) + + deletion = client.files_delete(file=upload["file"]["id"]) + self.assertIsNotNone(deletion) + + @async_test + async def test_uploading_binary_files_async(self): + client = self.async_client + current_dir = os.path.dirname(__file__) + file = f"{current_dir}/../../tests/data/slack_logo.png" + upload = await client.files_upload_v2( + channels=self.channel_id, + title="Good Old Slack Logo", + filename="slack_logo.png", + file=file, + ) + self.assertIsNotNone(upload) + self.assertIsNotNone(upload.get("files")[0].get("id")) + self.assertIsNotNone(upload.get("files")[0].get("title")) + + deletion = await client.files_delete(file=upload["file"]["id"]) + self.assertIsNotNone(deletion) + + def test_uploading_file_with_token_param(self): + client = WebClient() + current_dir = os.path.dirname(__file__) + file = f"{current_dir}/../../tests/data/slack_logo.png" + upload = client.files_upload_v2( + token=self.bot_token, + channels=self.channel_id, + title="Good Old Slack Logo", + filename="slack_logo.png", + file=file, + ) + self.assertIsNotNone(upload) + self.assertIsNotNone(upload.get("files")[0].get("id")) + self.assertIsNotNone(upload.get("files")[0].get("title")) + + deletion = client.files_delete( + token=self.bot_token, + file=upload["file"]["id"], + ) + self.assertIsNotNone(deletion) + + @async_test + async def test_uploading_file_with_token_param_async(self): + client = AsyncWebClient() + current_dir = os.path.dirname(__file__) + file = f"{current_dir}/../../tests/data/slack_logo.png" + upload = await client.files_upload_v2( + token=self.bot_token, + channels=self.channel_id, + title="Good Old Slack Logo", + filename="slack_logo.png", + file=file, + ) + self.assertIsNotNone(upload) + self.assertIsNotNone(upload.get("files")[0].get("id")) + self.assertIsNotNone(upload.get("files")[0].get("title")) + + deletion = await client.files_delete( + token=self.bot_token, + file=upload["file"]["id"], + ) + self.assertIsNotNone(deletion) diff --git a/integration_tests/web/test_issue_1053.py b/integration_tests/web/test_issue_1053.py new file mode 100644 index 000000000..607920efb --- /dev/null +++ b/integration_tests/web/test_issue_1053.py @@ -0,0 +1,46 @@ +import logging +import os +import time +import unittest + +from integration_tests.env_variable_names import SLACK_SDK_TEST_BOT_TOKEN +from slack_sdk.web import WebClient + + +class TestWebClient(unittest.TestCase): + """Runs integration tests with real Slack API + + https://github.com/slackapi/python-slack-sdk/issues/1053 + """ + + def setUp(self): + self.logger = logging.getLogger(__name__) + self.bot_token = os.environ[SLACK_SDK_TEST_BOT_TOKEN] + + def tearDown(self): + pass + + def test_issue_1053(self): + client: WebClient = WebClient(token=self.bot_token) + self_user_id = client.auth_test()["user_id"] + channel_name = f"test-channel-{str(time.time()).replace('.', '-')}" + channel_id = None + try: + creation = client.conversations_create(name=channel_name) + self.assertIsNone(creation.get("error")) + channel_id = creation["channel"]["id"] + user_ids = [ + u["id"] + for u in client.users_list(limit=100)["members"] + if u["id"] not in {"USLACKBOT", self_user_id} + and u.get("is_bot", False) is False + and u.get("is_app_user", False) is False + and u.get("is_restricted", False) is False + and u.get("is_ultra_restricted", False) is False + and u.get("is_email_confirmed", False) is True + ] + invitations = client.conversations_invite(channel=channel_id, users=user_ids) + self.assertIsNone(invitations.get("error")) + finally: + if channel_id is not None: + client.conversations_archive(channel=channel_id) diff --git a/integration_tests/web/test_issue_1143.py b/integration_tests/web/test_issue_1143.py new file mode 100644 index 000000000..850651439 --- /dev/null +++ b/integration_tests/web/test_issue_1143.py @@ -0,0 +1,41 @@ +import os +import unittest + +from integration_tests.env_variable_names import SLACK_SDK_TEST_BOT_TOKEN +from integration_tests.helpers import async_test +from slack_sdk.errors import SlackApiError +from slack_sdk.web import WebClient +from slack_sdk.web.async_client import AsyncWebClient + + +class TestWebClient(unittest.TestCase): + """Runs integration tests with real Slack API + + export SLACK_SDK_TEST_BOT_TOKEN=xoxb-xxx + ./scripts/run_integration_tests.sh integration_tests/web/test_issue_1143.py + + https://github.com/slackapi/python-slack-sdk/issues/1143 + """ + + def setUp(self): + self.bot_token = os.environ[SLACK_SDK_TEST_BOT_TOKEN] + + def tearDown(self): + pass + + def test_backward_compatible_header(self): + client: WebClient = WebClient(token=self.bot_token) + try: + while True: + client.users_list() + except SlackApiError as e: + self.assertIsNotNone(e.response.headers["Retry-After"]) + + @async_test + async def test_backward_compatible_header_async(self): + client: AsyncWebClient = AsyncWebClient(token=self.bot_token) + try: + while True: + await client.users_list() + except SlackApiError as e: + self.assertIsNotNone(e.response.headers["Retry-After"]) diff --git a/integration_tests/web/test_issue_1305.py b/integration_tests/web/test_issue_1305.py new file mode 100644 index 000000000..af002879f --- /dev/null +++ b/integration_tests/web/test_issue_1305.py @@ -0,0 +1,49 @@ +import asyncio +import os +import time +import unittest + +from integration_tests.env_variable_names import SLACK_SDK_TEST_GRID_ORG_ADMIN_USER_TOKEN +from integration_tests.helpers import async_test +from slack_sdk.web import WebClient +from slack_sdk.web.async_client import AsyncWebClient + + +class TestWebClient(unittest.TestCase): + """Runs integration tests with real Slack API + + https://github.com/slackapi/python-slack-sdk/issues/1305 + """ + + def setUp(self): + self.org_admin_token = os.environ[SLACK_SDK_TEST_GRID_ORG_ADMIN_USER_TOKEN] + self.sync_client: WebClient = WebClient(token=self.org_admin_token) + self.async_client: AsyncWebClient = AsyncWebClient(token=self.org_admin_token) + + def tearDown(self): + pass + + def test_sync(self): + client = self.sync_client + count = 0 + + for page in client.admin_conversations_search(limit=1): + count += len(page["conversations"]) + if count > 1: + break + time.sleep(1) + + self.assertGreater(count, 0) + + @async_test + async def test_async(self): + client = self.async_client + count = 0 + + async for page in await client.admin_conversations_search(limit=1): + count += len(page["conversations"]) + if count > 1: + break + await asyncio.sleep(1) + + self.assertGreater(count, 0) diff --git a/integration_tests/web/test_issue_378.py b/integration_tests/web/test_issue_378.py new file mode 100644 index 000000000..59b09a49d --- /dev/null +++ b/integration_tests/web/test_issue_378.py @@ -0,0 +1,36 @@ +import asyncio +import logging +import os +import unittest + +from integration_tests.env_variable_names import SLACK_SDK_TEST_USER_TOKEN +from integration_tests.helpers import async_test +from slack_sdk.web import WebClient +from slack_sdk.web.async_client import AsyncWebClient + + +class TestWebClient(unittest.TestCase): + """Runs integration tests with real Slack API + + https://github.com/slackapi/python-slack-sdk/issues/378 + """ + + def setUp(self): + self.logger = logging.getLogger(__name__) + self.user_token = os.environ[SLACK_SDK_TEST_USER_TOKEN] + self.sync_client: WebClient = WebClient(token=self.user_token) + self.async_client: AsyncWebClient = AsyncWebClient(token=self.user_token) + + def tearDown(self): + pass + + def test_issue_378(self): + client = self.sync_client + response = client.users_setPhoto(image="tests/data/slack_logo_new.png") + self.assertIsNotNone(response) + + @async_test + async def test_issue_378_async(self): + client = self.async_client + response = await client.users_setPhoto(image="tests/data/slack_logo_new.png") + self.assertIsNotNone(response) diff --git a/integration_tests/web/test_issue_480.py b/integration_tests/web/test_issue_480.py new file mode 100644 index 000000000..d3b2a95b9 --- /dev/null +++ b/integration_tests/web/test_issue_480.py @@ -0,0 +1,65 @@ +import logging +import multiprocessing +import os +import threading +import unittest + +from integration_tests.env_variable_names import SLACK_SDK_TEST_USER_TOKEN +from integration_tests.helpers import async_test +from slack_sdk.web import WebClient +from slack_sdk.web.async_client import AsyncWebClient + + +class TestWebClient(unittest.TestCase): + """Runs integration tests with real Slack API + https://github.com/slackapi/python-slack-sdk/issues/480 + """ + + def setUp(self): + self.logger = logging.getLogger(__name__) + self.user_token = os.environ[SLACK_SDK_TEST_USER_TOKEN] + self.sync_client: WebClient = WebClient(token=self.user_token) + self.async_client: AsyncWebClient = AsyncWebClient(token=self.user_token) + + def tearDown(self): + pass + + def test_issue_480_processes(self): + client = self.sync_client + before = len(multiprocessing.active_children()) + for idx in range(10): + response = client.api_test() + self.assertIsNotNone(response) + after = len(multiprocessing.active_children()) + self.assertEqual(0, after - before) + + @async_test + async def test_issue_480_processes_async(self): + client = self.async_client + before = len(multiprocessing.active_children()) + for idx in range(10): + response = await client.api_test() + self.assertIsNotNone(response) + after = len(multiprocessing.active_children()) + self.assertEqual(0, after - before) + + # fails with Python 3.6 + def test_issue_480_threads(self): + client = self.sync_client + before = threading.active_count() + for idx in range(10): + response = client.api_test() + self.assertIsNotNone(response) + after = threading.active_count() + self.assertEqual(0, after - before) + + # fails with Python 3.6 + @async_test + async def test_issue_480_threads_async(self): + client = self.async_client + before = threading.active_count() + for idx in range(10): + response = await client.api_test() + self.assertIsNotNone(response) + after = threading.active_count() + self.assertEqual(0, after - before) diff --git a/integration_tests/web/test_issue_560.py b/integration_tests/web/test_issue_560.py new file mode 100644 index 000000000..259ebe9f6 --- /dev/null +++ b/integration_tests/web/test_issue_560.py @@ -0,0 +1,52 @@ +import logging +import os +import unittest + +from integration_tests.env_variable_names import SLACK_SDK_TEST_BOT_TOKEN +from integration_tests.helpers import async_test +from slack_sdk.web import WebClient +from slack_sdk.web.async_client import AsyncWebClient + + +class TestWebClient(unittest.TestCase): + """Runs integration tests with real Slack API + + https://github.com/slackapi/python-slack-sdk/issues/560 + """ + + 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: AsyncWebClient = AsyncWebClient(token=self.bot_token) + + def tearDown(self): + pass + + def test_issue_560_success(self): + client = self.sync_client + response = client.conversations_list(exclude_archived=1) + self.assertIsNotNone(response) + + response = client.conversations_list(exclude_archived="true") + self.assertIsNotNone(response) + + @async_test + async def test_issue_560_success_async(self): + client = self.async_client + response = await client.conversations_list(exclude_archived=1) + self.assertIsNotNone(response) + + response = await client.conversations_list(exclude_archived="true") + self.assertIsNotNone(response) + + def test_issue_560_failure(self): + client = self.sync_client + response = client.conversations_list(exclude_archived=True) + self.assertIsNotNone(response) + + @async_test + async def test_issue_560_failure_async(self): + client = self.async_client + response = await client.conversations_list(exclude_archived=True) + self.assertIsNotNone(response) diff --git a/integration_tests/web/test_issue_594.py b/integration_tests/web/test_issue_594.py new file mode 100644 index 000000000..024d941a3 --- /dev/null +++ b/integration_tests/web/test_issue_594.py @@ -0,0 +1,106 @@ +import asyncio +import logging +import os +import unittest +from uuid import uuid4 + +from integration_tests.env_variable_names import ( + SLACK_SDK_TEST_BOT_TOKEN, + SLACK_SDK_TEST_WEB_TEST_CHANNEL_ID, + SLACK_SDK_TEST_WEB_TEST_USER_ID, +) +from slack_sdk.web import WebClient +from slack_sdk.web.async_client import AsyncWebClient + + +class TestWebClient(unittest.TestCase): + """Runs integration tests with real Slack API + + https://github.com/slackapi/python-slack-sdk/issues/594 + """ + + 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: AsyncWebClient = AsyncWebClient(token=self.bot_token) + self.channel_id = os.environ[SLACK_SDK_TEST_WEB_TEST_CHANNEL_ID] + self.user_id = os.environ[SLACK_SDK_TEST_WEB_TEST_USER_ID] + + def tearDown(self): + pass + + def test_issue_594(self): + client, logger = self.sync_client, self.logger + external_url = "https://www.example.com/good-old-slack-logo" + external_id = f"test-remote-file-{uuid4()}" + current_dir = os.path.dirname(__file__) + image = f"{current_dir}/../../tests/data/slack_logo.png" + creation = client.files_remote_add( + external_id=external_id, + external_url=external_url, + title="Good Old Slack Logo", + indexable_file_contents="Good Old Slack Logo".encode("utf-8"), + preview_image=image, + ) + self.assertIsNotNone(creation) + + sharing = client.files_remote_share(channels=self.channel_id, external_id=external_id) + self.assertIsNotNone(sharing) + + message = client.chat_postEphemeral( + channel=self.channel_id, + user=self.user_id, + blocks=[ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "This is a mrkdwn section block :ghost: *this is bold*, and ~this is crossed out~, and ", + }, + }, + { + "type": "file", + "external_id": external_id, + "source": "remote", + }, + ], + ) + self.assertIsNotNone(message) + + def test_no_preview_image(self): + client, logger = self.sync_client, self.logger + external_url = "https://www.example.com/what-is-slack" + external_id = f"test-remote-file-{uuid4()}" + creation = client.files_remote_add( + external_id=external_id, + external_url=external_url, + title="Slack (Wikipedia)", + indexable_file_contents="Slack is a proprietary business communication platform developed by Slack Technologies.".encode( + "utf-8" + ), + ) + self.assertIsNotNone(creation) + + sharing = client.files_remote_share(channels=self.channel_id, external_id=external_id) + self.assertIsNotNone(sharing) + + message = client.chat_postEphemeral( + channel=self.channel_id, + user=self.user_id, + blocks=[ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "This is a mrkdwn section block :ghost: *this is bold*, and ~this is crossed out~, and ", + }, + }, + { + "type": "file", + "external_id": external_id, + "source": "remote", + }, + ], + ) + self.assertIsNotNone(message) diff --git a/integration_tests/web/test_issue_654.py b/integration_tests/web/test_issue_654.py new file mode 100644 index 000000000..c7e82454f --- /dev/null +++ b/integration_tests/web/test_issue_654.py @@ -0,0 +1,72 @@ +import asyncio +import io +import logging +import os +import unittest + +from integration_tests.env_variable_names import ( + SLACK_SDK_TEST_BOT_TOKEN, + SLACK_SDK_TEST_WEB_TEST_CHANNEL_ID, +) +from integration_tests.helpers import async_test +from slack_sdk.web import WebClient +from slack_sdk.web.async_client import AsyncWebClient + + +class TestIssue654(unittest.TestCase): + """Runs integration tests with real Slack API + + https://github.com/slackapi/python-slack-sdk/issues/654 + """ + + def setUp(self): + if not hasattr(self, "logger"): + 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: AsyncWebClient = AsyncWebClient(token=self.bot_token) + self.channel_id = os.environ[SLACK_SDK_TEST_WEB_TEST_CHANNEL_ID] + + def tearDown(self): + pass + + def test_issue_654_files_upload(self): + client, logger, channel_ids = ( + self.sync_client, + self.logger, + ",".join([self.channel_id]), + ) + buff = io.BytesIO(b"here is my data but not sure what is wrong.......") + buff.seek(0) + upload = client.files_upload( + file=buff, + filename="output.text", + filetype="text", + channels=channel_ids, + ) + logger.debug("File uploaded - %s", upload) + self.assertIsNotNone(upload) + + deletion = client.files_delete(file=upload["file"]["id"]) + self.assertIsNotNone(deletion) + + @async_test + async def test_issue_654_files_upload_async(self): + client, logger, channel_ids = ( + self.async_client, + self.logger, + ",".join([self.channel_id]), + ) + buff = io.BytesIO(b"here is my data but not sure what is wrong.......") + buff.seek(0) + upload = await client.files_upload( + file=buff, + filename="output.text", + filetype="text", + channels=channel_ids, + ) + logger.debug("File uploaded - %s", upload) + self.assertIsNotNone(upload) + + deletion = await client.files_delete(file=upload["file"]["id"]) + self.assertIsNotNone(deletion) diff --git a/integration_tests/web/test_issue_670.py b/integration_tests/web/test_issue_670.py new file mode 100644 index 000000000..f8be75895 --- /dev/null +++ b/integration_tests/web/test_issue_670.py @@ -0,0 +1,51 @@ +import asyncio +import io +import logging +import os +import unittest + +from integration_tests.env_variable_names import SLACK_SDK_TEST_BOT_TOKEN +from integration_tests.helpers import async_test +from slack_sdk.web import WebClient +from slack_sdk.web.async_client import AsyncWebClient + + +class TestWebClient(unittest.TestCase): + """Runs integration tests with real Slack API + + https://github.com/slackapi/python-slack-sdk/issues/670 + """ + + 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: AsyncWebClient = AsyncWebClient(token=self.bot_token) + + def tearDown(self): + pass + + def test_issue_670(self): + client = self.sync_client + buff = io.BytesIO(b"here is my data but not sure what is wrong.......") + buff.seek(0) + upload = client.files_upload( + file=buff, + filename="output.text", + filetype="text", + title=None, + ) + self.assertIsNotNone(upload) + + @async_test + async def test_issue_670_async(self): + client = self.async_client + buff = io.BytesIO(b"here is my data but not sure what is wrong.......") + buff.seek(0) + upload = await client.files_upload( + file=buff, + filename="output.text", + filetype="text", + title=None, + ) + self.assertIsNotNone(upload) diff --git a/integration_tests/web/test_issue_672.py b/integration_tests/web/test_issue_672.py new file mode 100644 index 000000000..2263c918e --- /dev/null +++ b/integration_tests/web/test_issue_672.py @@ -0,0 +1,110 @@ +import asyncio +import logging +import os +import unittest + +from integration_tests.env_variable_names import ( + SLACK_SDK_TEST_BOT_TOKEN, + SLACK_SDK_TEST_WEB_TEST_CHANNEL_ID, +) +from integration_tests.helpers import async_test +from slack_sdk.web import WebClient +from slack_sdk.web.async_client import AsyncWebClient + + +class TestWebClient(unittest.TestCase): + """Runs integration tests with real Slack API + + https://github.com/slackapi/python-slack-sdk/issues/672 + """ + + def setUp(self): + if not hasattr(self, "logger"): + 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: AsyncWebClient = AsyncWebClient(token=self.bot_token) + self.channel_id = os.environ[SLACK_SDK_TEST_WEB_TEST_CHANNEL_ID] + + def tearDown(self): + pass + + def test_file_loading(self): + client, logger, channel_ids = ( + self.sync_client, + self.logger, + ",".join([self.channel_id]), + ) + upload = client.files_upload( + file="tests/data/日本語.txt", + filename="日本語.txt", + filetype="text", + channels=channel_ids, + ) + self.assertIsNotNone(upload) + self.assertEqual("日本語.txt", upload["file"]["name"]) + self.assertEqual("日本語の文書です。", upload["file"]["preview"]) + + deletion = client.files_delete(file=upload["file"]["id"]) + self.assertIsNotNone(deletion) + + @async_test + async def test_file_loading_async(self): + client, logger, channel_ids = ( + self.async_client, + self.logger, + ",".join([self.channel_id]), + ) + upload = await client.files_upload( + file="tests/data/日本語.txt", + filename="日本語.txt", + filetype="text", + channels=channel_ids, + ) + logger.debug("File uploaded - %s", upload) + self.assertIsNotNone(upload) + self.assertEqual("日本語.txt", upload["file"]["name"]) + self.assertEqual("日本語の文書です。", upload["file"]["preview"]) + + deletion = await client.files_delete(file=upload["file"]["id"]) + self.assertIsNotNone(deletion) + + def test_auto_filename_detection(self): + client, logger, channel_ids = ( + self.sync_client, + self.logger, + ",".join([self.channel_id]), + ) + upload = client.files_upload( + file="tests/data/日本語.txt", + # filename="日本語.txt", + filetype="text", + channels=channel_ids, + ) + self.assertIsNotNone(upload) + self.assertEqual("日本語.txt", upload["file"]["name"]) + self.assertEqual("日本語の文書です。", upload["file"]["preview"]) + + deletion = client.files_delete(file=upload["file"]["id"]) + self.assertIsNotNone(deletion) + + @async_test + async def test_auto_filename_detection_async(self): + client, logger, channel_ids = ( + self.async_client, + self.logger, + ",".join([self.channel_id]), + ) + upload = await client.files_upload( + file="tests/data/日本語.txt", + # filename="日本語.txt", + filetype="text", + channels=channel_ids, + ) + logger.debug("File uploaded - %s", upload) + self.assertIsNotNone(upload) + self.assertEqual("日本語.txt", upload["file"]["name"]) + self.assertEqual("日本語の文書です。", upload["file"]["preview"]) + + deletion = await client.files_delete(file=upload["file"]["id"]) + self.assertIsNotNone(deletion) diff --git a/integration_tests/web/test_issue_677.py b/integration_tests/web/test_issue_677.py new file mode 100644 index 000000000..15e1eb286 --- /dev/null +++ b/integration_tests/web/test_issue_677.py @@ -0,0 +1,67 @@ +import asyncio +import logging +import os +import unittest +from datetime import datetime + +from integration_tests.env_variable_names import ( + SLACK_SDK_TEST_BOT_TOKEN, + SLACK_SDK_TEST_WEB_TEST_CHANNEL_ID, +) +from integration_tests.helpers import async_test +from slack_sdk.web import WebClient + +# NOTE: this one is not supported in v3 +from slack.web.classes.objects import DateLink +from slack_sdk.web.async_client import AsyncWebClient + + +class TestWebClient(unittest.TestCase): + """Runs integration tests with real Slack API + + https://github.com/slackapi/python-slack-sdk/issues/677 + """ + + def setUp(self): + if not hasattr(self, "logger"): + 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: AsyncWebClient = AsyncWebClient(token=self.bot_token) + self.channel_id = os.environ[SLACK_SDK_TEST_WEB_TEST_CHANNEL_ID] + + def tearDown(self): + pass + + def test_date_link(self): + client = self.sync_client + link = DateLink( + date=datetime.now(), + date_format="{date_long} {time}", + fallback="fallback string", + link="https://www.example.com", + ) + message = f"Here is a date link: {link}" + response = client.chat_postMessage(channel=self.channel_id, text=message) + self.assertIsNotNone(response) + self.assertRegex( + r"Here is a date link: ", + response["message"]["text"], + ) + + @async_test + async def test_date_link_async(self): + client = self.async_client + link = DateLink( + date=datetime.now(), + date_format="{date_long} {time}", + fallback="fallback string", + link="https://www.example.com", + ) + message = f"Here is a date link: {link}" + response = await client.chat_postMessage(channel=self.channel_id, text=message) + self.assertIsNotNone(response) + self.assertRegex( + r"Here is a date link: ", + response["message"]["text"], + ) diff --git a/integration_tests/web/test_issue_714.py b/integration_tests/web/test_issue_714.py new file mode 100644 index 000000000..fbd2a9763 --- /dev/null +++ b/integration_tests/web/test_issue_714.py @@ -0,0 +1,31 @@ +import asyncio +import os +import unittest +from urllib.error import URLError + +from aiohttp import ClientConnectorError + +from integration_tests.env_variable_names import SLACK_SDK_TEST_BOT_TOKEN +from integration_tests.helpers import async_test +from slack_sdk.web import WebClient +from slack_sdk.web.async_client import AsyncWebClient + + +class TestWebClient(unittest.TestCase): + def setUp(self): + self.proxy = "http://invalid-host:9999" + self.bot_token = os.environ[SLACK_SDK_TEST_BOT_TOKEN] + + def tearDown(self): + pass + + def test_proxy_failure(self): + client: WebClient = WebClient(token=self.bot_token, proxy=self.proxy) + with self.assertRaises(URLError): + client.auth_test() + + @async_test + async def test_proxy_failure_async(self): + client: AsyncWebClient = AsyncWebClient(token=self.bot_token, proxy=self.proxy) + with self.assertRaises(ClientConnectorError): + await client.auth_test() diff --git a/integration_tests/web/test_issue_728.py b/integration_tests/web/test_issue_728.py new file mode 100644 index 000000000..080e1b988 --- /dev/null +++ b/integration_tests/web/test_issue_728.py @@ -0,0 +1,41 @@ +import os +import unittest + +from integration_tests.env_variable_names import ( + SLACK_SDK_TEST_BOT_TOKEN, + SLACK_SDK_TEST_WEB_TEST_CHANNEL_ID, +) +from integration_tests.helpers import async_test +from slack_sdk.web import WebClient +from slack_sdk.web.async_client import AsyncWebClient + + +class TestWebClient(unittest.TestCase): + """Runs integration tests with real Slack API + + https://github.com/slackapi/python-slack-sdk/issues/728 + """ + + def setUp(self): + self.bot_token = os.environ[SLACK_SDK_TEST_BOT_TOKEN] + self.channel_ids = ",".join([os.environ[SLACK_SDK_TEST_WEB_TEST_CHANNEL_ID]]) + + def tearDown(self): + pass + + def test_bytes_for_file_param(self): + client: WebClient = WebClient(token=self.bot_token) + bytes = bytearray("This is a test", "utf-8") + upload = client.files_upload(file=bytes, filename="test.txt", channels=self.channel_ids) + self.assertIsNotNone(upload) + deletion = client.files_delete(file=upload["file"]["id"]) + self.assertIsNotNone(deletion) + + @async_test + async def test_bytes_for_file_param_async(self): + client: AsyncWebClient = AsyncWebClient(token=self.bot_token) + bytes = bytearray("This is a test", "utf-8") + upload = await client.files_upload(file=bytes, filename="test.txt", channels=self.channel_ids) + self.assertIsNotNone(upload) + deletion = await client.files_delete(file=upload["file"]["id"]) + self.assertIsNotNone(deletion) diff --git a/integration_tests/web/test_issue_770.py b/integration_tests/web/test_issue_770.py new file mode 100644 index 000000000..d92395549 --- /dev/null +++ b/integration_tests/web/test_issue_770.py @@ -0,0 +1,46 @@ +import os +import unittest +from io import BytesIO + +from integration_tests.env_variable_names import ( + SLACK_SDK_TEST_BOT_TOKEN, + SLACK_SDK_TEST_WEB_TEST_CHANNEL_ID, +) +from integration_tests.helpers import async_test +from slack_sdk.web import WebClient +from slack_sdk.web.async_client import AsyncWebClient + + +class TestWebClient(unittest.TestCase): + """Runs integration tests with real Slack API + + export SLACK_SDK_TEST_BOT_TOKEN=xoxb-xxx + export SLACK_SDK_TEST_WEB_TEST_CHANNEL_ID=C111 + ./scripts/run_integration_tests.sh integration_tests/web/test_issue_770.py + + https://github.com/slackapi/python-slack-sdk/issues/770 + """ + + def setUp(self): + self.bot_token = os.environ[SLACK_SDK_TEST_BOT_TOKEN] + self.channel_ids = ",".join([os.environ[SLACK_SDK_TEST_WEB_TEST_CHANNEL_ID]]) + + def tearDown(self): + pass + + def test_bytes_for_file_param_bytes(self): + client: WebClient = WebClient(token=self.bot_token) + bytes = BytesIO(bytearray("This is a test (bytes)", "utf-8")).getvalue() + upload = client.files_upload(file=bytes, filename="test.txt", channels=self.channel_ids) + self.assertIsNotNone(upload) + deletion = client.files_delete(file=upload["file"]["id"]) + self.assertIsNotNone(deletion) + + @async_test + async def test_bytes_for_file_param_bytes_async(self): + client: AsyncWebClient = AsyncWebClient(token=self.bot_token) + bytes = BytesIO(bytearray("This is a test (bytes)", "utf-8")).getvalue() + upload = await client.files_upload(file=bytes, filename="test.txt", channels=self.channel_ids) + self.assertIsNotNone(upload) + deletion = await client.files_delete(file=upload["file"]["id"]) + self.assertIsNotNone(deletion) diff --git a/integration_tests/web/test_issue_809.py b/integration_tests/web/test_issue_809.py new file mode 100644 index 000000000..baa0123ba --- /dev/null +++ b/integration_tests/web/test_issue_809.py @@ -0,0 +1,41 @@ +import asyncio +import io +import logging +import os +import unittest + +from integration_tests.env_variable_names import SLACK_SDK_TEST_BOT_TOKEN +from integration_tests.helpers import async_test +from slack_sdk.web import WebClient +from slack_sdk.web.async_client import AsyncWebClient + + +class TestWebClient(unittest.TestCase): + """Runs integration tests with real Slack API + + https://github.com/slackapi/python-slack-sdk/issues/809 + """ + + 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: AsyncWebClient = AsyncWebClient(token=self.bot_token) + + def tearDown(self): + pass + + def test_issue_809(self): + client = self.sync_client + buff = io.BytesIO(b"here is my data but not sure what is wrong.......") + buff.seek(0) + upload = client.files_upload(file=buff) + self.assertIsNotNone(upload) + + @async_test + async def test_issue_809_async(self): + client = self.async_client + buff = io.BytesIO(b"here is my data but not sure what is wrong.......") + buff.seek(0) + upload = await client.files_upload(file=buff) + self.assertIsNotNone(upload) diff --git a/integration_tests/web/test_message_metadata.py b/integration_tests/web/test_message_metadata.py new file mode 100644 index 000000000..dc2626d27 --- /dev/null +++ b/integration_tests/web/test_message_metadata.py @@ -0,0 +1,244 @@ +import logging +import os +import time +import unittest +import json + +from integration_tests.env_variable_names import SLACK_SDK_TEST_BOT_TOKEN +from slack_sdk.models.metadata import ( + Metadata, + EventAndEntityMetadata, + EntityMetadata, + ExternalRef, + EntityPayload, + EntityAttributes, + EntityTitle, + TaskEntityFields, + EntityStringField, + EntityTitle, + EntityAttributes, + EntityFullSizePreview, + TaskEntityFields, + EntityTypedField, + EntityStringField, + EntityTimestampField, + EntityEditSupport, + EntityEditTextConfig, + EntityCustomField, + EntityUserIDField, + ExternalRef, +) +from slack_sdk.web import WebClient + + +class TestWebClient(unittest.TestCase): + def setUp(self): + self.logger = logging.getLogger(__name__) + self.bot_token = os.environ[SLACK_SDK_TEST_BOT_TOKEN] + + def tearDown(self): + pass + + def test_publishing_message_metadata(self): + client: WebClient = WebClient(token=self.bot_token) + new_message = client.chat_postMessage( + channel="#random", + text="message with metadata", + metadata={ + "event_type": "procurement-task", + "event_payload": { + "id": "11111", + "amount": 5000, + "tags": ["foo", "bar", "baz"], + }, + }, + ) + self.assertIsNone(new_message.get("error")) + self.assertIsNotNone(new_message.get("message").get("metadata")) + + history = client.conversations_history( + channel=new_message.get("channel"), + limit=1, + include_all_metadata=True, + ) + self.assertIsNone(history.get("error")) + self.assertIsNotNone(history.get("messages")[0].get("metadata")) + + modification = client.chat_update( + channel=new_message.get("channel"), + ts=new_message.get("ts"), + text="message with metadata (modified)", + metadata={ + "event_type": "procurement-task", + "event_payload": { + "id": "11111", + "amount": 6000, + }, + }, + ) + self.assertIsNone(modification.get("error")) + self.assertIsNotNone(modification.get("message").get("metadata")) + + scheduled = client.chat_scheduleMessage( + channel=new_message.get("channel"), + post_at=int(time.time()) + 30, + text="message with metadata (scheduled)", + metadata={ + "event_type": "procurement-task", + "event_payload": { + "id": "11111", + "amount": 10, + }, + }, + ) + self.assertIsNone(scheduled.get("error")) + self.assertIsNotNone(scheduled.get("message").get("metadata")) + + def test_publishing_message_metadata_using_models(self): + client: WebClient = WebClient(token=self.bot_token) + new_message = client.chat_postMessage( + channel="#random", + text="message with metadata", + metadata=Metadata( + event_type="procurement-task", + event_payload={ + "id": "11111", + "amount": 5000, + "tags": ["foo", "bar", "baz"], + }, + ), + ) + self.assertIsNone(new_message.get("error")) + self.assertIsNotNone(new_message.get("message").get("metadata")) + + history = client.conversations_history( + channel=new_message.get("channel"), + limit=1, + include_all_metadata=True, + ) + self.assertIsNone(history.get("error")) + self.assertIsNotNone(history.get("messages")[0].get("metadata")) + + modification = client.chat_update( + channel=new_message.get("channel"), + ts=new_message.get("ts"), + text="message with metadata (modified)", + metadata=Metadata( + event_type="procurement-task", + event_payload={ + "id": "11111", + "amount": 6000, + }, + ), + ) + self.assertIsNone(modification.get("error")) + self.assertIsNotNone(modification.get("message").get("metadata")) + + scheduled = client.chat_scheduleMessage( + channel=new_message.get("channel"), + post_at=int(time.time()) + 30, + text="message with metadata (scheduled)", + metadata=Metadata( + event_type="procurement-task", + event_payload={ + "id": "11111", + "amount": 10, + }, + ), + ) + self.assertIsNone(scheduled.get("error")) + self.assertIsNotNone(scheduled.get("message").get("metadata")) + + def test_publishing_entity_metadata(self): + client: WebClient = WebClient(token=self.bot_token) + new_message = client.chat_postMessage( + channel="C014KLZN9M0", + text="Message with entity metadata", + metadata={ + "entities": [ + { + "entity_type": "slack#/entities/task", + "url": "https://abc.com/123", + "external_ref": {"id": "123"}, + "entity_payload": { + "attributes": {"title": {"text": "My task"}, "product_name": "We reference only"}, + "fields": { + "due_date": {"value": "2026-06-06", "type": "slack#/types/date", "edit": {"enabled": True}}, + "created_by": {"type": "slack#/types/user", "user": {"user_id": "U014KLZE350"}}, + "date_created": {"value": 1760629278}, + }, + "custom_fields": [ + { + "label": "img", + "key": "img", + "type": "slack#/types/image", + "image_url": "https://s3-media2.fl.yelpcdn.com/bphoto/korel-1YjNtFtJlMTaC26A/o.jpg", + } + ], + }, + } + ] + }, + ) + + self.assertIsNone(new_message.get("error")) + self.assertIsNone(new_message.get("warning")) + + def test_publishing_entity_metadata_using_models(self): + # Build the metadata + + title = EntityTitle(text="My title") + full_size_preview = EntityFullSizePreview( + is_supported=True, + preview_url="https://s3-media3.fl.yelpcdn.com/bphoto/c7ed05m9lC2EmA3Aruue7A/o.jpg", + mime_type="image/jpeg", + ) + attributes = EntityAttributes(title=title, product_name="My Product", full_size_preview=full_size_preview) + description = EntityStringField( + value="Description of the task.", + long=True, + edit=EntityEditSupport(enabled=True, text=EntityEditTextConfig(min_length=5, max_length=100)), + ) + due_date = EntityTypedField(value="2026-06-06", type="slack#/types/date", edit=EntityEditSupport(enabled=True)) + created_by = EntityTypedField( + type="slack#/types/user", + user=EntityUserIDField(user_id="USLACKBOT"), + ) + date_created = EntityTimestampField(value=1762450663, type="slack#/types/timestamp") + date_updated = EntityTimestampField(value=1762450663, type="slack#/types/timestamp") + fields = TaskEntityFields( + description=description, + due_date=due_date, + created_by=created_by, + date_created=date_created, + date_updated=date_updated, + ) + custom_fields = [] + custom_fields.append( + EntityCustomField( + label="My Image", + key="my-image", + type="slack#/types/image", + image_url="https://s3-media3.fl.yelpcdn.com/bphoto/c7ed05m9lC2EmA3Aruue7A/o.jpg", + ) + ) + entity = EntityPayload(attributes=attributes, fields=fields, custom_fields=custom_fields) + + client: WebClient = WebClient(token=self.bot_token) + new_message = client.chat_postMessage( + channel="#random", + text="Message with entity metadata", + metadata=EventAndEntityMetadata( + entities=[ + EntityMetadata( + entity_type="slack#/entities/task", + external_ref=ExternalRef(id="abc123"), + url="https://myappdomain.com", + entity_payload=entity, + ) + ] + ), + ) + + self.assertIsNone(new_message.get("error")) + self.assertIsNone(new_message.get("warning")) diff --git a/integration_tests/web/test_remote_file_replacement.py b/integration_tests/web/test_remote_file_replacement.py new file mode 100644 index 000000000..2001b7c63 --- /dev/null +++ b/integration_tests/web/test_remote_file_replacement.py @@ -0,0 +1,88 @@ +import os +import time +import unittest +from uuid import uuid4 + +from integration_tests.env_variable_names import ( + SLACK_SDK_TEST_BOT_TOKEN, + SLACK_SDK_TEST_WEB_TEST_CHANNEL_ID, +) +from slack_sdk.web import WebClient + + +class TestWebClient(unittest.TestCase): + """ + Suggestion for https://github.com/slackapi/python-slack-sdk/issues/762 + """ + + def setUp(self): + self.bot_token = os.environ[SLACK_SDK_TEST_BOT_TOKEN] + self.channel_id = os.environ[SLACK_SDK_TEST_WEB_TEST_CHANNEL_ID] + + def tearDown(self): + pass + + def test_replacing_remote_file_blocks_in_a_message(self): + client: WebClient = WebClient(token=self.bot_token) + current_dir = os.path.dirname(__file__) + url = "https://www.example.com/slack-logo" + + external_id = f"remote-file-slack-logo-{uuid4()}" + remote_file_creation = client.files_remote_add( + external_id=external_id, + external_url=url, + title="Slack Logo", + indexable_file_contents="so many keywords!".encode("utf-8"), + preview_image=f"{current_dir}/../../tests/data/slack_logo.png", + ) + self.assertIsNotNone(remote_file_creation) + + new_message = client.chat_postMessage( + channel=self.channel_id, + text="Slack Logo v1", + blocks=[ + { + "type": "section", + "text": {"type": "plain_text", "text": "This is v1"}, + }, + { + "type": "file", + "external_id": external_id, + "source": "remote", + }, + ], + ) + self.assertIsNotNone(new_message) + message_ts = new_message["message"]["ts"] + + time.sleep(2) + + external_id = f"remote-file-slack-logo-{uuid4()}" + new_version = client.files_remote_add( + external_id=external_id, + external_url=url, + title="Slack Logo", + indexable_file_contents="more and more keywords!".encode("utf-8"), + preview_image=f"{current_dir}/../../tests/data/slack_logo_new.png", + ) + self.assertIsNotNone(new_version) + + time.sleep(3) + + modification = client.chat_update( + channel=self.channel_id, + ts=message_ts, + text="Slack Logo v2", + blocks=[ + { + "type": "section", + "text": {"type": "plain_text", "text": "This is v2"}, + }, + { + "type": "file", + "external_id": external_id, + "source": "remote", + }, + ], + ) + self.assertIsNotNone(modification) diff --git a/integration_tests/web/test_slack_lists.py b/integration_tests/web/test_slack_lists.py new file mode 100644 index 000000000..6d813bffe --- /dev/null +++ b/integration_tests/web/test_slack_lists.py @@ -0,0 +1,88 @@ +import logging +import os +import unittest + +from integration_tests.env_variable_names import ( + SLACK_SDK_TEST_BOT_TOKEN, +) +from integration_tests.helpers import async_test +from slack_sdk.web import WebClient +from slack_sdk.web.async_client import AsyncWebClient +from slack_sdk.models.blocks import RichTextBlock +from slack_sdk.models.blocks.block_elements import RichTextSection, RichTextText + + +class TestSlackLists(unittest.TestCase): + """Runs integration tests with real Slack API testing the slackLists.* APIs""" + + def setUp(self): + if not hasattr(self, "logger"): + self.logger = logging.getLogger(__name__) + self.bot_token = os.environ[SLACK_SDK_TEST_BOT_TOKEN] + self.async_client: AsyncWebClient = AsyncWebClient(token=self.bot_token) + self.sync_client: WebClient = WebClient(token=self.bot_token) + + def tearDown(self): + pass + + def test_create_list_with_dicts(self): + """Test creating a list with description_blocks as dicts""" + client = self.sync_client + + create_response = client.slackLists_create( + name="Test Sales Pipeline", + description_blocks=[ + { + "type": "rich_text", + "elements": [ + { + "type": "rich_text_section", + "elements": [{"type": "text", "text": "This is a test list for integration testing"}], + } + ], + } + ], + schema=[ + {"key": "deal_name", "name": "Deal Name", "type": "text", "is_primary_column": True}, + {"key": "amount", "name": "Amount", "type": "number", "options": {"format": "currency", "precision": 2}}, + ], + ) + + self.assertIsNotNone(create_response) + self.assertTrue(create_response["ok"]) + self.assertIn("list", create_response) + list_id = create_response["list"]["id"] + self.logger.info(f"✓ Created list with ID: {list_id}") + + def test_create_list_with_rich_text_blocks(self): + """Test creating a list with RichTextBlock objects""" + client = self.sync_client + + create_response = client.slackLists_create( + name="Test List with Rich Text Blocks", + description_blocks=[ + RichTextBlock( + elements=[RichTextSection(elements=[RichTextText(text="Created with RichTextBlock objects!")])] + ) + ], + schema=[{"key": "task_name", "name": "Task", "type": "text", "is_primary_column": True}], + ) + + self.assertIsNotNone(create_response) + self.assertTrue(create_response["ok"]) + list_id = create_response["list"]["id"] + self.logger.info(f"✓ Created list with RichTextBlocks, ID: {list_id}") + + @async_test + async def test_create_list_async(self): + """Test creating a list with async client""" + client = self.async_client + + create_response = await client.slackLists_create( + name="Async Test List", schema=[{"key": "item_name", "name": "Item", "type": "text", "is_primary_column": True}] + ) + + self.assertIsNotNone(create_response) + self.assertTrue(create_response["ok"]) + list_id = create_response["list"]["id"] + self.logger.info(f"✓ Created list asynchronously, ID: {list_id}") diff --git a/integration_tests/web/test_team.py b/integration_tests/web/test_team.py new file mode 100644 index 000000000..c560adc7b --- /dev/null +++ b/integration_tests/web/test_team.py @@ -0,0 +1,28 @@ +import os +import unittest + +from integration_tests.env_variable_names import SLACK_SDK_TEST_BOT_TOKEN +from slack_sdk.web import WebClient + + +class TestWebClient(unittest.TestCase): + def setUp(self): + self.bot_token = os.environ[SLACK_SDK_TEST_BOT_TOKEN] + self.client: WebClient = WebClient(token=self.bot_token) + + def tearDown(self): + pass + + def test_team_billing_info(self): + response = self.client.team_billing_info() + self.assertIsNone(response.get("error")) + self.assertIsNotNone(response.get("plan")) + + def test_team_preferences_list(self): + response = self.client.team_preferences_list() + self.assertIsNone(response.get("error")) + self.assertIsNotNone(response.get("msg_edit_window_mins")) + self.assertIsNotNone(response.get("allow_message_deletion")) + self.assertIsNotNone(response.get("display_real_names")) + self.assertIsNotNone(response.get("disable_file_uploads")) + self.assertIsNotNone(response.get("who_can_post_general")) diff --git a/integration_tests/web/test_web_client.py b/integration_tests/web/test_web_client.py new file mode 100644 index 000000000..21adbb569 --- /dev/null +++ b/integration_tests/web/test_web_client.py @@ -0,0 +1,358 @@ +import asyncio +import logging +import os +import unittest + +import pytest + +from integration_tests.env_variable_names import ( + SLACK_SDK_TEST_BOT_TOKEN, + SLACK_SDK_TEST_WEB_TEST_CHANNEL_ID, +) +from integration_tests.helpers import async_test, is_not_specified +from slack_sdk.web import WebClient +from slack_sdk.web.slack_response import SlackResponse +from slack_sdk.web.async_client import AsyncWebClient +from slack_sdk.web.legacy_client import LegacyWebClient + + +class TestWebClient(unittest.TestCase): + """Runs integration tests with real Slack API""" + + def setUp(self): + if not hasattr(self, "logger"): + self.logger = logging.getLogger(__name__) + self.bot_token = os.environ[SLACK_SDK_TEST_BOT_TOKEN] + self.async_client: AsyncWebClient = AsyncWebClient(token=self.bot_token) + self.sync_client: WebClient = WebClient(token=self.bot_token) + self.channel_id = os.environ[SLACK_SDK_TEST_WEB_TEST_CHANNEL_ID] + + def tearDown(self): + pass + + # ------------------------- + # api.test + + def test_api_test(self): + response: SlackResponse = self.sync_client.api_test(foo="bar") + self.assertEqual(response["args"]["foo"], "bar") + + @async_test + async def test_api_test_async(self): + response: SlackResponse = await self.async_client.api_test(foo="bar") + self.assertEqual(response["args"]["foo"], "bar") + + # ------------------------- + # auth.test + + def test_auth_test(self): + response: SlackResponse = self.sync_client.auth_test() + self.verify_auth_test_response(response) + + @async_test + async def test_auth_test_async(self): + response: SlackResponse = await self.async_client.auth_test() + self.verify_auth_test_response(response) + + def verify_auth_test_response(self, response): + self.assertIsNotNone(response["url"]) + self.assertIsNotNone(response["user"]) + self.assertIsNotNone(response["user_id"]) + self.assertIsNotNone(response["team"]) + self.assertIsNotNone(response["team_id"]) + self.assertIsNotNone(response["bot_id"]) + + # ------------------------- + # basic metadata retrieval + + def test_metadata_retrieval(self): + client = self.sync_client + auth = client.auth_test() + self.assertIsNotNone(auth) + bot = client.bots_info(bot=auth["bot_id"]) + self.assertIsNotNone(bot) + + @async_test + async def test_metadata_retrieval_async(self): + client = self.async_client + auth = await client.auth_test() + self.assertIsNotNone(auth) + bot = await client.bots_info(bot=auth["bot_id"]) + self.assertIsNotNone(bot) + + # ------------------------- + # basic chat operations + + def test_basic_chat_operations(self): + client = self.sync_client + + auth = client.auth_test() + self.assertIsNotNone(auth) + url = auth["url"] + + channel = self.channel_id + message = ( + "This message was posted by ! " + + "(integration_tests/test_web_client.py #test_chat_operations)" + ) + new_message: SlackResponse = client.chat_postMessage(channel=channel, text=message) + self.assertEqual(new_message["message"]["text"], message) + ts = new_message["ts"] + + permalink = client.chat_getPermalink(channel=channel, message_ts=ts) + self.assertIsNotNone(permalink) + self.assertRegex( + permalink["permalink"], + f"{url}archives/{channel}/.+", + ) + + new_reaction = client.reactions_add(channel=channel, timestamp=ts, name="eyes") + self.assertIsNotNone(new_reaction) + + reactions = client.reactions_get(channel=channel, timestamp=ts) + self.assertIsNotNone(reactions) + + reaction_removal = client.reactions_remove(channel=channel, timestamp=ts, name="eyes") + self.assertIsNotNone(reaction_removal) + + thread_reply = client.chat_postMessage(channel=channel, thread_ts=ts, text="threading...") + self.assertIsNotNone(thread_reply) + + modification = client.chat_update(channel=channel, ts=ts, text="Is this intentional?") + self.assertIsNotNone(modification) + + reply_deletion = client.chat_delete(channel=channel, ts=thread_reply["ts"]) + self.assertIsNotNone(reply_deletion) + message_deletion = client.chat_delete(channel=channel, ts=ts) + self.assertIsNotNone(message_deletion) + + @async_test + async def test_basic_chat_operations_async(self): + client = self.async_client + + auth = await client.auth_test() + self.assertIsNotNone(auth) + url = auth["url"] + + channel = self.channel_id + message = ( + "This message was posted by ! " + + "(integration_tests/test_web_client.py #test_chat_operations)" + ) + new_message: SlackResponse = await client.chat_postMessage(channel=channel, text=message) + self.assertEqual(new_message["message"]["text"], message) + ts = new_message["ts"] + + permalink = await client.chat_getPermalink(channel=channel, message_ts=ts) + self.assertIsNotNone(permalink) + self.assertRegex( + permalink["permalink"], + f"{url}archives/{channel}/.+", + ) + + new_reaction = await client.reactions_add(channel=channel, timestamp=ts, name="eyes") + self.assertIsNotNone(new_reaction) + + reactions = await client.reactions_get(channel=channel, timestamp=ts) + self.assertIsNotNone(reactions) + + reaction_removal = await client.reactions_remove(channel=channel, timestamp=ts, name="eyes") + self.assertIsNotNone(reaction_removal) + + thread_reply = await client.chat_postMessage(channel=channel, thread_ts=ts, text="threading...") + self.assertIsNotNone(thread_reply) + + modification = await client.chat_update(channel=channel, ts=ts, text="Is this intentional?") + self.assertIsNotNone(modification) + + reply_deletion = await client.chat_delete(channel=channel, ts=thread_reply["ts"]) + self.assertIsNotNone(reply_deletion) + message_deletion = await client.chat_delete(channel=channel, ts=ts) + self.assertIsNotNone(message_deletion) + + # ------------------------- + # file operations + + def test_uploading_text_files(self): + client = self.sync_client + file, filename = __file__, os.path.basename(__file__) + upload = client.files_upload(channels=self.channel_id, filename=filename, file=file) + self.assertIsNotNone(upload) + + deletion = client.files_delete(file=upload["file"]["id"]) + self.assertIsNotNone(deletion) + + @async_test + async def test_uploading_text_files_async(self): + client = self.async_client + file, filename = __file__, os.path.basename(__file__) + upload = await client.files_upload( + channels=self.channel_id, + title="Good Old Slack Logo", + filename=filename, + file=file, + ) + self.assertIsNotNone(upload) + + deletion = await client.files_delete(file=upload["file"]["id"]) + self.assertIsNotNone(deletion) + + def test_uploading_binary_files(self): + client = self.sync_client + current_dir = os.path.dirname(__file__) + file = f"{current_dir}/../../tests/data/slack_logo.png" + upload = client.files_upload( + channels=self.channel_id, + title="Good Old Slack Logo", + filename="slack_logo.png", + file=file, + ) + self.assertIsNotNone(upload) + + deletion = client.files_delete(file=upload["file"]["id"]) + self.assertIsNotNone(deletion) + + def test_uploading_binary_files_as_content(self): + client = self.sync_client + current_dir = os.path.dirname(__file__) + file = f"{current_dir}/../../tests/data/slack_logo.png" + with open(file, "rb") as f: + content = f.read() + upload = client.files_upload( + channels=self.channel_id, + title="Good Old Slack Logo", + filename="slack_logo.png", + content=content, + ) + self.assertIsNotNone(upload) + + deletion = client.files_delete(file=upload["file"]["id"]) + self.assertIsNotNone(deletion) + + @async_test + async def test_uploading_binary_files_async(self): + client = self.async_client + current_dir = os.path.dirname(__file__) + file = f"{current_dir}/../../tests/data/slack_logo.png" + upload = await client.files_upload( + channels=self.channel_id, + title="Good Old Slack Logo", + filename="slack_logo.png", + file=file, + ) + self.assertIsNotNone(upload) + + deletion = await client.files_delete(file=upload["file"]["id"]) + self.assertIsNotNone(deletion) + + def test_uploading_file_with_token_param(self): + client = WebClient() + current_dir = os.path.dirname(__file__) + file = f"{current_dir}/../../tests/data/slack_logo.png" + upload = client.files_upload( + token=self.bot_token, + channels=self.channel_id, + title="Good Old Slack Logo", + filename="slack_logo.png", + file=file, + ) + self.assertIsNotNone(upload) + + deletion = client.files_delete( + token=self.bot_token, + file=upload["file"]["id"], + ) + self.assertIsNotNone(deletion) + + @async_test + async def test_uploading_file_with_token_param_async(self): + client = AsyncWebClient() + current_dir = os.path.dirname(__file__) + file = f"{current_dir}/../../tests/data/slack_logo.png" + upload = await client.files_upload( + token=self.bot_token, + channels=self.channel_id, + title="Good Old Slack Logo", + filename="slack_logo.png", + file=file, + ) + self.assertIsNotNone(upload) + + deletion = await client.files_delete( + token=self.bot_token, + file=upload["file"]["id"], + ) + self.assertIsNotNone(deletion) + + # ------------------------- + # pagination + + def test_pagination_with_iterator(self): + client = self.sync_client + fetched_count = 0 + # SlackResponse is an iterator that fetches next if next_cursor is not "" + for response in client.conversations_list(limit=1, exclude_archived=1, types="public_channel"): + fetched_count += len(response["channels"]) + if fetched_count > 1: + break + + self.assertGreater(fetched_count, 1) + + def test_pagination_with_iterator_use_sync_aiohttp(self): + client: LegacyWebClient = LegacyWebClient( + token=self.bot_token, + run_async=False, + use_sync_aiohttp=True, + loop=asyncio.new_event_loop(), + ) + fetched_count = 0 + # SlackResponse is an iterator that fetches next if next_cursor is not "" + for response in client.conversations_list(limit=1, exclude_archived=1, types="public_channel"): + fetched_count += len(response["channels"]) + if fetched_count > 1: + break + + self.assertGreater(fetched_count, 1) + + @pytest.mark.skipif(condition=is_not_specified(), reason="still unfixed") + @async_test + async def test_pagination_with_iterator_async(self): + client = self.async_client + fetched_count = 0 + # SlackResponse is an iterator that fetches next if next_cursor is not "" + for response in await client.conversations_list(limit=1, exclude_archived=1, types="public_channel"): + fetched_count += len(response["channels"]) + if fetched_count > 1: + break + + self.assertGreater(fetched_count, 1) + + # ====================================================================================================== FAILURES ======================================================================================================= + # __________________________________________________________________________________ TestWebClient.test_pagination_with_iterator_async __________________________________________________________________________________ + # + # args = (,), kwargs = {}, current_loop = <_UnixSelectorEventLoop running=False closed=False debug=False> + # + # def wrapper(*args, **kwargs): + # current_loop: AbstractEventLoop = asyncio.get_event_loop() + # > return current_loop.run_until_complete(coro(*args, **kwargs)) + # + # integration_tests/helpers.py:11: + # _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + # path-to-python/asyncio/base_events.py:616: in run_until_complete + # return future.result() + # integration_tests/web/test_web_client.py:183: in test_pagination_with_iterator_async + # for response in await client.conversations_list(limit=1, exclude_archived=1, types="public_channel"): + # slack/web/slack_response.py:135: in __next__ + # response = asyncio.get_event_loop().run_until_complete( + # path-to-python/asyncio/base_events.py:592: in run_until_complete + # self._check_running() + # _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + # + # self = <_UnixSelectorEventLoop running=False closed=False debug=False> + # + # def _check_running(self): + # if self.is_running(): + # > raise RuntimeError('This event loop is already running') + # E RuntimeError: This event loop is already running + # + # path-to-python/asyncio/base_events.py:552: RuntimeError diff --git a/integration_tests/webhook/test_async_webhook.py b/integration_tests/webhook/test_async_webhook.py new file mode 100644 index 000000000..8095c2fa1 --- /dev/null +++ b/integration_tests/webhook/test_async_webhook.py @@ -0,0 +1,292 @@ +import os +from tests.helpers import async_test +import unittest +import time + +from integration_tests.env_variable_names import ( + SLACK_SDK_TEST_INCOMING_WEBHOOK_URL, + SLACK_SDK_TEST_INCOMING_WEBHOOK_CHANNEL_NAME, + SLACK_SDK_TEST_BOT_TOKEN, +) +from slack_sdk.web.async_client import AsyncWebClient +from slack_sdk.webhook.async_client import AsyncWebhookClient +from slack_sdk.models.attachments import Attachment, AttachmentField +from slack_sdk.models.blocks import SectionBlock, DividerBlock, ActionsBlock +from slack_sdk.models.blocks.block_elements import ButtonElement +from slack_sdk.models.blocks.basic_components import MarkdownTextObject, PlainTextObject + + +class TestAsyncWebhook(unittest.TestCase): + @async_test + async def setUp(self): + if not hasattr(self, "channel_id"): + token = os.environ[SLACK_SDK_TEST_BOT_TOKEN] + channel_name = os.environ[SLACK_SDK_TEST_INCOMING_WEBHOOK_CHANNEL_NAME].replace("#", "") + client = AsyncWebClient(token=token) + self.channel_id = None + async for resp in await client.conversations_list(limit=1000): + for c in resp["channels"]: + if c["name"] == channel_name: + self.channel_id = c["id"] + break + if self.channel_id is not None: + break + + def tearDown(self): + pass + + @async_test + async def test_webhook(self): + url = os.environ[SLACK_SDK_TEST_INCOMING_WEBHOOK_URL] + webhook = AsyncWebhookClient(url) + response = await webhook.send(text="Hello!") + self.assertEqual(200, response.status_code) + self.assertEqual("ok", response.body) + + token = os.environ[SLACK_SDK_TEST_BOT_TOKEN] + client = AsyncWebClient(token=token) + history = await client.conversations_history(channel=self.channel_id, limit=1) + self.assertIsNotNone(history) + actual_text = history["messages"][0]["text"] + self.assertEqual("Hello!", actual_text) + + @async_test + async def test_with_unfurls_off(self): + url = os.environ[SLACK_SDK_TEST_INCOMING_WEBHOOK_URL] + token = os.environ[SLACK_SDK_TEST_BOT_TOKEN] + webhook = AsyncWebhookClient(url) + client = AsyncWebClient(token=token) + # send message that does not unfurl + response = await webhook.send( + text="", + unfurl_links=False, + unfurl_media=False, + ) + self.assertEqual(200, response.status_code) + self.assertEqual("ok", response.body) + # wait to allow Slack API to edit message with attachments + time.sleep(2) + history = await client.conversations_history(channel=self.channel_id, limit=1) + self.assertIsNotNone(history) + self.assertTrue("attachments" not in history["messages"][0]) + + @async_test + async def test_with_unfurls_on(self): + # Slack API rate limits unfurls of unique links so test will + # fail when repeated. For testing, either use a different URL + # for text option or delete existing attachments in webhook channel. + url = os.environ[SLACK_SDK_TEST_INCOMING_WEBHOOK_URL] + token = os.environ[SLACK_SDK_TEST_BOT_TOKEN] + webhook = AsyncWebhookClient(url) + client = AsyncWebClient(token=token) + # send message that does unfurl + response = await webhook.send( + text="", + unfurl_links=True, + unfurl_media=True, + ) + self.assertEqual(200, response.status_code) + self.assertEqual("ok", response.body) + # wait to allow Slack API to edit message with attachments + time.sleep(2) + history = await client.conversations_history(channel=self.channel_id, limit=1) + self.assertIsNotNone(history) + # FIXME: when repeatedly running this test, the following assertion can fail + self.assertTrue("attachments" in history["messages"][0]) + + @async_test + async def test_with_blocks(self): + url = os.environ[SLACK_SDK_TEST_INCOMING_WEBHOOK_URL] + webhook = AsyncWebhookClient(url) + response = await webhook.send( + text="fallback", + blocks=[ + SectionBlock( + block_id="sb-id", + text=MarkdownTextObject(text="This is a mrkdwn text section block."), + fields=[ + PlainTextObject(text="*this is plain_text text*", emoji=True), + MarkdownTextObject(text="*this is mrkdwn text*"), + PlainTextObject(text="*this is plain_text text*", emoji=True), + ], + ), + DividerBlock(), + ActionsBlock( + elements=[ + ButtonElement( + text=PlainTextObject(text="Create New Task", emoji=True), + style="primary", + value="create_task", + ), + ButtonElement( + text=PlainTextObject(text="Create New Project", emoji=True), + value="create_project", + ), + ButtonElement( + text=PlainTextObject(text="Help", emoji=True), + value="help", + ), + ], + ), + ], + ) + self.assertEqual(200, response.status_code) + self.assertEqual("ok", response.body) + + @async_test + async def test_with_blocks_dict(self): + url = os.environ[SLACK_SDK_TEST_INCOMING_WEBHOOK_URL] + webhook = AsyncWebhookClient(url) + response = await webhook.send( + text="fallback", + blocks=[ + { + "type": "section", + "block_id": "sb-id", + "text": { + "type": "mrkdwn", + "text": "This is a mrkdwn text section block.", + }, + "fields": [ + { + "type": "plain_text", + "text": "*this is plain_text text*", + }, + { + "type": "mrkdwn", + "text": "*this is mrkdwn text*", + }, + { + "type": "plain_text", + "text": "*this is plain_text text*", + }, + ], + }, + {"type": "divider", "block_id": "9SxG"}, + { + "type": "actions", + "block_id": "avJ", + "elements": [ + { + "type": "button", + "action_id": "yXqIx", + "text": { + "type": "plain_text", + "text": "Create New Task", + }, + "style": "primary", + "value": "create_task", + }, + { + "type": "button", + "action_id": "KCdDw", + "text": { + "type": "plain_text", + "text": "Create New Project", + }, + "value": "create_project", + }, + { + "type": "button", + "action_id": "MXjB", + "text": { + "type": "plain_text", + "text": "Help", + }, + "value": "help", + }, + ], + }, + ], + ) + self.assertEqual(200, response.status_code) + self.assertEqual("ok", response.body) + + @async_test + async def test_with_attachments(self): + url = os.environ[SLACK_SDK_TEST_INCOMING_WEBHOOK_URL] + webhook = AsyncWebhookClient(url) + response = await webhook.send( + text="fallback", + attachments=[ + Attachment( + text="attachment text", + title="Attachment", + fallback="fallback_text", + pretext="some_pretext", + title_link="link in title", + fields=[AttachmentField(title=f"field_{i}_title", value=f"field_{i}_value") for i in range(5)], + color="#FFFF00", + author_name="John Doe", + author_link="http://johndoeisthebest.com", + author_icon="http://johndoeisthebest.com/avatar.jpg", + thumb_url="thumbnail URL", + footer="and a footer", + footer_icon="link to footer icon", + ts=123456789, + markdown_in=["fields"], + ) + ], + ) + self.assertEqual(200, response.status_code) + self.assertEqual("ok", response.body) + + @async_test + async def test_with_attachments_dict(self): + url = os.environ[SLACK_SDK_TEST_INCOMING_WEBHOOK_URL] + webhook = AsyncWebhookClient(url) + response = await webhook.send( + text="fallback", + attachments=[ + { + "author_name": "John Doe", + "fallback": "fallback_text", + "text": "attachment text", + "pretext": "some_pretext", + "title": "Attachment", + "footer": "and a footer", + "id": 1, + "author_link": "http://johndoeisthebest.com", + "color": "FFFF00", + "fields": [ + { + "title": "field_0_title", + "value": "field_0_value", + }, + { + "title": "field_1_title", + "value": "field_1_value", + }, + { + "title": "field_2_title", + "value": "field_2_value", + }, + { + "title": "field_3_title", + "value": "field_3_value", + }, + { + "title": "field_4_title", + "value": "field_4_value", + }, + ], + "mrkdwn_in": ["fields"], + } + ], + ) + self.assertEqual(200, response.status_code) + self.assertEqual("ok", response.body) + + @async_test + async def test_metadata(self): + url = os.environ[SLACK_SDK_TEST_INCOMING_WEBHOOK_URL] + webhook = AsyncWebhookClient(url) + response = await webhook.send( + text="Hello with metadata", + metadata={ + "event_type": "foo", + "event_payload": {"foo": "bar"}, + }, + ) + self.assertEqual(200, response.status_code) + self.assertEqual("ok", response.body) diff --git a/integration_tests/webhook/test_webhook.py b/integration_tests/webhook/test_webhook.py new file mode 100644 index 000000000..d1e55c6a0 --- /dev/null +++ b/integration_tests/webhook/test_webhook.py @@ -0,0 +1,285 @@ +import os +import unittest +import time + +import pytest + +from integration_tests.env_variable_names import ( + SLACK_SDK_TEST_INCOMING_WEBHOOK_URL, + SLACK_SDK_TEST_INCOMING_WEBHOOK_CHANNEL_NAME, + SLACK_SDK_TEST_BOT_TOKEN, +) +from slack_sdk.web import WebClient +from slack_sdk.webhook import WebhookClient +from slack_sdk.models.attachments import Attachment, AttachmentField +from slack_sdk.models.blocks import SectionBlock, DividerBlock, ActionsBlock +from slack_sdk.models.blocks.block_elements import ButtonElement +from slack_sdk.models.blocks.basic_components import MarkdownTextObject, PlainTextObject + + +class TestWebhook(unittest.TestCase): + def setUp(self): + if not hasattr(self, "channel_id"): + token = os.environ[SLACK_SDK_TEST_BOT_TOKEN] + channel_name = os.environ[SLACK_SDK_TEST_INCOMING_WEBHOOK_CHANNEL_NAME].replace("#", "") + client = WebClient(token=token) + self.channel_id = None + for resp in client.conversations_list(limit=1000): + for c in resp["channels"]: + if c["name"] == channel_name: + self.channel_id = c["id"] + break + if self.channel_id is not None: + break + + def tearDown(self): + pass + + def test_webhook(self): + url = os.environ[SLACK_SDK_TEST_INCOMING_WEBHOOK_URL] + webhook = WebhookClient(url) + response = webhook.send(text="Hello!") + self.assertEqual(200, response.status_code) + self.assertEqual("ok", response.body) + + token = os.environ[SLACK_SDK_TEST_BOT_TOKEN] + client = WebClient(token=token) + history = client.conversations_history(channel=self.channel_id, limit=1) + self.assertIsNotNone(history) + actual_text = history["messages"][0]["text"] + self.assertEqual("Hello!", actual_text) + + def test_with_unfurls_off(self): + url = os.environ[SLACK_SDK_TEST_INCOMING_WEBHOOK_URL] + token = os.environ[SLACK_SDK_TEST_BOT_TOKEN] + webhook = WebhookClient(url) + client = WebClient(token=token) + # send message that does not unfurl + response = webhook.send( + text="", + unfurl_links=False, + unfurl_media=False, + ) + self.assertEqual(200, response.status_code) + self.assertEqual("ok", response.body) + # wait to allow Slack API to edit message with attachments + time.sleep(2) + history = client.conversations_history(channel=self.channel_id, limit=1) + self.assertIsNotNone(history) + self.assertTrue("attachments" not in history["messages"][0]) + + # FIXME: This test started failing as of August 5, 2021 + @pytest.mark.skip() + def test_with_unfurls_on(self): + # Slack API rate limits unfurls of unique links so test will + # fail when repeated. For testing, either use a different URL + # for text option or delete existing attachments in webhook channel. + url = os.environ[SLACK_SDK_TEST_INCOMING_WEBHOOK_URL] + token = os.environ[SLACK_SDK_TEST_BOT_TOKEN] + webhook = WebhookClient(url) + client = WebClient(token=token) + # send message that does unfurl + response = webhook.send( + text="", + unfurl_links=True, + unfurl_media=True, + ) + self.assertEqual(200, response.status_code) + self.assertEqual("ok", response.body) + # wait to allow Slack API to edit message with attachments + time.sleep(2) + history = client.conversations_history(channel=self.channel_id, limit=1) + self.assertIsNotNone(history) + self.assertTrue("attachments" in history["messages"][0]) + + def test_with_blocks(self): + url = os.environ[SLACK_SDK_TEST_INCOMING_WEBHOOK_URL] + webhook = WebhookClient(url) + response = webhook.send( + text="fallback", + blocks=[ + SectionBlock( + block_id="sb-id", + text=MarkdownTextObject(text="This is a mrkdwn text section block."), + fields=[ + PlainTextObject(text="*this is plain_text text*", emoji=True), + MarkdownTextObject(text="*this is mrkdwn text*"), + PlainTextObject(text="*this is plain_text text*", emoji=True), + ], + ), + DividerBlock(), + ActionsBlock( + elements=[ + ButtonElement( + text=PlainTextObject(text="Create New Task", emoji=True), + style="primary", + value="create_task", + ), + ButtonElement( + text=PlainTextObject(text="Create New Project", emoji=True), + value="create_project", + ), + ButtonElement( + text=PlainTextObject(text="Help", emoji=True), + value="help", + ), + ], + ), + ], + ) + self.assertEqual(200, response.status_code) + self.assertEqual("ok", response.body) + + def test_with_blocks_dict(self): + url = os.environ[SLACK_SDK_TEST_INCOMING_WEBHOOK_URL] + webhook = WebhookClient(url) + response = webhook.send( + text="fallback", + blocks=[ + { + "type": "section", + "block_id": "sb-id", + "text": { + "type": "mrkdwn", + "text": "This is a mrkdwn text section block.", + }, + "fields": [ + { + "type": "plain_text", + "text": "*this is plain_text text*", + }, + { + "type": "mrkdwn", + "text": "*this is mrkdwn text*", + }, + { + "type": "plain_text", + "text": "*this is plain_text text*", + }, + ], + }, + {"type": "divider", "block_id": "9SxG"}, + { + "type": "actions", + "block_id": "avJ", + "elements": [ + { + "type": "button", + "action_id": "yXqIx", + "text": { + "type": "plain_text", + "text": "Create New Task", + }, + "style": "primary", + "value": "create_task", + }, + { + "type": "button", + "action_id": "KCdDw", + "text": { + "type": "plain_text", + "text": "Create New Project", + }, + "value": "create_project", + }, + { + "type": "button", + "action_id": "MXjB", + "text": { + "type": "plain_text", + "text": "Help", + }, + "value": "help", + }, + ], + }, + ], + ) + self.assertEqual(200, response.status_code) + self.assertEqual("ok", response.body) + + def test_with_attachments(self): + url = os.environ[SLACK_SDK_TEST_INCOMING_WEBHOOK_URL] + webhook = WebhookClient(url) + response = webhook.send( + text="fallback", + attachments=[ + Attachment( + text="attachment text", + title="Attachment", + fallback="fallback_text", + pretext="some_pretext", + title_link="link in title", + fields=[AttachmentField(title=f"field_{i}_title", value=f"field_{i}_value") for i in range(5)], + color="#FFFF00", + author_name="John Doe", + author_link="http://johndoeisthebest.com", + author_icon="http://johndoeisthebest.com/avatar.jpg", + thumb_url="thumbnail URL", + footer="and a footer", + footer_icon="link to footer icon", + ts=123456789, + markdown_in=["fields"], + ) + ], + ) + self.assertEqual(200, response.status_code) + self.assertEqual("ok", response.body) + + def test_with_attachments_dict(self): + url = os.environ[SLACK_SDK_TEST_INCOMING_WEBHOOK_URL] + webhook = WebhookClient(url) + response = webhook.send( + text="fallback", + attachments=[ + { + "author_name": "John Doe", + "fallback": "fallback_text", + "text": "attachment text", + "pretext": "some_pretext", + "title": "Attachment", + "footer": "and a footer", + "id": 1, + "author_link": "http://johndoeisthebest.com", + "color": "FFFF00", + "fields": [ + { + "title": "field_0_title", + "value": "field_0_value", + }, + { + "title": "field_1_title", + "value": "field_1_value", + }, + { + "title": "field_2_title", + "value": "field_2_value", + }, + { + "title": "field_3_title", + "value": "field_3_value", + }, + { + "title": "field_4_title", + "value": "field_4_value", + }, + ], + "mrkdwn_in": ["fields"], + } + ], + ) + self.assertEqual(200, response.status_code) + self.assertEqual("ok", response.body) + + def test_metadata(self): + url = os.environ[SLACK_SDK_TEST_INCOMING_WEBHOOK_URL] + webhook = WebhookClient(url) + response = webhook.send( + text="Hello with metadata", + metadata={ + "event_type": "foo", + "event_payload": {"foo": "bar"}, + }, + ) + self.assertEqual(200, response.status_code) + self.assertEqual("ok", response.body) diff --git a/logs/.gitkeep b/logs/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 000000000..37c6f3546 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,79 @@ +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" + +[project] +name = "slack_sdk" +dynamic = ["version", "readme", "authors", "optional-dependencies"] +description = "The Slack API Platform SDK for Python" +license = { text = "MIT" } +requires-python = ">=3.7" +keywords = [ + "slack", + "slack-api", + "web-api", + "slack-rtm", + "websocket", + "chat", + "chatbot", + "chatops", +] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "Topic :: Communications :: Chat", + "Topic :: System :: Networking", + "Topic :: Office/Business", + "License :: OSI Approved :: MIT License", + "Programming Language :: Python", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", +] + + +[project.urls] +Documentation = "https://docs.slack.dev/tools/python-slack-sdk/" + +[tool.setuptools.packages.find] +include = ["slack*", "slack_sdk*"] + +[tool.setuptools.dynamic] +version = { attr = "slack_sdk.version.__version__" } +readme = { file = ["README.md"], content-type = "text/markdown" } +optional-dependencies.optional = { file = ["requirements/optional.txt"] } + +[tool.distutils.bdist_wheel] +universal = true + +[tool.black] +line-length = 125 + +[tool.pytest.ini_options] +testpaths = ["tests"] +log_file = "logs/pytest.log" +log_file_level = "DEBUG" +log_format = "%(asctime)s %(levelname)s %(message)s" +log_date_format = "%Y-%m-%d %H:%M:%S" +filterwarnings = [ + "ignore:\"@coroutine\" decorator is deprecated since Python 3.8, use \"async def\" instead:DeprecationWarning", + "ignore:The loop argument is deprecated since Python 3.8, and scheduled for removal in Python 3.10.:DeprecationWarning", + "ignore:slack.* package is deprecated. Please use slack_sdk.* package instead.*:UserWarning", +] +asyncio_mode = "auto" + + +[tool.mypy] +files = "slack_sdk/" +exclude = ["slack_sdk/scim", "slack_sdk/rtm"] +force_union_syntax = true +warn_unused_ignores = true +enable_error_code = "ignore-without-code" diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index db0afb87c..000000000 --- a/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -websocket-client diff --git a/requirements/documentation.txt b/requirements/documentation.txt new file mode 100644 index 000000000..57304cb53 --- /dev/null +++ b/requirements/documentation.txt @@ -0,0 +1,2 @@ +docutils==0.22.4 +pdoc3==0.11.6 diff --git a/requirements/optional.txt b/requirements/optional.txt new file mode 100644 index 000000000..48c9a630d --- /dev/null +++ b/requirements/optional.txt @@ -0,0 +1,16 @@ +# pip install -r requirements/optional.txt +# async modules depend on aiohttp +aiodns>1.0 +# We recommend using 3.7.1+ for RTMClient +# https://github.com/slackapi/python-slack-sdk/issues/912 +aiohttp>=3.7.3,<4 +# used only under slack_sdk/*_store +boto3<=2 +# InstallationStore/OAuthStateStore +# Since v3.20, we no longer support SQLAlchemy 1.3 or older. +# If you need to use a legacy version, please add our v3.19.5 code to your project. +SQLAlchemy>=1.4,<3 +# Socket Mode +# websockets 9 is not compatible with Python 3.10 +websockets>=9.1,<16 +websocket-client>=1,<2 diff --git a/requirements/testing.txt b/requirements/testing.txt new file mode 100644 index 000000000..9e1a3fd67 --- /dev/null +++ b/requirements/testing.txt @@ -0,0 +1,14 @@ +# pip install -r requirements/testing.txt +aiohttp<4 # used for a WebSocket server mock +pytest>=7.0.1,<9 +pytest-asyncio<2 # for async +pytest-cov>=2,<8 +click==8.0.4 # black is affected by https://github.com/pallets/click/issues/2225 +psutil>=6.0.0,<8 +# used only under slack_sdk/*_store +boto3<=2 +# For AWS tests +moto>=4.0.13,<6 +# For AsyncSQLAlchemy tests +greenlet<=4 +aiosqlite<=1 diff --git a/requirements/tools.txt b/requirements/tools.txt new file mode 100644 index 000000000..39946a86d --- /dev/null +++ b/requirements/tools.txt @@ -0,0 +1,7 @@ +mypy<=1.19.0; +# while flake8 5.x have issues with Python 3.12, flake8 6.x requires Python >= 3.8.1, +# so 5.x should be kept in order to stay compatible with Python 3.7/3.8 +flake8>=5.0.4,<8 +# Don't change this version without running CI builds; +# The latest version may not be available for older Python runtime +black==24.3.0; diff --git a/scripts/build_pypi_package.sh b/scripts/build_pypi_package.sh new file mode 100755 index 000000000..76b3e8e41 --- /dev/null +++ b/scripts/build_pypi_package.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +script_dir=`dirname $0` +cd ${script_dir}/.. +rm -rf ./slack_sdk.egg-info + +pip install -U pip && \ + pip install -U twine build && \ + rm -rf dist/ build/ slack_sdk.egg-info/ && \ + python -m build --sdist --wheel && \ + twine check dist/* diff --git a/scripts/codegen.py b/scripts/codegen.py new file mode 100644 index 000000000..3633faf35 --- /dev/null +++ b/scripts/codegen.py @@ -0,0 +1,153 @@ +import argparse +import sys + +parser = argparse.ArgumentParser() +parser.add_argument("-p", "--path", help="Path to the project source code.", type=str) +if len(sys.argv) == 1: + parser.print_help(sys.stderr) + sys.exit(1) +args = parser.parse_args() + +header = ( + "# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n" + "#\n" + "# *** DO NOT EDIT THIS FILE ***\n" + "#\n" + "# 1) Modify slack_sdk/web/client.py\n" + "# 2) Run `python scripts/codegen.py`\n" + "# 3) Run `black slack_sdk/`\n" + "#\n" + "# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n" + "\n" +) + +with open(f"{args.path}/slack_sdk/web/client.py", "r") as original: + source = original.read() + import re + + async_source = header + source + async_source = re.sub(" def ", " async def ", async_source) + async_source = re.sub("from asyncio import Future\n", "", async_source) + async_source = re.sub(r"return self.api_call\(", "return await self.api_call(", async_source) + async_source = re.sub("-> SlackResponse", "-> AsyncSlackResponse", async_source) + async_source = re.sub( + "from .base_client import BaseClient, SlackResponse", + "from .async_base_client import AsyncBaseClient, AsyncSlackResponse", + async_source, + ) + async_source = re.sub( + r"class WebClient\(BaseClient\):", + "class AsyncWebClient(AsyncBaseClient):", + async_source, + ) + async_source = re.sub( + "from slack_sdk import WebClient", + "from slack_sdk.web.async_client import AsyncWebClient", + async_source, + ) + async_source = re.sub(r"= WebClient\(", "= AsyncWebClient(", async_source) + async_source = re.sub( + "from slack_sdk.web.chat_stream import ChatStream", + "from slack_sdk.web.async_chat_stream import AsyncChatStream", + async_source, + ) + async_source = re.sub(r"ChatStream:", "AsyncChatStream:", async_source) + async_source = re.sub(r"ChatStream\(", "AsyncChatStream(", async_source) + async_source = re.sub( + r" client.chat_stream\(", + " await client.chat_stream(", + async_source, + ) + async_source = re.sub( + r" streamer.append\(", + " await streamer.append(", + async_source, + ) + async_source = re.sub( + r" streamer.stop\(", + " await streamer.stop(", + async_source, + ) + async_source = re.sub( + r" self.files_getUploadURLExternal\(", + " await self.files_getUploadURLExternal(", + async_source, + ) + async_source = re.sub( + r" self._upload_file\(", + " await self._upload_file(", + async_source, + ) + async_source = re.sub( + r" self.files_completeUploadExternal\(", + " await self.files_completeUploadExternal(", + async_source, + ) + async_source = re.sub( + r" self.files_info\(", + " await self.files_info(", + async_source, + ) + async_source = re.sub( + "_attach_full_file_metadata", + "_attach_full_file_metadata_async", + async_source, + ) + async_source = re.sub( + r" _attach_full_file_metadata_async\(", + " await _attach_full_file_metadata_async(", + async_source, + ) + with open(f"{args.path}/slack_sdk/web/async_client.py", "w") as output: + output.write(async_source) + + legacy_source = header + "from asyncio import Future\n" + source + legacy_source = re.sub("-> SlackResponse", "-> Union[Future, SlackResponse]", legacy_source) + legacy_source = re.sub( + "from .base_client import BaseClient, SlackResponse", + "from .legacy_base_client import LegacyBaseClient, SlackResponse", + legacy_source, + ) + legacy_source = re.sub( + r"class WebClient\(BaseClient\):", + "class LegacyWebClient(LegacyBaseClient):", + legacy_source, + ) + legacy_source = re.sub( + "from slack_sdk import WebClient", + "from slack_sdk.web.legacy_client import LegacyWebClient", + legacy_source, + ) + legacy_source = re.sub(r"= WebClient\(", "= LegacyWebClient(", legacy_source) + legacy_source = re.sub(r"^from slack_sdk.web.chat_stream import ChatStream\n", "", legacy_source, flags=re.MULTILINE) + legacy_source = re.sub(r"(?s)def chat_stream.*?(?=def)", "", legacy_source) + with open(f"{args.path}/slack_sdk/web/legacy_client.py", "w") as output: + output.write(legacy_source) + +with open(f"{args.path}/slack_sdk/web/chat_stream.py", "r") as original: + source = original.read() + import re + + async_source = header + source + async_source = re.sub( + "from slack_sdk.web.slack_response import SlackResponse", + "from slack_sdk.web.async_slack_response import AsyncSlackResponse", + async_source, + ) + async_source = re.sub( + r"from slack_sdk import WebClient", + "from slack_sdk.web.async_client import AsyncWebClient", + async_source, + ) + async_source = re.sub("class ChatStream", "class AsyncChatStream", async_source) + async_source = re.sub('"WebClient"', '"AsyncWebClient"', async_source) + async_source = re.sub(r"Optional\[SlackResponse\]", "Optional[AsyncSlackResponse]", async_source) + async_source = re.sub(r"SlackResponse ", "AsyncSlackResponse ", async_source) + async_source = re.sub(r"SlackResponse:", "AsyncSlackResponse:", async_source) + async_source = re.sub(r"def append\(", "async def append(", async_source) + async_source = re.sub(r"def stop\(", "async def stop(", async_source) + async_source = re.sub(r"def _flush_buffer\(", "async def _flush_buffer(", async_source) + async_source = re.sub("self._client.chat_", "await self._client.chat_", async_source) + async_source = re.sub("self._flush_buffer", "await self._flush_buffer", async_source) + with open(f"{args.path}/slack_sdk/web/async_chat_stream.py", "w") as output: + output.write(async_source) diff --git a/scripts/deploy_to_test_pypi.sh b/scripts/deploy_to_test_pypi.sh new file mode 100755 index 000000000..54ed0e04b --- /dev/null +++ b/scripts/deploy_to_test_pypi.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +script_dir=`dirname $0` +cd ${script_dir}/.. +rm -rf ./slack_sdk.egg-info + +pip install -U pip && \ + pip install -U twine build && \ + rm -rf dist/ build/ slack_sdk.egg-info/ && \ + python -m build --sdist --wheel && \ + twine check dist/* && \ + twine upload --repository testpypi dist/* diff --git a/scripts/format.sh b/scripts/format.sh new file mode 100755 index 000000000..56ca68077 --- /dev/null +++ b/scripts/format.sh @@ -0,0 +1,13 @@ +#!/bin/bash +# ./scripts/format.sh + +script_dir=`dirname $0` +cd ${script_dir}/.. + +if [[ "$1" != "--no-install" ]]; then + export PIP_REQUIRE_VIRTUALENV=1 + pip install -U pip + pip install -U -r requirements/tools.txt +fi + +black slack/ slack_sdk/ tests/ integration_tests/ diff --git a/scripts/generate_api_docs.sh b/scripts/generate_api_docs.sh new file mode 100755 index 000000000..c2bb260ab --- /dev/null +++ b/scripts/generate_api_docs.sh @@ -0,0 +1,16 @@ +#!/bin/bash +# Generate API documents from the latest source code + +script_dir=`dirname $0` +cd ${script_dir}/.. + +pip install -U -r requirements/documentation.txt +pip install -U -r requirements/optional.txt + +rm -rf docs/reference + +HOME="\$HOME" pdoc slack_sdk --html -o docs/reference +cp -R docs/reference/slack_sdk/* docs/reference/ +rm -rf docs/reference/slack_sdk + +open docs/reference/index.html diff --git a/scripts/lint.sh b/scripts/lint.sh new file mode 100755 index 000000000..8fac67888 --- /dev/null +++ b/scripts/lint.sh @@ -0,0 +1,14 @@ + +#!/bin/bash +# ./scripts/lint.sh + +script_dir=`dirname $0` +cd ${script_dir}/.. + +if [[ "$1" != "--no-install" ]]; then + pip install -U pip + pip install -U -r requirements/tools.txt +fi + +black --check slack/ slack_sdk/ tests/ integration_tests/ +flake8 slack/ slack_sdk/ diff --git a/scripts/run_integration_tests.sh b/scripts/run_integration_tests.sh new file mode 100755 index 000000000..1a6f254cb --- /dev/null +++ b/scripts/run_integration_tests.sh @@ -0,0 +1,20 @@ +#!/bin/bash +# Run all the tests or a single test +# all: ./scripts/run_integration_tests.sh +# single: ./scripts/run_integration_tests.sh integration_tests/web/test_async_web_client.py + +set -e + +script_dir=`dirname $0` +cd ${script_dir}/.. + +pip install -U pip +pip install -U -r requirements/testing.txt \ + -U -r requirements/optional.txt \ + -U -r requirements/tools.txt + +echo "Generating code ..." && python scripts/codegen.py --path . +echo "Running black (code formatter) ..." && ./scripts/format.sh --no-install + +test_target="${1:-tests/integration_tests/}" +PYTHONPATH=$PWD:$PYTHONPATH pytest $test_target diff --git a/scripts/run_mypy.sh b/scripts/run_mypy.sh new file mode 100755 index 000000000..cc1146f15 --- /dev/null +++ b/scripts/run_mypy.sh @@ -0,0 +1,14 @@ +#!/bin/bash +# ./scripts/run_mypy.sh + +set -e + +script_dir=$(dirname $0) +cd ${script_dir}/.. + +pip install -U pip setuptools wheel +pip install -U -r requirements/testing.txt \ + -U -r requirements/optional.txt \ + -U -r requirements/tools.txt + +mypy --config-file pyproject.toml diff --git a/scripts/run_unit_tests.sh b/scripts/run_unit_tests.sh new file mode 100755 index 000000000..c8ab0af78 --- /dev/null +++ b/scripts/run_unit_tests.sh @@ -0,0 +1,21 @@ +#!/bin/bash +# Run all the tests or a single test +# all: ./scripts/run_unit_tests.sh +# single: ./scripts/run_unit_tests.sh tests/slack_sdk_async/web/test_web_client_coverage.py + +set -e + +script_dir=`dirname $0` +cd ${script_dir}/.. + +pip install -U pip +pip install -U -r requirements/testing.txt \ + -U -r requirements/optional.txt \ + -U -r requirements/tools.txt + +echo "Generating code ..." && python scripts/codegen.py --path . +echo "Running black (code formatter) ..." && ./scripts/format.sh --no-install + +echo "Running tests ..." +test_target="${1:-tests/}" +PYTHONPATH=$PWD:$PYTHONPATH pytest $test_target diff --git a/scripts/run_validation.sh b/scripts/run_validation.sh new file mode 100755 index 000000000..366f0d321 --- /dev/null +++ b/scripts/run_validation.sh @@ -0,0 +1,21 @@ +#!/bin/bash +# all: ./scripts/run_validation.sh +# single: ./scripts/run_validation.sh tests/slack_sdk_async/web/test_web_client_coverage.py + +set -e + +script_dir=`dirname $0` +cd ${script_dir}/.. + +pip install -U -r requirements/testing.txt \ + -U -r requirements/optional.txt \ + -U -r requirements/tools.txt + +echo "Generating code ..." && python scripts/codegen.py --path . +echo "Running black (code formatter) ..." && ./scripts/format.sh --no-install + +echo "Running linting checks ..." && ./scripts/lint.sh --no-install + +echo "Running tests with coverage reporting ..." +test_target="${1:-tests/}" +PYTHONPATH=$PWD:$PYTHONPATH pytest --cov-report=xml --cov=slack_sdk/ $test_target diff --git a/scripts/uninstall_all.sh b/scripts/uninstall_all.sh new file mode 100755 index 000000000..71a1e51f6 --- /dev/null +++ b/scripts/uninstall_all.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +# remove slack-sdk without a version specifier so that local builds are cleaned up +pip uninstall -y slack-sdk +# collect all installed packages +PACKAGES=$(pip freeze | grep -v "^-e" | sed 's/@.*//' | sed 's/\=\=.*//') +# uninstall packages without exiting on a failure +for package in $PACKAGES; do + pip uninstall -y $package +done diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 000000000..384126b63 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,5 @@ +; Legacy package configuration, prefer pyproject.toml over setup.cfg or setup.py +[metadata] +url=https://github.com/slackapi/python-slack-sdk +author=Slack Technologies, LLC +author_email=opensource@slack.com diff --git a/setup.py b/setup.py deleted file mode 100644 index 2eda2bd00..000000000 --- a/setup.py +++ /dev/null @@ -1,14 +0,0 @@ -from setuptools import setup - -setup(name='slackclient', - version='0.15', - description='Python client for Slack.com', - url='http://github.com/slackhq/python-slackclient', - author='Ryan Huber', - author_email='ryan@slack-corp.com', - license='MIT', - packages=['slackclient'], - install_requires=[ - 'websocket-client', - ], - zip_safe=False) diff --git a/slack/__init__.py b/slack/__init__.py new file mode 100644 index 000000000..bad341401 --- /dev/null +++ b/slack/__init__.py @@ -0,0 +1,14 @@ +import logging +from logging import NullHandler +from slack import deprecation + +deprecation.show_message(__name__, "slack_sdk.web/webhook/rtm") + +from slack_sdk.rtm import RTMClient # noqa +from slack_sdk.web.async_client import AsyncWebClient # noqa +from slack_sdk.web.legacy_client import LegacyWebClient as WebClient # noqa +from slack_sdk.webhook.async_client import AsyncWebhookClient # noqa +from slack_sdk.webhook.client import WebhookClient # noqa + +# Set default logging handler to avoid "No handler found" warnings. +logging.getLogger(__name__).addHandler(NullHandler()) diff --git a/slack/deprecation.py b/slack/deprecation.py new file mode 100644 index 000000000..8c5e8207b --- /dev/null +++ b/slack/deprecation.py @@ -0,0 +1,14 @@ +import os +import warnings + + +def show_message(old: str, new: str) -> None: + skip_deprecation = os.environ.get("SLACKCLIENT_SKIP_DEPRECATION") # for unit tests etc. + if skip_deprecation: + return + + message = ( + f"{old} package is deprecated. Please use {new} package instead. " + "For more info, go to https://docs.slack.dev/tools/python-slack-sdk/v3-migration/" + ) + warnings.warn(message) diff --git a/slack/errors.py b/slack/errors.py new file mode 100644 index 000000000..7d5f13090 --- /dev/null +++ b/slack/errors.py @@ -0,0 +1,10 @@ +from slack_sdk.errors import BotUserAccessError # noqa +from slack_sdk.errors import SlackApiError # noqa +from slack_sdk.errors import SlackClientError # noqa +from slack_sdk.errors import SlackClientNotConnectedError # noqa +from slack_sdk.errors import SlackObjectFormationError # noqa +from slack_sdk.errors import SlackRequestError # noqa + +from slack import deprecation + +deprecation.show_message(__name__, "slack_sdk.errors") diff --git a/slack/py.typed b/slack/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/slack/rtm/__init__.py b/slack/rtm/__init__.py new file mode 100644 index 000000000..1d3d1a05e --- /dev/null +++ b/slack/rtm/__init__.py @@ -0,0 +1,6 @@ +from slack_sdk.rtm import RTMClient # noqa +from slack_sdk.web.legacy_client import LegacyWebClient as WebClient # noqa + +from slack import deprecation + +deprecation.show_message(__name__, "slack_sdk.web/rtm") diff --git a/slack/rtm/client.py b/slack/rtm/client.py new file mode 100644 index 000000000..cff4d0bd0 --- /dev/null +++ b/slack/rtm/client.py @@ -0,0 +1,6 @@ +from slack_sdk.rtm import RTMClient # noqa +from slack_sdk.web.legacy_client import LegacyWebClient as WebClient # noqa + +from slack import deprecation + +deprecation.show_message(__name__, "slack_sdk.rtm.client") diff --git a/slack/signature/__init__.py b/slack/signature/__init__.py new file mode 100644 index 000000000..cf3fefcf7 --- /dev/null +++ b/slack/signature/__init__.py @@ -0,0 +1,5 @@ +from slack_sdk.signature import SignatureVerifier # noqa + +from slack import deprecation + +deprecation.show_message(__name__, "slack_sdk.signature") diff --git a/slack/signature/verifier.py b/slack/signature/verifier.py new file mode 100644 index 000000000..da9c6ef5e --- /dev/null +++ b/slack/signature/verifier.py @@ -0,0 +1,71 @@ +import hashlib +import hmac +from time import time +from typing import Dict, Optional, Union + + +class Clock: + @staticmethod + def now() -> float: + return time() + + +class SignatureVerifier: + def __init__(self, signing_secret: str, clock: Clock = Clock()): + """Slack request signature verifier + + Slack signs its requests using a secret that's unique to your app. + With the help of signing secrets, your app can more confidently verify + whether requests from us are authentic. + https://docs.slack.dev/authentication/verifying-requests-from-slack/ + """ + self.signing_secret = signing_secret + self.clock = clock + + def is_valid_request( + self, + body: Union[str, bytes], + headers: Dict[str, str], + ) -> bool: + """Verifies if the given signature is valid""" + if headers is None: + return False + normalized_headers = {k.lower(): v for k, v in headers.items()} + return self.is_valid( + body=body, + timestamp=normalized_headers.get("x-slack-request-timestamp", None), + signature=normalized_headers.get("x-slack-signature", None), + ) + + def is_valid( + self, + body: Union[str, bytes], + timestamp: str, + signature: str, + ) -> bool: + """Verifies if the given signature is valid""" + if timestamp is None or signature is None: + return False + + if abs(self.clock.now() - int(timestamp)) > 60 * 5: + return False + + calculated_signature = self.generate_signature(timestamp=timestamp, body=body) + if calculated_signature is None: + return False + return hmac.compare_digest(calculated_signature, signature) + + def generate_signature(self, *, timestamp: str, body: Union[str, bytes]) -> Optional[str]: + """Generates a signature""" + if timestamp is None: + return None + if body is None: + body = "" + if isinstance(body, bytes): + body = body.decode("utf-8") + + format_req = str.encode(f"v0:{timestamp}:{body}") + encoded_secret = str.encode(self.signing_secret) + request_hash = hmac.new(encoded_secret, format_req, hashlib.sha256).hexdigest() + calculated_signature = f"v0={request_hash}" + return calculated_signature diff --git a/slack/version.py b/slack/version.py new file mode 100644 index 000000000..a51188cef --- /dev/null +++ b/slack/version.py @@ -0,0 +1 @@ +from slack_sdk.version import __version__ # noqa diff --git a/slack/web/__init__.py b/slack/web/__init__.py new file mode 100644 index 000000000..f61f4b611 --- /dev/null +++ b/slack/web/__init__.py @@ -0,0 +1,11 @@ +import slack_sdk.version as slack_version # noqa +from slack import deprecation +from slack_sdk.web.async_client import AsyncSlackResponse # noqa +from slack_sdk.web.async_client import AsyncWebClient # noqa +from slack_sdk.web.internal_utils import _to_0_or_1_if_bool # noqa +from slack_sdk.web.internal_utils import convert_bool_to_0_or_1 # noqa +from slack_sdk.web.internal_utils import get_user_agent # noqa +from slack_sdk.web.legacy_client import LegacyWebClient as WebClient # noqa +from slack_sdk.web.slack_response import SlackResponse # noqa + +deprecation.show_message(__name__, "slack_sdk.web") diff --git a/slack/web/async_base_client.py b/slack/web/async_base_client.py new file mode 100644 index 000000000..c0cc3f962 --- /dev/null +++ b/slack/web/async_base_client.py @@ -0,0 +1,165 @@ +import logging +from ssl import SSLContext +from typing import Optional, Union, Dict + +import aiohttp +from aiohttp import FormData + +from slack.web import convert_bool_to_0_or_1, get_user_agent +from slack.web.async_internal_utils import ( + _build_req_args, + _get_url, + _files_to_data, + _request_with_session, +) +from slack.web.async_slack_response import AsyncSlackResponse +from slack.web.deprecation import show_2020_01_deprecation + + +class AsyncBaseClient: + BASE_URL = "https://slack.com/api/" + + def __init__( + self, + token: Optional[str] = None, + base_url: str = BASE_URL, + timeout: int = 30, + ssl: Optional[SSLContext] = None, + proxy: Optional[str] = None, + session: Optional[aiohttp.ClientSession] = None, + trust_env_in_session: bool = False, + headers: Optional[dict] = None, + user_agent_prefix: Optional[str] = None, + user_agent_suffix: Optional[str] = None, + ): + self.token = None if token is None else token.strip() + self.base_url = base_url + self.timeout = timeout + self.ssl = ssl + self.proxy = proxy + self.session = session + # https://github.com/slackapi/python-slack-sdk/issues/738 + self.trust_env_in_session = trust_env_in_session + self.headers = headers or {} + self.headers["User-Agent"] = get_user_agent(user_agent_prefix, user_agent_suffix) + self._logger = logging.getLogger(__name__) + + async def api_call( # skipcq: PYL-R1710 + self, + api_method: str, + *, + http_verb: str = "POST", + files: dict = None, + data: Union[dict, FormData] = None, + params: dict = None, + json: dict = None, # skipcq: PYL-W0621 + headers: dict = None, + auth: dict = None, + ) -> AsyncSlackResponse: + """Create a request and execute the API call to Slack. + + Args: + api_method (str): The target Slack API method. + e.g. 'chat.postMessage' + http_verb (str): HTTP Verb. e.g. 'POST' + files (dict): Files to multipart upload. + e.g. {image OR file: file_object OR file_path} + data: The body to attach to the request. If a dictionary is + provided, form-encoding will take place. + e.g. {'key1': 'value1', 'key2': 'value2'} + params (dict): The URL parameters to append to the URL. + e.g. {'key1': 'value1', 'key2': 'value2'} + json (dict): JSON for the body to attach to the request + (if files or data is not specified). + e.g. {'key1': 'value1', 'key2': 'value2'} + headers (dict): Additional request headers + auth (dict): A dictionary that consists of client_id and client_secret + + Returns: + (AsyncSlackResponse) + The server's response to an HTTP request. Data + from the response can be accessed like a dict. + If the response included 'next_cursor' it can + be iterated on to execute subsequent requests. + + Raises: + SlackApiError: The following Slack API call failed: + 'chat.postMessage'. + SlackRequestError: Json data can only be submitted as + POST requests. + """ + + api_url = _get_url(self.base_url, api_method) + headers = headers or {} + headers.update(self.headers) + + req_args = _build_req_args( + token=self.token, + http_verb=http_verb, + files=files, + data=data, + params=params, + json=json, # skipcq: PYL-W0621 + headers=headers, + auth=auth, + ssl=self.ssl, + proxy=self.proxy, + ) + + show_2020_01_deprecation(api_method) + + return await self._send( + http_verb=http_verb, + api_url=api_url, + req_args=req_args, + ) + + async def _send(self, http_verb: str, api_url: str, req_args: dict) -> AsyncSlackResponse: + """Sends the request out for transmission. + + Args: + http_verb (str): The HTTP verb. e.g. 'GET' or 'POST'. + api_url (str): The Slack API url. e.g. 'https://slack.com/api/chat.postMessage' + req_args (dict): The request arguments to be attached to the request. + e.g. + { + json: { + 'attachments': [{"pretext": "pre-hello", "text": "text-world"}], + 'channel': '#random' + } + } + Returns: + The response parsed into a AsyncSlackResponse object. + """ + open_files = _files_to_data(req_args) + try: + if "params" in req_args: + # True/False -> "1"/"0" + req_args["params"] = convert_bool_to_0_or_1(req_args["params"]) + + res = await self._request(http_verb=http_verb, api_url=api_url, req_args=req_args) + finally: + for f in open_files: + f.close() + + data = { + "client": self, + "http_verb": http_verb, + "api_url": api_url, + "req_args": req_args, + } + return AsyncSlackResponse(**{**data, **res}).validate() + + async def _request(self, *, http_verb, api_url, req_args) -> Dict[str, any]: + """Submit the HTTP request with the running session or a new session. + Returns: + A dictionary of the response data. + """ + return await _request_with_session( + current_session=self.session, + timeout=self.timeout, + logger=self._logger, + http_verb=http_verb, + api_url=api_url, + req_args=req_args, + ) diff --git a/slack/web/async_client.py b/slack/web/async_client.py new file mode 100644 index 000000000..7258c5eb2 --- /dev/null +++ b/slack/web/async_client.py @@ -0,0 +1,16 @@ +# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +# +# *** DO NOT EDIT THIS FILE *** +# +# 1) Modify slack/web/client.py +# 2) Run `python scripts/codegen.py` +# 3) Run `black slack_sdk/` +# +# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +from slack import deprecation +from slack_sdk.web.legacy_client import LegacyWebClient as WebClient # noqa +from slack_sdk.web.async_client import AsyncWebClient # noqa +from slack_sdk.web.async_client import AsyncSlackResponse # noqa + +deprecation.show_message(__name__, "slack_sdk.web.client") diff --git a/slack/web/async_internal_utils.py b/slack/web/async_internal_utils.py new file mode 100644 index 000000000..1148dc9e7 --- /dev/null +++ b/slack/web/async_internal_utils.py @@ -0,0 +1,199 @@ +import asyncio +import json +from asyncio import AbstractEventLoop +from logging import Logger +from ssl import SSLContext +from typing import Union, Optional, BinaryIO, List, Dict +from urllib.parse import urljoin + +import aiohttp +from aiohttp import FormData, BasicAuth, ClientSession + +from slack.errors import SlackRequestError, SlackApiError +from slack.web import get_user_agent + + +def _get_event_loop() -> AbstractEventLoop: + """Retrieves the event loop or creates a new one.""" + try: + return asyncio.get_event_loop() + except RuntimeError: + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + return loop + + +def _get_url(base_url: str, api_method: str) -> str: + """Joins the base Slack URL and an API method to form an absolute URL. + + Args: + base_url (str): The base URL + api_method (str): The Slack Web API method. e.g. 'chat.postMessage' + + Returns: + The absolute API URL. + e.g. 'https://slack.com/api/chat.postMessage' + """ + return urljoin(base_url, api_method) + + +def _get_headers( + *, + headers: dict, + token: Optional[str], + has_json: bool, + has_files: bool, + request_specific_headers: Optional[dict], +) -> Dict[str, str]: + """Constructs the headers need for a request. + Args: + has_json (bool): Whether or not the request has json. + has_files (bool): Whether or not the request has files. + request_specific_headers (dict): Additional headers specified by the user for a specific request. + + Returns: + The headers dictionary. + e.g. { + 'Content-Type': 'application/json;charset=utf-8', + 'Authorization': 'Bearer xoxb-1234-1243', + 'User-Agent': 'Python/3.7.17 slack/2.1.0 Darwin/17.7.0' + } + """ + final_headers = { + "User-Agent": get_user_agent(), + "Content-Type": "application/x-www-form-urlencoded", + } + + if token: + final_headers.update({"Authorization": "Bearer {}".format(token)}) + if headers is None: + headers = {} + + # Merge headers specified at client initialization. + final_headers.update(headers) + + # Merge headers specified for a specific request. e.g. oauth.access + if request_specific_headers: + final_headers.update(request_specific_headers) + + if has_json: + final_headers.update({"Content-Type": "application/json;charset=utf-8"}) + + if has_files: + # These are set automatically by the aiohttp library. + final_headers.pop("Content-Type", None) + + return final_headers + + +def _build_req_args( + *, + token: Optional[str], + http_verb: str, + files: dict, + data: Union[dict, FormData], + params: dict, + json: dict, # skipcq: PYL-W0621 + headers: dict, + auth: dict, + ssl: Optional[SSLContext], + proxy: Optional[str], +) -> dict: + has_json = json is not None + has_files = files is not None + if has_json and http_verb != "POST": + msg = "Json data can only be submitted as POST requests. GET requests should use the 'params' argument." + raise SlackRequestError(msg) + + if auth: + auth = BasicAuth(auth["client_id"], auth["client_secret"]) + + if data is not None and isinstance(data, dict): + data = {k: v for k, v in data.items() if v is not None} + if files is not None and isinstance(files, dict): + files = {k: v for k, v in files.items() if v is not None} + if params is not None and isinstance(params, dict): + params = {k: v for k, v in params.items() if v is not None} + + token: Optional[str] = token + if params is not None and "token" in params: + token = params.pop("token") + if json is not None and "token" in json: + token = json.pop("token") + req_args = { + "headers": _get_headers( + headers=headers, + token=token, + has_json=has_json, + has_files=has_files, + request_specific_headers=headers, + ), + "data": data, + "files": files, + "params": params, + "json": json, + "ssl": ssl, + "proxy": proxy, + "auth": auth, + } + return req_args + + +def _files_to_data(req_args: dict) -> List[BinaryIO]: + open_files = [] + files = req_args.pop("files", None) + if files is not None: + for k, v in files.items(): + if isinstance(v, str): + f = open(v.encode("utf-8", "ignore"), "rb") + open_files.append(f) + req_args["data"].update({k: f}) + else: + req_args["data"].update({k: v}) + return open_files + + +async def _request_with_session( + *, + current_session: Optional[ClientSession], + timeout: int, + logger: Logger, + http_verb: str, + api_url: str, + req_args: dict, +) -> Dict[str, any]: + """Submit the HTTP request with the running session or a new session. + Returns: + A dictionary of the response data. + """ + session = None + use_running_session = current_session and not current_session.closed + if use_running_session: + session = current_session + else: + session = aiohttp.ClientSession( + timeout=aiohttp.ClientTimeout(total=timeout), + auth=req_args.pop("auth", None), + ) + + response = None + try: + async with session.request(http_verb, api_url, **req_args) as res: + data = {} + try: + data = await res.json() + except aiohttp.ContentTypeError: + logger.debug(f"No response data returned from the following API call: {api_url}.") + except json.decoder.JSONDecodeError as e: + message = f"Failed to parse the response body: {str(e)}" + raise SlackApiError(message, res) + + response = { + "data": data, + "headers": res.headers, + "status_code": res.status, + } + finally: + if not use_running_session: + await session.close() + return response diff --git a/slack/web/async_slack_response.py b/slack/web/async_slack_response.py new file mode 100644 index 000000000..150bc519e --- /dev/null +++ b/slack/web/async_slack_response.py @@ -0,0 +1,187 @@ +"""A Python module for interacting and consuming responses from Slack.""" + +import logging + +import slack.errors as e +from slack.web.internal_utils import _next_cursor_is_present + + +class AsyncSlackResponse: + """An iterable container of response data. + + Attributes: + data (dict): The json-encoded content of the response. Along + with the headers and status code information. + + Methods: + validate: Check if the response from Slack was successful. + get: Retrieves any key from the response data. + next: Retrieves the next portion of results, + if 'next_cursor' is present. + + Example: + ```python + import os + import slack + + client = slack.AsyncWebClient(token=os.environ['SLACK_API_TOKEN']) + + response1 = await client.auth_revoke(test='true') + assert not response1['revoked'] + + response2 = await client.auth_test() + assert response2.get('ok', False) + + users = [] + async for page in await client.users_list(limit=2): + users = users + page['members'] + ``` + + Note: + Some responses return collections of information + like channel and user lists. If they do it's likely + that you'll only receive a portion of results. This + object allows you to iterate over the response which + makes subsequent API requests until your code hits + 'break' or there are no more results to be found. + + Any attributes or methods prefixed with _underscores are + intended to be "private" internal use only. They may be changed or + removed at anytime. + """ + + def __init__( + self, + *, + client, # AsyncWebClient + http_verb: str, + api_url: str, + req_args: dict, + data: dict, + headers: dict, + status_code: int, + ): + self.http_verb = http_verb + self.api_url = api_url + self.req_args = req_args + self.data = data + self.headers = headers + self.status_code = status_code + self._initial_data = data + self._iteration = None # for __iter__ & __next__ + self._client = client + self._logger = logging.getLogger(__name__) + + def __str__(self): + """Return the Response data if object is converted to a string.""" + if isinstance(self.data, bytes): + raise ValueError("As the response.data is binary data, this operation is unsupported") + return f"{self.data}" + + def __contains__(self, key: str) -> bool: + return self.get(key) is not None + + def __getitem__(self, key): + """Retrieves any key from the data store. + + Note: + This is implemented so users can reference the + SlackResponse object like a dictionary. + e.g. response["ok"] + + Returns: + The value from data or None. + """ + if isinstance(self.data, bytes): + raise ValueError("As the response.data is binary data, this operation is unsupported") + if self.data is None: + raise ValueError("As the response.data is empty, this operation is unsupported") + return self.data.get(key, None) + + def __aiter__(self): + """Enables the ability to iterate over the response. + It's required async-for the iterator protocol. + + Note: + This enables Slack cursor-based pagination. + + Returns: + (AsyncSlackResponse) self + """ + self._iteration = 0 + self.data = self._initial_data + return self + + async def __anext__(self): + """Retrieves the next portion of results, if 'next_cursor' is present. + + Note: + Some responses return collections of information + like channel and user lists. If they do it's likely + that you'll only receive a portion of results. This + method allows you to iterate over the response until + your code hits 'break' or there are no more results + to be found. + + Returns: + (AsyncSlackResponse) self + With the new response data now attached to this object. + + Raises: + SlackApiError: If the request to the Slack API failed. + StopAsyncIteration: If 'next_cursor' is not present or empty. + """ + self._iteration += 1 + if self._iteration == 1: + return self + if _next_cursor_is_present(self.data): # skipcq: PYL-R1705 + params = self.req_args.get("params", {}) + if params is None: + params = {} + params.update({"cursor": self.data["response_metadata"]["next_cursor"]}) + self.req_args.update({"params": params}) + + response = await self._client._request( # skipcq: PYL-W0212 + http_verb=self.http_verb, + api_url=self.api_url, + req_args=self.req_args, + ) + + self.data = response["data"] + self.headers = response["headers"] + self.status_code = response["status_code"] + return self.validate() + else: + raise StopAsyncIteration + + def get(self, key, default=None): + """Retrieves any key from the response data. + + Note: + This is implemented so users can reference the + SlackResponse object like a dictionary. + e.g. response.get("ok", False) + + Returns: + The value from data or the specified default. + """ + if isinstance(self.data, bytes): + raise ValueError("As the response.data is binary data, this operation is unsupported") + if self.data is None: + return None + return self.data.get(key, default) + + def validate(self): + """Check if the response from Slack was successful. + + Returns: + (AsyncSlackResponse) + This method returns it's own object. e.g. 'self' + + Raises: + SlackApiError: The request to the Slack API failed. + """ + if self.status_code == 200 and self.data and self.data.get("ok", False): + return self + msg = "The request to the Slack API failed." + raise e.SlackApiError(message=msg, response=self) diff --git a/slack/web/base_client.py b/slack/web/base_client.py new file mode 100644 index 000000000..28f597411 --- /dev/null +++ b/slack/web/base_client.py @@ -0,0 +1,497 @@ +"""A Python module for interacting with Slack's Web API.""" + +import asyncio +import copy +import hashlib +import hmac +import io +import json +import logging +import mimetypes +import urllib +import uuid +import warnings +from http.client import HTTPResponse +from ssl import SSLContext +from typing import BinaryIO, Dict, List +from typing import Optional, Union +from urllib.error import HTTPError +from urllib.parse import urlencode +from urllib.request import Request, urlopen, OpenerDirector, ProxyHandler, HTTPSHandler + +import aiohttp +from aiohttp import FormData, BasicAuth + +import slack.errors as err +from slack.errors import SlackRequestError +from slack.web import convert_bool_to_0_or_1, get_user_agent +from slack.web.async_internal_utils import ( + _get_event_loop, + _build_req_args, + _get_url, + _files_to_data, + _request_with_session, +) +from slack.web.deprecation import show_2020_01_deprecation +from slack.web.slack_response import SlackResponse + + +class BaseClient: + BASE_URL = "https://slack.com/api/" + + def __init__( + self, + token: Optional[str] = None, + base_url: str = BASE_URL, + timeout: int = 30, + loop: Optional[asyncio.AbstractEventLoop] = None, + ssl: Optional[SSLContext] = None, + proxy: Optional[str] = None, + run_async: bool = False, + use_sync_aiohttp: bool = False, + session: Optional[aiohttp.ClientSession] = None, + headers: Optional[dict] = None, + user_agent_prefix: Optional[str] = None, + user_agent_suffix: Optional[str] = None, + ): + self.token = None if token is None else token.strip() + self.base_url = base_url + self.timeout = timeout + self.ssl = ssl + self.proxy = proxy + self.run_async = run_async + self.use_sync_aiohttp = use_sync_aiohttp + self.session = session + self.headers = headers or {} + self.headers["User-Agent"] = get_user_agent(user_agent_prefix, user_agent_suffix) + self._logger = logging.getLogger(__name__) + self._event_loop = loop + + def api_call( # skipcq: PYL-R1710 + self, + api_method: str, + *, + http_verb: str = "POST", + files: dict = None, + data: Union[dict, FormData] = None, + params: dict = None, + json: dict = None, # skipcq: PYL-W0621 + headers: dict = None, + auth: dict = None, + ) -> Union[asyncio.Future, SlackResponse]: + """Create a request and execute the API call to Slack. + + Args: + api_method (str): The target Slack API method. + e.g. 'chat.postMessage' + http_verb (str): HTTP Verb. e.g. 'POST' + files (dict): Files to multipart upload. + e.g. {image OR file: file_object OR file_path} + data: The body to attach to the request. If a dictionary is + provided, form-encoding will take place. + e.g. {'key1': 'value1', 'key2': 'value2'} + params (dict): The URL parameters to append to the URL. + e.g. {'key1': 'value1', 'key2': 'value2'} + json (dict): JSON for the body to attach to the request + (if files or data is not specified). + e.g. {'key1': 'value1', 'key2': 'value2'} + headers (dict): Additional request headers + auth (dict): A dictionary that consists of client_id and client_secret + + Returns: + (SlackResponse) + The server's response to an HTTP request. Data + from the response can be accessed like a dict. + If the response included 'next_cursor' it can + be iterated on to execute subsequent requests. + + Raises: + SlackApiError: The following Slack API call failed: + 'chat.postMessage'. + SlackRequestError: Json data can only be submitted as + POST requests. + """ + + api_url = _get_url(self.base_url, api_method) + headers = headers or {} + headers.update(self.headers) + + req_args = _build_req_args( + token=self.token, + http_verb=http_verb, + files=files, + data=data, + params=params, + json=json, # skipcq: PYL-W0621 + headers=headers, + auth=auth, + ssl=self.ssl, + proxy=self.proxy, + ) + + show_2020_01_deprecation(api_method) + + if self.run_async or self.use_sync_aiohttp: + if self._event_loop is None: + self._event_loop = _get_event_loop() + + future = asyncio.ensure_future( + self._send(http_verb=http_verb, api_url=api_url, req_args=req_args), + loop=self._event_loop, + ) + if self.run_async: + return future + if self.use_sync_aiohttp: + # Using this is no longer recommended - just keep this for backward-compatibility + return self._event_loop.run_until_complete(future) + else: + return self._sync_send(api_url=api_url, req_args=req_args) + + # ================================================================= + # aiohttp based async WebClient + # ================================================================= + + async def _send(self, http_verb: str, api_url: str, req_args: dict) -> SlackResponse: + """Sends the request out for transmission. + + Args: + http_verb (str): The HTTP verb. e.g. 'GET' or 'POST'. + api_url (str): The Slack API url. e.g. 'https://slack.com/api/chat.postMessage' + req_args (dict): The request arguments to be attached to the request. + e.g. + { + json: { + 'attachments': [{"pretext": "pre-hello", "text": "text-world"}], + 'channel': '#random' + } + } + Returns: + The response parsed into a SlackResponse object. + """ + open_files = _files_to_data(req_args) + try: + if "params" in req_args: + # True/False -> "1"/"0" + req_args["params"] = convert_bool_to_0_or_1(req_args["params"]) + + res = await self._request(http_verb=http_verb, api_url=api_url, req_args=req_args) + finally: + for f in open_files: + f.close() + + data = { + "client": self, + "http_verb": http_verb, + "api_url": api_url, + "req_args": req_args, + "use_sync_aiohttp": self.use_sync_aiohttp, + } + return SlackResponse(**{**data, **res}).validate() + + async def _request(self, *, http_verb, api_url, req_args) -> Dict[str, any]: + """Submit the HTTP request with the running session or a new session. + Returns: + A dictionary of the response data. + """ + return await _request_with_session( + current_session=self.session, + timeout=self.timeout, + logger=self._logger, + http_verb=http_verb, + api_url=api_url, + req_args=req_args, + ) + + # ================================================================= + # urllib based WebClient + # ================================================================= + + def _sync_send(self, api_url, req_args) -> SlackResponse: + params = req_args["params"] if "params" in req_args else None + data = req_args["data"] if "data" in req_args else None + files = req_args["files"] if "files" in req_args else None + _json = req_args["json"] if "json" in req_args else None + headers = req_args["headers"] if "headers" in req_args else None + token = params.get("token") if params and "token" in params else None + auth = req_args["auth"] if "auth" in req_args else None # Basic Auth for oauth.v2.access / oauth.access + if auth is not None: + if isinstance(auth, BasicAuth): + headers["Authorization"] = auth.encode() + elif isinstance(auth, str): + headers["Authorization"] = auth + else: + self._logger.warning(f"As the auth: {auth}: {type(auth)} is unsupported, skipped") + + body_params = {} + if params: + body_params.update(params) + if data: + body_params.update(data) + + return self._urllib_api_call( + token=token, + url=api_url, + query_params={}, + body_params=body_params, + files=files, + json_body=_json, + additional_headers=headers, + ) + + def _request_for_pagination(self, api_url, req_args) -> Dict[str, any]: + """This method is supposed to be used only for SlackResponse pagination + + You can paginate using Python's for iterator as below: + + for response in client.conversations_list(limit=100): + # do something with each response here + """ + response = self._perform_urllib_http_request(url=api_url, args=req_args) + return { + "status_code": int(response["status"]), + "headers": dict(response["headers"]), + "data": json.loads(response["body"]), + } + + def _urllib_api_call( + self, + *, + token: str = None, + url: str, + query_params: Dict[str, str] = {}, + json_body: Dict = {}, + body_params: Dict[str, str] = {}, + files: Dict[str, io.BytesIO] = {}, + additional_headers: Dict[str, str] = {}, + ) -> SlackResponse: + files_to_close: List[BinaryIO] = [] + try: + # True/False -> "1"/"0" + query_params = convert_bool_to_0_or_1(query_params) + body_params = convert_bool_to_0_or_1(body_params) + + if self._logger.level <= logging.DEBUG: + + def convert_params(values: dict) -> dict: + if not values or not isinstance(values, dict): + return {} + return {k: ("(bytes)" if isinstance(v, bytes) else v) for k, v in values.items()} + + headers = {k: "(redacted)" if k.lower() == "authorization" else v for k, v in additional_headers.items()} + self._logger.debug( + f"Sending a request - url: {url}, " + f"query_params: {convert_params(query_params)}, " + f"body_params: {convert_params(body_params)}, " + f"files: {convert_params(files)}, " + f"json_body: {json_body}, " + f"headers: {headers}" + ) + + request_data = {} + if files is not None and isinstance(files, dict) and len(files) > 0: + if body_params: + for k, v in body_params.items(): + request_data.update({k: v}) + + for k, v in files.items(): + if isinstance(v, str): + f: BinaryIO = open(v.encode("utf-8", "ignore"), "rb") + files_to_close.append(f) + request_data.update({k: f}) + elif isinstance(v, (bytearray, bytes)): + request_data.update({k: io.BytesIO(v)}) + else: + request_data.update({k: v}) + + request_headers = self._build_urllib_request_headers( + token=token or self.token, + has_json=json is not None, + has_files=files is not None, + additional_headers=additional_headers, + ) + request_args = { + "headers": request_headers, + "data": request_data, + "params": body_params, + "files": files, + "json": json_body, + } + if query_params: + q = urlencode(query_params) + url = f"{url}&{q}" if "?" in url else f"{url}?{q}" + + response = self._perform_urllib_http_request(url=url, args=request_args) + if response.get("body"): + try: + response_body_data: dict = json.loads(response["body"]) + except json.decoder.JSONDecodeError as e: + message = f"Failed to parse the response body: {str(e)}" + raise err.SlackApiError(message, response) + else: + response_body_data: dict = None + + if query_params: + all_params = copy.copy(body_params) + all_params.update(query_params) + else: + all_params = body_params + request_args["params"] = all_params # for backward-compatibility + + return SlackResponse( + client=self, + http_verb="POST", # you can use POST method for all the Web APIs + api_url=url, + req_args=request_args, + data=response_body_data, + headers=dict(response["headers"]), + status_code=response["status"], + use_sync_aiohttp=False, + ).validate() + finally: + for f in files_to_close: + if not f.closed: + f.close() + + def _perform_urllib_http_request(self, *, url: str, args: Dict[str, Dict[str, any]]) -> Dict[str, any]: + headers = args["headers"] + if args["json"]: + body = json.dumps(args["json"]) + headers["Content-Type"] = "application/json;charset=utf-8" + elif args["data"]: + boundary = f"--------------{uuid.uuid4()}" + sep_boundary = b"\r\n--" + boundary.encode("ascii") + end_boundary = sep_boundary + b"--\r\n" + body = io.BytesIO() + data = args["data"] + for key, value in data.items(): + readable = getattr(value, "readable", None) + if readable and value.readable(): + filename = "Uploaded file" + name_attr = getattr(value, "name", None) + if name_attr: + filename = name_attr.decode("utf-8") if isinstance(name_attr, bytes) else name_attr + if "filename" in data: + filename = data["filename"] + mimetype = mimetypes.guess_type(filename)[0] or "application/octet-stream" + title = ( + f'\r\nContent-Disposition: form-data; name="{key}"; filename="{filename}"\r\n' + + f"Content-Type: {mimetype}\r\n" + ) + value = value.read() + else: + title = f'\r\nContent-Disposition: form-data; name="{key}"\r\n' + value = str(value).encode("utf-8") + body.write(sep_boundary) + body.write(title.encode("utf-8")) + body.write(b"\r\n") + body.write(value) + + body.write(end_boundary) + body = body.getvalue() + headers["Content-Type"] = f"multipart/form-data; boundary={boundary}" + headers["Content-Length"] = len(body) + elif args["params"]: + body = urlencode(args["params"]) + headers["Content-Type"] = "application/x-www-form-urlencoded" + else: + body = None + + if isinstance(body, str): + body = body.encode("utf-8") + + # NOTE: Intentionally ignore the `http_verb` here + # Slack APIs accepts any API method requests with POST methods + try: + # urllib not only opens http:// or https:// URLs, but also ftp:// and file://. + # With this it might be possible to open local files on the executing machine + # which might be a security risk if the URL to open can be manipulated by an external user. + # (BAN-B310) + if url.lower().startswith("http"): + req = Request(method="POST", url=url, data=body, headers=headers) + opener: Optional[OpenerDirector] = None + if self.proxy is not None: + if isinstance(self.proxy, str): + opener = urllib.request.build_opener( + ProxyHandler({"http": self.proxy, "https": self.proxy}), + HTTPSHandler(context=self.ssl), + ) + else: + raise SlackRequestError(f"Invalid proxy detected: {self.proxy} must be a str value") + + # NOTE: BAN-B310 is already checked above + resp: Optional[HTTPResponse] = None + if opener: + resp = opener.open(req, timeout=self.timeout) # skipcq: BAN-B310 + else: + resp = urlopen(req, context=self.ssl, timeout=self.timeout) # skipcq: BAN-B310 + charset = resp.headers.get_content_charset() or "utf-8" + body: str = resp.read().decode(charset) # read the response body here + return {"status": resp.code, "headers": resp.headers, "body": body} + raise SlackRequestError(f"Invalid URL detected: {url}") + except HTTPError as e: + resp = {"status": e.code, "headers": e.headers} + if e.code == 429: + # for compatibility with aiohttp + resp["headers"]["Retry-After"] = resp["headers"]["retry-after"] + + charset = e.headers.get_content_charset() or "utf-8" + body: str = e.read().decode(charset) # read the response body here + resp["body"] = body + return resp + + except Exception as err: + self._logger.error(f"Failed to send a request to Slack API server: {err}") + raise err + + def _build_urllib_request_headers( + self, token: str, has_json: bool, has_files: bool, additional_headers: dict + ) -> Dict[str, str]: + headers = {"Content-Type": "application/x-www-form-urlencoded"} + headers.update(self.headers) + if token: + headers.update({"Authorization": "Bearer {}".format(token)}) + if additional_headers: + headers.update(additional_headers) + if has_json: + headers.update({"Content-Type": "application/json;charset=utf-8"}) + if has_files: + # will be set afterwards + headers.pop("Content-Type", None) + return headers + + # ================================================================= + + @staticmethod + def validate_slack_signature(*, signing_secret: str, data: str, timestamp: str, signature: str) -> bool: + """ + Slack creates a unique string for your app and shares it with you. Verify + requests from Slack with confidence by verifying signatures using your + signing secret. + + On each HTTP request that Slack sends, we add an X-Slack-Signature HTTP + header. The signature is created by combining the signing secret with the + body of the request we're sending using a standard HMAC-SHA256 keyed hash. + + https://docs.slack.dev/authentication/verifying-requests-from-slack/ + + Args: + signing_secret: Your application's signing secret, available in the + Slack API dashboard + data: The raw body of the incoming request - no headers, just the body. + timestamp: from the 'X-Slack-Request-Timestamp' header + signature: from the 'X-Slack-Signature' header - the calculated signature + should match this. + + Returns: + True if signatures matches + """ + warnings.warn( + "As this method is deprecated since slackclient 2.6.0, " + "use `from slack.signature import SignatureVerifier` instead", + DeprecationWarning, + ) + format_req = str.encode(f"v0:{timestamp}:{data}") + encoded_secret = str.encode(signing_secret) + request_hash = hmac.new(encoded_secret, format_req, hashlib.sha256).hexdigest() + calculated_signature = f"v0={request_hash}" + return hmac.compare_digest(calculated_signature, signature) diff --git a/slack/web/classes/__init__.py b/slack/web/classes/__init__.py new file mode 100644 index 000000000..58311a601 --- /dev/null +++ b/slack/web/classes/__init__.py @@ -0,0 +1,10 @@ +from slack_sdk.models import BaseObject # noqa +from slack_sdk.models import JsonObject # noqa +from slack_sdk.models import JsonValidator # noqa +from slack_sdk.models import EnumValidator # noqa +from slack_sdk.models import extract_json # noqa +from slack_sdk.models import show_unknown_key_warning # noqa + +from slack import deprecation + +deprecation.show_message(__name__, "slack_sdk.models") diff --git a/slack/web/classes/actions.py b/slack/web/classes/actions.py new file mode 100644 index 000000000..6f2ded52d --- /dev/null +++ b/slack/web/classes/actions.py @@ -0,0 +1,13 @@ +from slack_sdk.models.attachments import AbstractActionSelector # noqa +from slack_sdk.models.attachments import Action # noqa +from slack_sdk.models.attachments import ActionButton # noqa +from slack_sdk.models.attachments import ActionChannelSelector # noqa +from slack_sdk.models.attachments import ActionConversationSelector # noqa +from slack_sdk.models.attachments import ActionExternalSelector # noqa +from slack_sdk.models.attachments import ActionLinkButton # noqa +from slack_sdk.models.attachments import ActionUserSelector # noqa +from slack_sdk.models.dialogs import ActionStaticSelector # noqa + +from slack import deprecation + +deprecation.show_message(__name__, "slack_sdk.models.attachments/dialogs") diff --git a/slack/web/classes/attachments.py b/slack/web/classes/attachments.py new file mode 100644 index 000000000..3f85c408c --- /dev/null +++ b/slack/web/classes/attachments.py @@ -0,0 +1,9 @@ +from slack_sdk.models.attachments import Attachment # noqa +from slack_sdk.models.attachments import AttachmentField # noqa +from slack_sdk.models.attachments import BlockAttachment # noqa +from slack_sdk.models.attachments import InteractiveAttachment # noqa +from slack_sdk.models.attachments import SeededColors # noqa + +from slack import deprecation + +deprecation.show_message(__name__, "slack_sdk.models.attachments") diff --git a/slack/web/classes/blocks.py b/slack/web/classes/blocks.py new file mode 100644 index 000000000..6ad4fc725 --- /dev/null +++ b/slack/web/classes/blocks.py @@ -0,0 +1,13 @@ +from slack import deprecation +from slack_sdk.models.blocks import ActionsBlock # noqa +from slack_sdk.models.blocks import Block # noqa +from slack_sdk.models.blocks import CallBlock # noqa +from slack_sdk.models.blocks import ContextBlock # noqa +from slack_sdk.models.blocks import DividerBlock # noqa +from slack_sdk.models.blocks import FileBlock # noqa +from slack_sdk.models.blocks import HeaderBlock # noqa +from slack_sdk.models.blocks import ImageBlock # noqa +from slack_sdk.models.blocks import InputBlock # noqa +from slack_sdk.models.blocks import SectionBlock # noqa + +deprecation.show_message(__name__, "slack_sdk.models.blocks") diff --git a/slack/web/classes/dialog_elements.py b/slack/web/classes/dialog_elements.py new file mode 100644 index 000000000..00b3126d7 --- /dev/null +++ b/slack/web/classes/dialog_elements.py @@ -0,0 +1,14 @@ +from slack_sdk.models.dialogs import AbstractDialogSelector # noqa +from slack_sdk.models.dialogs import DialogChannelSelector # noqa +from slack_sdk.models.dialogs import DialogConversationSelector # noqa +from slack_sdk.models.dialogs import DialogExternalSelector # noqa +from slack_sdk.models.dialogs import DialogStaticSelector # noqa +from slack_sdk.models.dialogs import DialogTextArea # noqa +from slack_sdk.models.dialogs import DialogTextComponent # noqa +from slack_sdk.models.dialogs import DialogTextField # noqa +from slack_sdk.models.dialogs import DialogUserSelector # noqa +from slack_sdk.models.dialogs import TextElementSubtypes # noqa + +from slack import deprecation + +deprecation.show_message(__name__, "slack_sdk.models.blocks") diff --git a/slack/web/classes/dialogs.py b/slack/web/classes/dialogs.py new file mode 100644 index 000000000..7e6d1cbd2 --- /dev/null +++ b/slack/web/classes/dialogs.py @@ -0,0 +1,5 @@ +from slack_sdk.models.dialogs import DialogBuilder # noqa + +from slack import deprecation + +deprecation.show_message(__name__, "slack_sdk.models.dialogs") diff --git a/slack/web/classes/elements.py b/slack/web/classes/elements.py new file mode 100644 index 000000000..fbe4b801e --- /dev/null +++ b/slack/web/classes/elements.py @@ -0,0 +1,31 @@ +from slack_sdk.models.blocks import BlockElement # noqa +from slack_sdk.models.blocks import ButtonElement # noqa +from slack_sdk.models.blocks import ChannelMultiSelectElement # noqa +from slack_sdk.models.blocks import ChannelSelectElement # noqa +from slack_sdk.models.blocks import CheckboxesElement # noqa +from slack_sdk.models.blocks import ConversationFilter # noqa +from slack_sdk.models.blocks import ConversationMultiSelectElement # noqa +from slack_sdk.models.blocks import ConversationSelectElement # noqa +from slack_sdk.models.blocks import DatePickerElement # noqa +from slack_sdk.models.blocks import DateTimePickerElement # noqa +from slack_sdk.models.blocks import ExternalDataMultiSelectElement # noqa +from slack_sdk.models.blocks import ExternalDataSelectElement # noqa +from slack_sdk.models.blocks import ImageElement # noqa +from slack_sdk.models.blocks import InputInteractiveElement # noqa +from slack_sdk.models.blocks import InteractiveElement # noqa +from slack_sdk.models.blocks import LinkButtonElement # noqa +from slack_sdk.models.blocks import OverflowMenuElement # noqa +from slack_sdk.models.blocks import PlainTextInputElement # noqa +from slack_sdk.models.blocks import EmailInputElement # noqa +from slack_sdk.models.blocks import UrlInputElement # noqa +from slack_sdk.models.blocks import NumberInputElement # noqa +from slack_sdk.models.blocks import RadioButtonsElement # noqa +from slack_sdk.models.blocks import SelectElement # noqa +from slack_sdk.models.blocks import StaticMultiSelectElement # noqa +from slack_sdk.models.blocks import StaticSelectElement # noqa +from slack_sdk.models.blocks import UserMultiSelectElement # noqa +from slack_sdk.models.blocks import UserSelectElement # noqa + +from slack import deprecation + +deprecation.show_message(__name__, "slack_sdk.models.blocks") diff --git a/slack/web/classes/interactions.py b/slack/web/classes/interactions.py new file mode 100644 index 000000000..f4d871efa --- /dev/null +++ b/slack/web/classes/interactions.py @@ -0,0 +1,137 @@ +import json +from typing import List, NamedTuple + +from . import BaseObject + + +class IDNamePair(NamedTuple): + """Simple type used to help with unpacking event data""" + + id: str + name: str + + +class InteractiveEvent(BaseObject): + response_url: str + user: IDNamePair + team: IDNamePair + channel: IDNamePair + + raw_event: dict + + def __init__(self, event: dict): + self.raw_event = event + self.response_url = event["response_url"] + + +class MessageInteractiveEvent(InteractiveEvent): + event_type: str + message_ts: str + trigger_id: str + action_id: str + block_id: str + message: dict + + def __init__(self, event: dict): + """ + Convenience class to parse an interactive message payload from the events API + + Args: + event: the raw event dictionary + """ + super().__init__(event) + self.user = IDNamePair(event["user"]["id"], event["user"]["username"]) + self.team: IDNamePair = IDNamePair(event["team"]["id"], event["team"]["domain"]) + self.channel: IDNamePair = IDNamePair(event["channel"]["id"], event["channel"]["name"]) + self.event_type = event["type"] + self.message_ts = event["message"]["ts"] + self.trigger_id = event["trigger_id"] + # actions payload is an array, but will only have one item (the action + # actually interacted with) + action = event["actions"][0] + self.action_id = action["action_id"] + self.block_id = action["block_id"] + if action.get("selected_option"): + self.value = action["selected_option"]["value"] + else: + self.value = action["value"] + self.message = event["message"] + + +class DialogInteractiveEvent(InteractiveEvent): + event_type: str + submission: dict + state: dict + + def __init__(self, event: dict): + """ + Convenience class to parse a dialog interaction payload from the events API + + Args: + event: the raw event dictionary + """ + super().__init__(event) + self.user = IDNamePair(event["user"]["id"], event["user"]["name"]) + self.team = IDNamePair(event["team"]["id"], event["team"]["domain"]) + self.channel = IDNamePair(event["channel"]["id"], event["channel"]["name"]) + self.callback_id = event["callback_id"] + self.event_type = event["type"] + self.submission = event["submission"] + if event["state"]: + self.state = json.loads(event["state"]) + else: + self.state = {} + + def require_any(self, requirements: List[str]) -> dict: + """ + Convenience method to construct the 'errors' response to send directly back to + the invoking HTTP request + + Args: + requirements: List of required dialog components, by name + """ + if any(self.submission.get(requirement, "") for requirement in requirements): # skipcq: PYL-R1705 + return {} + else: + errors = [] + for key in self.submission: + error_text = "At least one value is required" + errors.append({"name": key, "error": error_text}) + return {"errors": errors} + + +class SlashCommandInteractiveEvent(InteractiveEvent): + trigger_id: str + command: str + text: str + + def __init__(self, event: dict): + """ + Convenience class to parse a slash command payload from the events API + + Args: + event: the raw event dictionary + """ + super().__init__(event) + self.user = IDNamePair(event["user_id"], event["user_name"]) + self.channel = IDNamePair(event["channel_id"], event["channel_name"]) + self.team = IDNamePair(event["team_id"], event["team_domain"]) + self.trigger_id = event["trigger_id"] + self.command = event["command"] + self.text = event["text"] + + @staticmethod + def create_reply(message, ephemeral=False) -> dict: + """ + Create a reply suitable to send directly back to the invoking HTTP request + + Args: + message: Text to send + ephemeral: Whether the response should be limited to a single user, or to + broadcast the reply (_and_ the user's original invocation) to the + channel publicly + """ + if ephemeral: # skipcq: PYL-R1705 + return {"text": message, "response_type": "ephemeral"} + else: + return {"text": message, "response_type": "in_channel"} diff --git a/slack/web/classes/messages.py b/slack/web/classes/messages.py new file mode 100644 index 000000000..37b1dca6f --- /dev/null +++ b/slack/web/classes/messages.py @@ -0,0 +1 @@ +from slack_sdk.models.messages.message import Message # noqa diff --git a/slack/web/classes/objects.py b/slack/web/classes/objects.py new file mode 100644 index 000000000..6025aa029 --- /dev/null +++ b/slack/web/classes/objects.py @@ -0,0 +1,19 @@ +from slack_sdk.models.blocks import ButtonStyles # noqa +from slack_sdk.models.blocks import ConfirmObject # noqa +from slack_sdk.models.blocks import DynamicSelectElementTypes # noqa +from slack_sdk.models.blocks import MarkdownTextObject # noqa +from slack_sdk.models.blocks import Option # noqa +from slack_sdk.models.blocks import OptionGroup # noqa +from slack_sdk.models.blocks import PlainTextObject # noqa +from slack_sdk.models.blocks import TextObject # noqa +from slack_sdk.models.messages import ChannelLink # noqa +from slack_sdk.models.messages import DateLink # noqa +from slack_sdk.models.messages import EveryoneLink # noqa +from slack_sdk.models.messages import HereLink # noqa +from slack_sdk.models.messages import Link # noqa +from slack_sdk.models.messages import ObjectLink # noqa + + +from slack import deprecation + +deprecation.show_message(__name__, "slack_sdk.models.blocks/messages") diff --git a/slack/web/classes/readme.md b/slack/web/classes/readme.md new file mode 100644 index 000000000..27ca73714 --- /dev/null +++ b/slack/web/classes/readme.md @@ -0,0 +1,49 @@ +# Using the Slack object classes + +## Composing Messages + +Messages are built up out of blocks and legacy attachments. Blocks are composed of the base Block classes in `blocks.py`, which themselves are composed of elements (`elements.py`) which are either atomic or contain common sub-objects (`objects.py`). + +For example: A simple block template, containing a header, some fields, and an actions block at the bottom would be built up as follows: + +```python +from slack.web.client import WebClient +from slack.web.classes import messages, blocks, elements + +client = WebClient(token="abc") + +fields = blocks.SectionBlock(fields=["*Type:*\nComputer", "*Reason:*\nAll vowel keys aren't working"]) + +approve_button = elements.ButtonElement(text="Approve", action_id="approval", value="order_123", style="primary") +deny_button = elements.ButtonElement(text="Deny", action_id="denial", value="order_123", style="danger") + +buttons = [approve_button, deny_button] + +actions = blocks.ActionsBlock(elements=buttons) + +work_order_message = messages.Message(text="You have a new request", blocks=[fields, actions]) + +client.chat_postMessage(channel="C12345", **work_order_message.to_dict()) +``` + +## Composing Dialogs +Dialogs can be built using a helper 'builder' class, to simplify keeping track of required fields. + +```python +from slack.web.client import WebClient +from slack.web.classes import dialogs + +builder = ( + dialogs.DialogBuilder() + .title("My Cool Dialog") + .callback_id("myCoolDialog") + .state({'value': 123, 'key': "something"}) + .conversation_selector(name="target", label="Choose Target") + .text_area(name="message", label="Message", hint="Enter a message", max_length=500) + .text_field(name="signature", label="Signature", optional=True, max_length=50) +) + +client = WebClient(token="abc") + +client.dialog_open(dialog=builder.to_dict(), trigger_id="123458.12355") +``` diff --git a/slack/web/classes/views.py b/slack/web/classes/views.py new file mode 100644 index 000000000..a23f3ed74 --- /dev/null +++ b/slack/web/classes/views.py @@ -0,0 +1,7 @@ +from slack_sdk.models.views import View # noqa +from slack_sdk.models.views import ViewState # noqa +from slack_sdk.models.views import ViewStateValue # noqa + +from slack import deprecation + +deprecation.show_message(__name__, "slack_sdk.models.views") diff --git a/slack/web/client.py b/slack/web/client.py new file mode 100644 index 000000000..82900bfb0 --- /dev/null +++ b/slack/web/client.py @@ -0,0 +1,4 @@ +from slack import deprecation +from slack_sdk.web.legacy_client import LegacyWebClient as WebClient # noqa + +deprecation.show_message(__name__, "slack_sdk.web.client") diff --git a/slack/web/deprecation.py b/slack/web/deprecation.py new file mode 100644 index 000000000..059bf149f --- /dev/null +++ b/slack/web/deprecation.py @@ -0,0 +1,30 @@ +import os +import warnings + +# https://docs.slack.dev/changelog/2020-01-deprecating-antecedents-to-the-conversations-api/ +deprecated_method_prefixes_2020_01 = [ + "channels.", + "groups.", + "im.", + "mpim.", + "admin.conversations.whitelist.", +] + + +def show_2020_01_deprecation(method_name: str): + """Prints a warning if the given method is deprecated""" + + skip_deprecation = os.environ.get("SLACKCLIENT_SKIP_DEPRECATION") # for unit tests etc. + if skip_deprecation: + return + if not method_name: + return + + matched_prefixes = [prefix for prefix in deprecated_method_prefixes_2020_01 if method_name.startswith(prefix)] + if len(matched_prefixes) > 0: + message = ( + f"{method_name} is deprecated. Please use the Conversations API instead. " + "For more info, go to " + "https://docs.slack.dev/changelog/2020-01-deprecating-antecedents-to-the-conversations-api/" + ) + warnings.warn(message) diff --git a/slack/web/internal_utils.py b/slack/web/internal_utils.py new file mode 100644 index 000000000..67ce3d364 --- /dev/null +++ b/slack/web/internal_utils.py @@ -0,0 +1,52 @@ +import json +from typing import Union, Dict, List + +from slack.errors import SlackRequestError +from slack.web.classes.attachments import Attachment +from slack.web.classes.blocks import Block + + +def _parse_web_class_objects(kwargs) -> None: + def to_dict(obj: Union[Dict, Block, Attachment]): + if isinstance(obj, Block): + return obj.to_dict() + if isinstance(obj, Attachment): + return obj.to_dict() + return obj + + blocks = kwargs.get("blocks", None) + if blocks is not None and isinstance(blocks, list): + dict_blocks = [to_dict(b) for b in blocks] + kwargs.update({"blocks": dict_blocks}) + + attachments = kwargs.get("attachments", None) + if attachments is not None and isinstance(attachments, list): + dict_attachments = [to_dict(a) for a in attachments] + kwargs.update({"attachments": dict_attachments}) + + +def _update_call_participants(kwargs, users: Union[str, List[Dict[str, str]]]) -> None: + 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]]") + + +def _next_cursor_is_present(data) -> bool: + """Determine if the response contains 'next_cursor' + and 'next_cursor' is not empty. + + Returns: + A boolean value. + """ + present = ( + "response_metadata" in data + and "next_cursor" in data["response_metadata"] + and data["response_metadata"]["next_cursor"] != "" + ) + return present diff --git a/slack/web/slack_response.py b/slack/web/slack_response.py new file mode 100644 index 000000000..a6c9cdefe --- /dev/null +++ b/slack/web/slack_response.py @@ -0,0 +1,6 @@ +from slack import deprecation +from slack_sdk.web.legacy_slack_response import ( # noqa + LegacySlackResponse as SlackResponse, +) + +deprecation.show_message(__name__, "slack_sdk.web.slack_response") diff --git a/slack/webhook/__init__.py b/slack/webhook/__init__.py new file mode 100644 index 000000000..6b03c460a --- /dev/null +++ b/slack/webhook/__init__.py @@ -0,0 +1,7 @@ +from slack_sdk.webhook.webhook_response import WebhookResponse # noqa +from slack_sdk.webhook.client import WebhookClient # noqa +from slack_sdk.webhook.async_client import AsyncWebhookClient # noqa + +from slack import deprecation + +deprecation.show_message(__name__, "slack_sdk.webhook") diff --git a/slack/webhook/async_client.py b/slack/webhook/async_client.py new file mode 100644 index 000000000..31310c96f --- /dev/null +++ b/slack/webhook/async_client.py @@ -0,0 +1,119 @@ +import json +import logging +from ssl import SSLContext +from typing import Dict, Union, List, Optional + +import aiohttp +from aiohttp import BasicAuth, ClientSession + +from slack.errors import SlackApiError +from .internal_utils import _debug_log_response, _build_request_headers, _build_body +from .webhook_response import WebhookResponse +from ..web.classes.attachments import Attachment +from ..web.classes.blocks import Block + + +class AsyncWebhookClient: + logger = logging.getLogger(__name__) + + def __init__( + self, + url: str, + timeout: int = 30, + ssl: Optional[SSLContext] = None, + proxy: Optional[str] = None, + session: Optional[ClientSession] = None, + trust_env_in_session: bool = False, + auth: Optional[BasicAuth] = None, + default_headers: Optional[Dict[str, str]] = None, + ): + self.url = url + self.timeout = timeout + self.ssl = ssl + self.proxy = proxy + self.trust_env_in_session = trust_env_in_session + self.session = session + self.auth = auth + self.default_headers = default_headers if default_headers else {} + + async def send( + self, + *, + text: Optional[str] = None, + attachments: Optional[List[Union[Dict[str, any], Attachment]]] = None, + blocks: Optional[List[Union[Dict[str, any], Block]]] = None, + response_type: Optional[str] = None, + headers: Optional[Dict[str, str]] = None, + ) -> WebhookResponse: + """Performs a Slack API request and returns the result. + + Args: + text: The text message (even when having blocks, setting this as well is recommended as it works as fallback) + attachments: A collection of attachments + blocks: A collection of Block Kit UI components + response_type: The type of message (either 'in_channel' or 'ephemeral') + headers: Request headers to append only for this request + Returns: + Webhook response + """ + return await self.send_dict( + body={ + "text": text, + "attachments": attachments, + "blocks": blocks, + "response_type": response_type, + }, + headers=headers, + ) + + async def send_dict(self, body: Dict[str, any], headers: Optional[Dict[str, str]] = None) -> WebhookResponse: + return await self._perform_http_request( + body=_build_body(body), + headers=_build_request_headers(self.default_headers, headers), + ) + + async def _perform_http_request(self, *, body: Dict[str, any], headers: Dict[str, str]) -> WebhookResponse: + body = json.dumps(body) + headers["Content-Type"] = "application/json;charset=utf-8" + + if self.logger.level <= logging.DEBUG: + self.logger.debug(f"Sending a request - url: {self.url}, body: {body}, headers: {headers}") + session: Optional[ClientSession] = None + use_running_session = self.session and not self.session.closed + if use_running_session: + session = self.session + else: + session = aiohttp.ClientSession( + timeout=aiohttp.ClientTimeout(total=self.timeout), + auth=self.auth, + trust_env=self.trust_env_in_session, + ) + + try: + request_kwargs = { + "headers": headers, + "data": body, + "ssl": self.ssl, + "proxy": self.proxy, + } + async with session.request("POST", self.url, **request_kwargs) as res: + response_body = {} + try: + response_body = await res.text() + except aiohttp.ContentTypeError: + self._logger.debug(f"No response data returned from the following API call: {self.url}.") + except json.decoder.JSONDecodeError as e: + message = f"Failed to parse the response body: {str(e)}" + raise SlackApiError(message, res) + + resp = WebhookResponse( + url=self.url, + status_code=res.status, + body=response_body, + headers=res.headers, + ) + _debug_log_response(self.logger, resp) + return resp + finally: + if not use_running_session: + await session.close() diff --git a/slack/webhook/client.py b/slack/webhook/client.py new file mode 100644 index 000000000..7d39c03df --- /dev/null +++ b/slack/webhook/client.py @@ -0,0 +1,116 @@ +import json +import logging +import urllib +from http.client import HTTPResponse +from ssl import SSLContext +from typing import Dict, Union, List, Optional +from urllib.error import HTTPError +from urllib.request import Request, urlopen, OpenerDirector, ProxyHandler, HTTPSHandler + +from slack.errors import SlackRequestError +from .internal_utils import _build_body, _build_request_headers, _debug_log_response +from .webhook_response import WebhookResponse +from ..web.classes.attachments import Attachment +from ..web.classes.blocks import Block + + +class WebhookClient: + logger = logging.getLogger(__name__) + + def __init__( + self, + url: str, + timeout: int = 30, + ssl: Optional[SSLContext] = None, + proxy: Optional[str] = None, + default_headers: Optional[Dict[str, str]] = None, + ): + self.url = url + self.timeout = timeout + self.ssl = ssl + self.proxy = proxy + self.default_headers = default_headers if default_headers else {} + + def send( + self, + *, + text: Optional[str] = None, + attachments: Optional[List[Union[Dict[str, any], Attachment]]] = None, + blocks: Optional[List[Union[Dict[str, any], Block]]] = None, + response_type: Optional[str] = None, + headers: Optional[Dict[str, str]] = None, + ) -> WebhookResponse: + return self.send_dict( + body={ + "text": text, + "attachments": attachments, + "blocks": blocks, + "response_type": response_type, + }, + headers=headers, + ) + + def send_dict(self, body: Dict[str, any], headers: Optional[Dict[str, str]] = None) -> WebhookResponse: + return self._perform_http_request( + body=_build_body(body), + headers=_build_request_headers(self.default_headers, headers), + ) + + def _perform_http_request(self, *, body: Dict[str, any], headers: Dict[str, str]) -> WebhookResponse: + body = json.dumps(body) + headers["Content-Type"] = "application/json;charset=utf-8" + + if self.logger.level <= logging.DEBUG: + self.logger.debug(f"Sending a request - url: {self.url}, body: {body}, headers: {headers}") + try: + url = self.url + opener: Optional[OpenerDirector] = None + # for security (BAN-B310) + if url.lower().startswith("http"): + req = Request(method="POST", url=url, data=body.encode("utf-8"), headers=headers) + if self.proxy is not None: + if isinstance(self.proxy, str): + opener = urllib.request.build_opener( + ProxyHandler({"http": self.proxy, "https": self.proxy}), + HTTPSHandler(context=self.ssl), + ) + else: + raise SlackRequestError(f"Invalid proxy detected: {self.proxy} must be a str value") + else: + raise SlackRequestError(f"Invalid URL detected: {url}") + + # NOTE: BAN-B310 is already checked above + resp: Optional[HTTPResponse] = None + if opener: + resp = opener.open(req, timeout=self.timeout) # skipcq: BAN-B310 + else: + resp = urlopen(req, context=self.ssl, timeout=self.timeout) # skipcq: BAN-B310 + charset: str = resp.headers.get_content_charset() or "utf-8" + response_body: str = resp.read().decode(charset) + resp = WebhookResponse( + url=url, + status_code=resp.status, + body=response_body, + headers=resp.headers, + ) + _debug_log_response(self.logger, resp) + return resp + + except HTTPError as e: + charset = e.headers.get_content_charset() or "utf-8" + body: str = e.read().decode(charset) # read the response body here + resp = WebhookResponse( + url=url, + status_code=e.code, + body=body, + headers=e.headers, + ) + if e.code == 429: + # for backward-compatibility with WebClient (v.2.5.0 or older) + resp.headers["Retry-After"] = resp.headers["retry-after"] + _debug_log_response(self.logger, resp) + return resp + + except Exception as err: + self.logger.error(f"Failed to send a request to Slack API server: {err}") + raise err diff --git a/slack/webhook/internal_utils.py b/slack/webhook/internal_utils.py new file mode 100644 index 000000000..ea21e88e2 --- /dev/null +++ b/slack/webhook/internal_utils.py @@ -0,0 +1,40 @@ +import logging +from typing import Optional, Dict + +from slack.web import get_user_agent, convert_bool_to_0_or_1 +from slack.web.internal_utils import _parse_web_class_objects +from slack.webhook import WebhookResponse + + +def _build_body(original_body: Dict[str, any]) -> Dict[str, any]: + body = {k: v for k, v in original_body.items() if v is not None} + body = convert_bool_to_0_or_1(body) + _parse_web_class_objects(body) + return body + + +def _build_request_headers( + default_headers: Dict[str, str], + additional_headers: Optional[Dict[str, str]], +) -> Dict[str, str]: + if additional_headers is None: + return {} + + request_headers = { + "User-Agent": get_user_agent(), + "Content-Type": "application/json;charset=utf-8", + } + request_headers.update(default_headers) + if additional_headers: + request_headers.update(additional_headers) + return request_headers + + +def _debug_log_response(logger, resp: WebhookResponse) -> None: + if logger.level <= logging.DEBUG: + logger.debug( + "Received the following response - " + f"status: {resp.status_code}, " + f"headers: {(dict(resp.headers))}, " + f"body: {resp.body}" + ) diff --git a/slack/webhook/webhook_response.py b/slack/webhook/webhook_response.py new file mode 100644 index 000000000..5da6b8086 --- /dev/null +++ b/slack/webhook/webhook_response.py @@ -0,0 +1,13 @@ +class WebhookResponse: + def __init__( + self, + *, + url: str, + status_code: int, + body: str, + headers: dict, + ): + self.api_url = url + self.status_code = status_code + self.body = body + self.headers = headers diff --git a/slack_sdk/__init__.py b/slack_sdk/__init__.py new file mode 100644 index 000000000..b5204e3e3 --- /dev/null +++ b/slack_sdk/__init__.py @@ -0,0 +1,54 @@ +""" +* The SDK website: https://docs.slack.dev/tools/python-slack-sdk +* PyPI package: https://pypi.org/project/slack-sdk/ + +Here is the list of key modules in this SDK: + +#### Web API Client + +* Web API client: `slack_sdk.web.client` +* asyncio-based Web API client: `slack_sdk.web.async_client` + +#### Webhook / response_url Client + +* Webhook client: `slack_sdk.webhook.client` +* asyncio-based Webhook client: `slack_sdk.webhook.async_client` + +#### Socket Mode Client + +* The built-in Socket Mode client: `slack_sdk.socket_mode.builtin.client` +* [aiohttp](https://pypi.org/project/aiohttp/) based client: `slack_sdk.socket_mode.aiohttp` +* [websocket_client](https://pypi.org/project/websocket-client/) based client: `slack_sdk.socket_mode.websocket_client` +* [websockets](https://pypi.org/project/websockets/) based client: `slack_sdk.socket_mode.websockets` + +#### OAuth + +* `slack_sdk.oauth.installation_store.installation_store` +* `slack_sdk.oauth.state_store` + +#### Audit Logs API Client + +* `slack_sdk.audit_logs.v1.client` +* `slack_sdk.audit_logs.v1.async_client` + +#### SCIM API Client + +* `slack_sdk.scim.v1.client` +* `slack_sdk.scim.v1.async_client` + +""" + +import logging +from logging import NullHandler + +# from .rtm import RTMClient +from .web import WebClient +from .webhook import WebhookClient + +__all__ = [ + "WebClient", + "WebhookClient", +] + +# Set default logging handler to avoid "No handler found" warnings. +logging.getLogger(__name__).addHandler(NullHandler()) diff --git a/slack_sdk/aiohttp_version_checker.py b/slack_sdk/aiohttp_version_checker.py new file mode 100644 index 000000000..1eff14efa --- /dev/null +++ b/slack_sdk/aiohttp_version_checker.py @@ -0,0 +1,24 @@ +"""Internal module for checking aiohttp compatibility of async modules""" + +import logging +from typing import Callable + + +def _print_warning_log(message: str) -> None: + logging.getLogger(__name__).warning(message) + + +def validate_aiohttp_version( + aiohttp_version: str, + print_warning: Callable[[str], None] = _print_warning_log, +): + if aiohttp_version is not None: + elements = aiohttp_version.split(".") + if len(elements) >= 3: + # patch version can be a non-numeric value + major, minor, patch = int(elements[0]), int(elements[1]), elements[2] + if major <= 2 or (major == 3 and (minor == 6 or (minor == 7 and patch == "0"))): + print_warning( + "We highly recommend upgrading aiohttp to 3.7.3 or higher versions." + "An older version of the library may not work with the Slack server-side in the future." + ) diff --git a/slack_sdk/audit_logs/__init__.py b/slack_sdk/audit_logs/__init__.py new file mode 100644 index 000000000..f8a7a2a91 --- /dev/null +++ b/slack_sdk/audit_logs/__init__.py @@ -0,0 +1,12 @@ +"""Audit Logs API is a set of APIs for monitoring what’s happening in your Enterprise Grid organization. + +Refer to https://docs.slack.dev/tools/python-slack-sdk/audit-logs for details. +""" + +from .v1.client import AuditLogsClient +from .v1.response import AuditLogsResponse + +__all__ = [ + "AuditLogsClient", + "AuditLogsResponse", +] diff --git a/slack_sdk/audit_logs/async_client.py b/slack_sdk/audit_logs/async_client.py new file mode 100644 index 000000000..3e606fb5f --- /dev/null +++ b/slack_sdk/audit_logs/async_client.py @@ -0,0 +1,5 @@ +from .v1.async_client import AsyncAuditLogsClient + +__all__ = [ + "AsyncAuditLogsClient", +] diff --git a/slack_sdk/audit_logs/v1/__init__.py b/slack_sdk/audit_logs/v1/__init__.py new file mode 100644 index 000000000..9d03c76ce --- /dev/null +++ b/slack_sdk/audit_logs/v1/__init__.py @@ -0,0 +1,4 @@ +"""Audit Logs API is a set of APIs for monitoring what’s happening in your Enterprise Grid organization. + +Refer to https://docs.slack.dev/tools/python-slack-sdk/audit-logs for details. +""" diff --git a/slack_sdk/audit_logs/v1/async_client.py b/slack_sdk/audit_logs/v1/async_client.py new file mode 100644 index 000000000..7de8fd5b2 --- /dev/null +++ b/slack_sdk/audit_logs/v1/async_client.py @@ -0,0 +1,361 @@ +"""Audit Logs API is a set of APIs for monitoring what’s happening in your Enterprise Grid organization. + +Refer to https://docs.slack.dev/tools/python-slack-sdk/audit-logs for details. +""" + +import json +import logging +from ssl import SSLContext +from typing import Any, List +from typing import Dict, Optional + +import aiohttp +from aiohttp import BasicAuth, ClientSession + +from slack_sdk.errors import SlackApiError +from .internal_utils import ( + _build_request_headers, + _debug_log_response, + get_user_agent, +) +from .response import AuditLogsResponse +from slack_sdk.http_retry.async_handler import AsyncRetryHandler +from slack_sdk.http_retry.builtin_async_handlers import async_default_handlers +from slack_sdk.http_retry.request import HttpRequest as RetryHttpRequest +from slack_sdk.http_retry.response import HttpResponse as RetryHttpResponse +from slack_sdk.http_retry.state import RetryState +from ...proxy_env_variable_loader import load_http_proxy_from_env + + +class AsyncAuditLogsClient: + BASE_URL = "https://api.slack.com/audit/v1/" + + token: str + timeout: int + ssl: Optional[SSLContext] + proxy: Optional[str] + base_url: str + session: Optional[ClientSession] + trust_env_in_session: bool + auth: Optional[BasicAuth] + default_headers: Dict[str, str] + logger: logging.Logger + retry_handlers: List[AsyncRetryHandler] + + def __init__( + self, + token: str, + timeout: int = 30, + ssl: Optional[SSLContext] = None, + proxy: Optional[str] = None, + base_url: str = BASE_URL, + session: Optional[ClientSession] = None, + trust_env_in_session: bool = False, + auth: Optional[BasicAuth] = None, + default_headers: Optional[Dict[str, str]] = None, + user_agent_prefix: Optional[str] = None, + user_agent_suffix: Optional[str] = None, + logger: Optional[logging.Logger] = None, + retry_handlers: Optional[List[AsyncRetryHandler]] = None, + ): + """API client for Audit Logs API + See https://docs.slack.dev/admins/audit-logs-api/ for more details + + Args: + token: An admin user's token, which starts with `xoxp-` + timeout: Request timeout (in seconds) + ssl: `ssl.SSLContext` to use for requests + proxy: Proxy URL (e.g., `localhost:9000`, `http://localhost:9000`) + base_url: The base URL for API calls + session: `aiohttp.ClientSession` instance + trust_env_in_session: True/False for `aiohttp.ClientSession` + auth: Basic auth info for `aiohttp.ClientSession` + default_headers: Request headers to add to all requests + user_agent_prefix: Prefix for User-Agent header value + user_agent_suffix: Suffix for User-Agent header value + logger: Custom logger + retry_handlers: Retry handlers + """ + self.token = token + self.timeout = timeout + self.ssl = ssl + self.proxy = proxy + self.base_url = base_url + self.session = session + self.trust_env_in_session = trust_env_in_session + self.auth = auth + self.default_headers = default_headers if default_headers else {} + self.default_headers["User-Agent"] = get_user_agent(user_agent_prefix, user_agent_suffix) + self.logger = logger if logger is not None else logging.getLogger(__name__) + self.retry_handlers = retry_handlers if retry_handlers is not None else async_default_handlers() + + if self.proxy is None or len(self.proxy.strip()) == 0: + env_variable = load_http_proxy_from_env(self.logger) + if env_variable is not None: + self.proxy = env_variable + + async def schemas( + self, + *, + query_params: Optional[Dict[str, Any]] = None, + headers: Optional[Dict[str, str]] = None, + ) -> AuditLogsResponse: + """Returns information about the kind of objects which the Audit Logs API + returns as a list of all objects and a short description. + Authentication not required. + + Args: + query_params: Set any values if you want to add query params + headers: Additional request headers + Returns: + API response + """ + return await self.api_call( + path="schemas", + query_params=query_params, + headers=headers, + ) + + async def actions( + self, + *, + query_params: Optional[Dict[str, Any]] = None, + headers: Optional[Dict[str, str]] = None, + ) -> AuditLogsResponse: + """Returns information about the kind of actions that the Audit Logs API + returns as a list of all actions and a short description of each. + Authentication not required. + + Args: + query_params: Set any values if you want to add query params + headers: Additional request headers + + Returns: + API response + """ + return await self.api_call( + path="actions", + query_params=query_params, + headers=headers, + ) + + async def logs( + self, + *, + latest: Optional[int] = None, + oldest: Optional[int] = None, + limit: Optional[int] = None, + action: Optional[str] = None, + actor: Optional[str] = None, + entity: Optional[str] = None, + cursor: Optional[str] = None, + additional_query_params: Optional[Dict[str, Any]] = None, + headers: Optional[Dict[str, str]] = None, + ) -> AuditLogsResponse: + """This is the primary endpoint for retrieving actual audit events from your organization. + It will return a list of actions that have occurred on the installed workspace or grid organization. + Authentication required. + + The following filters can be applied in order to narrow the range of actions returned. + Filters are added as query string parameters and can be combined together. + Multiple filter parameters are additive (a boolean AND) and are separated + with an ampersand (&) in the query string. Filtering is entirely optional. + + Args: + latest: Unix timestamp of the most recent audit event to include (inclusive). + oldest: Unix timestamp of the least recent audit event to include (inclusive). + Data is not available prior to March 2018. + limit: Number of results to optimistically return, maximum 9999. + action: Name of the action. + actor: User ID who initiated the action. + entity: ID of the target entity of the action (such as a channel, workspace, organization, file). + cursor: The next page cursor of pagination + additional_query_params: Add anything else if you need to use the ones this library does not support + headers: Additional request headers + + Returns: + API response + """ + query_params = { + "latest": latest, + "oldest": oldest, + "limit": limit, + "action": action, + "actor": actor, + "entity": entity, + "cursor": cursor, + } + if additional_query_params is not None: + query_params.update(additional_query_params) + query_params = {k: v for k, v in query_params.items() if v is not None} + return await self.api_call( + path="logs", + query_params=query_params, + headers=headers, + ) + + async def api_call( + self, + *, + http_verb: str = "GET", + path: str, + query_params: Optional[Dict[str, Any]] = None, + body_params: Optional[Dict[str, Any]] = None, + headers: Optional[Dict[str, str]] = None, + ) -> AuditLogsResponse: + url = f"{self.base_url}{path}" + return await self._perform_http_request( + http_verb=http_verb, + url=url, + query_params=query_params, + body_params=body_params, + headers=_build_request_headers( + token=self.token, + default_headers=self.default_headers, + additional_headers=headers, + ), + ) + + async def _perform_http_request( + self, + *, + http_verb: str, + url: str, + query_params: Optional[Dict[str, Any]], + body_params: Optional[Dict[str, Any]], + headers: Dict[str, str], + ) -> AuditLogsResponse: + if body_params is not None: + body_params = json.dumps(body_params) # type: ignore[assignment] + headers["Content-Type"] = "application/json;charset=utf-8" + + session: Optional[ClientSession] = None + use_running_session = self.session and not self.session.closed + if use_running_session: + session = self.session + else: + session = aiohttp.ClientSession( + timeout=aiohttp.ClientTimeout(total=self.timeout), + auth=self.auth, + trust_env=self.trust_env_in_session, + ) + + last_error = None + resp: Optional[AuditLogsResponse] = None + try: + request_kwargs = { + "headers": headers, + "params": query_params, + "data": body_params, + "ssl": self.ssl, + "proxy": self.proxy, + } + retry_request = RetryHttpRequest( + method=http_verb, + url=url, + headers=headers, # type: ignore[arg-type] + body_params=body_params, + ) + + retry_state = RetryState() + counter_for_safety = 0 + while counter_for_safety < 100: + counter_for_safety += 1 + # If this is a retry, the next try started here. We can reset the flag. + retry_state.next_attempt_requested = False + retry_response: Optional[RetryHttpResponse] = None + response_body = "" + + if self.logger.level <= logging.DEBUG: + headers_for_logging = { + k: "(redacted)" if k.lower() == "authorization" else v for k, v in headers.items() + } + self.logger.debug( + f"Sending a request - " + f"url: {url}, " + f"params: {query_params}, " + f"body: {body_params}, " + f"headers: {headers_for_logging}" + ) + + try: + async with session.request(http_verb, url, **request_kwargs) as res: # type: ignore[arg-type, union-attr] # noqa: E501 + try: + response_body = await res.text() + retry_response = RetryHttpResponse( + status_code=res.status, + headers=res.headers, # type: ignore[arg-type] + data=response_body.encode("utf-8") if response_body is not None else None, + ) + except aiohttp.ContentTypeError: + self.logger.debug(f"No response data returned from the following API call: {url}.") + retry_response = RetryHttpResponse( + status_code=res.status, + headers=res.headers, # type: ignore[arg-type] + ) + except json.decoder.JSONDecodeError as e: + message = f"Failed to parse the response body: {str(e)}" + raise SlackApiError(message, res) + + if res.status == 429: + for handler in self.retry_handlers: + if await handler.can_retry_async( + state=retry_state, + request=retry_request, + response=retry_response, + ): + if self.logger.level <= logging.DEBUG: + self.logger.info( + f"A retry handler found: {type(handler).__name__} " + f"for {http_verb} {url} - rate_limited" + ) + await handler.prepare_for_next_attempt_async( + state=retry_state, + request=retry_request, + response=retry_response, + ) + break + + if retry_state.next_attempt_requested is False: + resp = AuditLogsResponse( + url=url, + status_code=res.status, + raw_body=response_body, + headers=res.headers, # type: ignore[arg-type] + ) + _debug_log_response(self.logger, resp) + return resp + + except Exception as e: + last_error = e + for handler in self.retry_handlers: + if await handler.can_retry_async( + state=retry_state, + request=retry_request, + response=retry_response, + error=e, + ): + if self.logger.level <= logging.DEBUG: + self.logger.info( + f"A retry handler found: {type(handler).__name__} " f"for {http_verb} {url} - {e}" + ) + await handler.prepare_for_next_attempt_async( + state=retry_state, + request=retry_request, + response=retry_response, + error=e, + ) + break + + if retry_state.next_attempt_requested is False: + raise last_error + + if resp is not None: + return resp + raise last_error # type: ignore[misc] + + finally: + if not use_running_session: + await session.close() # type: ignore[union-attr] + + return resp diff --git a/slack_sdk/audit_logs/v1/client.py b/slack_sdk/audit_logs/v1/client.py new file mode 100644 index 000000000..704b872fa --- /dev/null +++ b/slack_sdk/audit_logs/v1/client.py @@ -0,0 +1,362 @@ +"""Audit Logs API is a set of APIs for monitoring what’s happening in your Enterprise Grid organization. + +Refer to https://docs.slack.dev/tools/python-slack-sdk/audit-logs for details. +""" + +import json +import logging +import urllib +from http.client import HTTPResponse +from ssl import SSLContext +from typing import Dict, Optional, List, Any +from urllib.error import HTTPError +from urllib.request import Request, urlopen, OpenerDirector, ProxyHandler, HTTPSHandler + +from slack_sdk.errors import SlackRequestError +from .internal_utils import ( + _build_query, + _build_request_headers, + _debug_log_response, + get_user_agent, +) +from .response import AuditLogsResponse +from slack_sdk.http_retry import default_retry_handlers +from slack_sdk.http_retry.handler import RetryHandler +from slack_sdk.http_retry.request import HttpRequest as RetryHttpRequest +from slack_sdk.http_retry.response import HttpResponse as RetryHttpResponse +from slack_sdk.http_retry.state import RetryState +from ...proxy_env_variable_loader import load_http_proxy_from_env + + +class AuditLogsClient: + BASE_URL = "https://api.slack.com/audit/v1/" + + token: str + timeout: int + ssl: Optional[SSLContext] + proxy: Optional[str] + base_url: str + default_headers: Dict[str, str] + logger: logging.Logger + retry_handlers: List[RetryHandler] + + def __init__( + self, + token: str, + timeout: int = 30, + ssl: Optional[SSLContext] = None, + proxy: Optional[str] = None, + base_url: str = BASE_URL, + default_headers: Optional[Dict[str, str]] = None, + user_agent_prefix: Optional[str] = None, + user_agent_suffix: Optional[str] = None, + logger: Optional[logging.Logger] = None, + retry_handlers: Optional[List[RetryHandler]] = None, + ): + """API client for Audit Logs API + See https://docs.slack.dev/admins/audit-logs-api/ for more details + + Args: + token: An admin user's token, which starts with `xoxp-` + timeout: Request timeout (in seconds) + ssl: `ssl.SSLContext` to use for requests + proxy: Proxy URL (e.g., `localhost:9000`, `http://localhost:9000`) + base_url: The base URL for API calls + default_headers: Request headers to add to all requests + user_agent_prefix: Prefix for User-Agent header value + user_agent_suffix: Suffix for User-Agent header value + logger: Custom logger + retry_handlers: Retry handlers + """ + self.token = token + self.timeout = timeout + self.ssl = ssl + self.proxy = proxy + self.base_url = base_url + self.default_headers = default_headers if default_headers else {} + self.default_headers["User-Agent"] = get_user_agent(user_agent_prefix, user_agent_suffix) + self.logger = logger if logger is not None else logging.getLogger(__name__) + self.retry_handlers = retry_handlers if retry_handlers is not None else default_retry_handlers() + + if self.proxy is None or len(self.proxy.strip()) == 0: + env_variable = load_http_proxy_from_env(self.logger) + if env_variable is not None: + self.proxy = env_variable + + def schemas( + self, + *, + query_params: Optional[Dict[str, Any]] = None, + headers: Optional[Dict[str, str]] = None, + ) -> AuditLogsResponse: + """Returns information about the kind of objects which the Audit Logs API + returns as a list of all objects and a short description. + Authentication not required. + + Args: + query_params: Set any values if you want to add query params + headers: Additional request headers + Returns: + API response + """ + return self.api_call( + path="schemas", + query_params=query_params, + headers=headers, + ) + + def actions( + self, + *, + query_params: Optional[Dict[str, Any]] = None, + headers: Optional[Dict[str, str]] = None, + ) -> AuditLogsResponse: + """Returns information about the kind of actions that the Audit Logs API + returns as a list of all actions and a short description of each. + Authentication not required. + + Args: + query_params: Set any values if you want to add query params + headers: Additional request headers + + Returns: + API response + """ + return self.api_call( + path="actions", + query_params=query_params, + headers=headers, + ) + + def logs( + self, + *, + latest: Optional[int] = None, + oldest: Optional[int] = None, + limit: Optional[int] = None, + action: Optional[str] = None, + actor: Optional[str] = None, + entity: Optional[str] = None, + cursor: Optional[str] = None, + additional_query_params: Optional[Dict[str, Any]] = None, + headers: Optional[Dict[str, str]] = None, + ) -> AuditLogsResponse: + """This is the primary endpoint for retrieving actual audit events from your organization. + It will return a list of actions that have occurred on the installed workspace or grid organization. + Authentication required. + + The following filters can be applied in order to narrow the range of actions returned. + Filters are added as query string parameters and can be combined together. + Multiple filter parameters are additive (a boolean AND) and are separated + with an ampersand (&) in the query string. Filtering is entirely optional. + + Args: + latest: Unix timestamp of the most recent audit event to include (inclusive). + oldest: Unix timestamp of the least recent audit event to include (inclusive). + Data is not available prior to March 2018. + limit: Number of results to optimistically return, maximum 9999. + action: Name of the action. + actor: User ID who initiated the action. + entity: ID of the target entity of the action (such as a channel, workspace, organization, file). + cursor: The next page cursor of pagination + additional_query_params: Add anything else if you need to use the ones this library does not support + headers: Additional request headers + + Returns: + API response + """ + query_params = { + "latest": latest, + "oldest": oldest, + "limit": limit, + "action": action, + "actor": actor, + "entity": entity, + "cursor": cursor, + } + if additional_query_params is not None: + query_params.update(additional_query_params) + query_params = {k: v for k, v in query_params.items() if v is not None} + return self.api_call( + path="logs", + query_params=query_params, + headers=headers, + ) + + def api_call( + self, + *, + http_verb: str = "GET", + path: str, + query_params: Optional[Dict[str, Any]] = None, + body_params: Optional[Dict[str, Any]] = None, + headers: Optional[Dict[str, str]] = None, + ) -> AuditLogsResponse: + """Performs a Slack API request and returns the result.""" + url = f"{self.base_url}{path}" + query = _build_query(query_params) + if len(query) > 0: + url += f"?{query}" + + return self._perform_http_request( + http_verb=http_verb, + url=url, + body=body_params, + headers=_build_request_headers( + token=self.token, + default_headers=self.default_headers, + additional_headers=headers, + ), + ) + + def _perform_http_request( + self, + *, + http_verb: str = "GET", + url: str, + body: Optional[Dict[str, Any]] = None, + headers: Dict[str, str], + ) -> AuditLogsResponse: + if body is not None: + body = json.dumps(body) # type: ignore[assignment] + headers["Content-Type"] = "application/json;charset=utf-8" + + if self.logger.level <= logging.DEBUG: + headers_for_logging = {k: "(redacted)" if k.lower() == "authorization" else v for k, v in headers.items()} + self.logger.debug(f"Sending a request - url: {url}, body: {body}, headers: {headers_for_logging}") + + # NOTE: Intentionally ignore the `http_verb` here + # Slack APIs accepts any API method requests with POST methods + req = Request( + method=http_verb, + url=url, + data=body.encode("utf-8") if body is not None else None, # type: ignore[attr-defined] + headers=headers, + ) + resp = None + last_error = None + + retry_state = RetryState() + counter_for_safety = 0 + while counter_for_safety < 100: + counter_for_safety += 1 + # If this is a retry, the next try started here. We can reset the flag. + retry_state.next_attempt_requested = False + + try: + resp = self._perform_http_request_internal(url, req) + # The resp is a 200 OK response + return resp + + except HTTPError as e: + # read the response body here + charset = e.headers.get_content_charset() or "utf-8" + response_body: str = e.read().decode(charset) + # As adding new values to HTTPError#headers can be ignored, building a new dict object here + response_headers = dict(e.headers.items()) + resp = AuditLogsResponse( + url=url, + status_code=e.code, + raw_body=response_body, + headers=response_headers, + ) + if e.code == 429: + # for backward-compatibility with WebClient (v.2.5.0 or older) + if "retry-after" not in resp.headers and "Retry-After" in resp.headers: + resp.headers["retry-after"] = resp.headers["Retry-After"] + if "Retry-After" not in resp.headers and "retry-after" in resp.headers: + resp.headers["Retry-After"] = resp.headers["retry-after"] + _debug_log_response(self.logger, resp) + + # Try to find a retry handler for this error + retry_request = RetryHttpRequest.from_urllib_http_request(req) + retry_response = RetryHttpResponse( + status_code=e.code, + headers={k: [v] for k, v in e.headers.items()}, + data=response_body.encode("utf-8") if response_body is not None else None, + ) + for handler in self.retry_handlers: + if handler.can_retry( + state=retry_state, + request=retry_request, + response=retry_response, + error=e, + ): + if self.logger.level <= logging.DEBUG: + self.logger.info( + f"A retry handler found: {type(handler).__name__} for {req.method} {req.full_url} - {e}" + ) + handler.prepare_for_next_attempt( + state=retry_state, + request=retry_request, + response=retry_response, + error=e, + ) + break + + if retry_state.next_attempt_requested is False: + return resp + + except Exception as err: + last_error = err + self.logger.error(f"Failed to send a request to Slack API server: {err}") + + # Try to find a retry handler for this error + retry_request = RetryHttpRequest.from_urllib_http_request(req) + for handler in self.retry_handlers: + if handler.can_retry( + state=retry_state, + request=retry_request, + response=None, + error=err, + ): + if self.logger.level <= logging.DEBUG: + self.logger.info( + f"A retry handler found: {type(handler).__name__} for {req.method} {req.full_url} - {err}" + ) + handler.prepare_for_next_attempt( + state=retry_state, + request=retry_request, + response=None, + error=err, + ) + self.logger.info(f"Going to retry the same request: {req.method} {req.full_url}") + break + + if retry_state.next_attempt_requested is False: + raise err + + if resp is not None: + return resp + raise last_error # type: ignore[misc] + + def _perform_http_request_internal(self, url: str, req: Request) -> AuditLogsResponse: + opener: Optional[OpenerDirector] = None + # for security (BAN-B310) + if url.lower().startswith("http"): + if self.proxy is not None: + if isinstance(self.proxy, str): + opener = urllib.request.build_opener( + ProxyHandler({"http": self.proxy, "https": self.proxy}), + HTTPSHandler(context=self.ssl), + ) + else: + raise SlackRequestError(f"Invalid proxy detected: {self.proxy} must be a str value") + else: + raise SlackRequestError(f"Invalid URL detected: {url}") + + http_resp: HTTPResponse + if opener: + http_resp = opener.open(req, timeout=self.timeout) + else: + http_resp = urlopen(req, context=self.ssl, timeout=self.timeout) + charset: str = http_resp.headers.get_content_charset() or "utf-8" + response_body: str = http_resp.read().decode(charset) + resp = AuditLogsResponse( + url=url, + status_code=http_resp.status, + raw_body=response_body, + headers=http_resp.headers, # type: ignore[arg-type] + ) + _debug_log_response(self.logger, resp) + return resp diff --git a/slack_sdk/audit_logs/v1/internal_utils.py b/slack_sdk/audit_logs/v1/internal_utils.py new file mode 100644 index 000000000..c4521c7eb --- /dev/null +++ b/slack_sdk/audit_logs/v1/internal_utils.py @@ -0,0 +1,40 @@ +import logging +from typing import Optional, Dict, Any +from urllib.parse import quote + +from slack_sdk.web.internal_utils import get_user_agent +from .response import AuditLogsResponse + + +def _build_query(params: Optional[Dict[str, Any]]) -> str: + if params is not None and len(params) > 0: + return "&".join({f"{quote(str(k))}={quote(str(v))}" for k, v in params.items() if v is not None}) + return "" + + +def _build_request_headers( + token: str, + default_headers: Dict[str, str], + additional_headers: Optional[Dict[str, str]], +) -> Dict[str, str]: + request_headers = { + "Content-Type": "application/json;charset=utf-8", + "Authorization": f"Bearer {token}", + } + if default_headers is None or "User-Agent" not in default_headers: + request_headers["User-Agent"] = get_user_agent() + if default_headers is not None: + request_headers.update(default_headers) + if additional_headers is not None: + request_headers.update(additional_headers) + return request_headers + + +def _debug_log_response(logger, resp: AuditLogsResponse) -> None: + if logger.level <= logging.DEBUG: + logger.debug( + "Received the following response - " + f"status: {resp.status_code}, " + f"headers: {(dict(resp.headers))}, " + f"body: {resp.raw_body}" + ) diff --git a/slack_sdk/audit_logs/v1/logs.py b/slack_sdk/audit_logs/v1/logs.py new file mode 100644 index 000000000..6ace8f8ba --- /dev/null +++ b/slack_sdk/audit_logs/v1/logs.py @@ -0,0 +1,1246 @@ +import json +from typing import Optional, List, Union, Any, Dict + + +class App: + id: Optional[str] + name: Optional[str] + is_distributed: Optional[bool] + is_directory_approved: Optional[bool] + is_workflow_app: Optional[bool] + scopes: Optional[List[str]] + unknown_fields: Dict[str, Any] + + def __init__( + self, + *, + id: Optional[str] = None, + name: Optional[str] = None, + is_distributed: Optional[bool] = None, + is_directory_approved: Optional[bool] = None, + is_workflow_app: Optional[bool] = None, + scopes: Optional[List[str]] = None, + **kwargs, + ) -> None: + self.id = id + self.name = name + self.is_distributed = is_distributed + self.is_directory_approved = is_directory_approved + self.is_workflow_app = is_workflow_app + self.scopes = scopes + self.unknown_fields = kwargs + + +class User: + id: Optional[str] + name: Optional[str] + email: Optional[str] + team: Optional[str] + unknown_fields: Dict[str, Any] + + def __init__( + self, + *, + id: Optional[str] = None, + name: Optional[str] = None, + email: Optional[str] = None, + team: Optional[str] = None, + **kwargs, + ) -> None: + self.id = id + self.name = name + self.email = email + self.team = team + self.unknown_fields = kwargs + + +class Actor: + type: Optional[str] + user: Optional[User] + unknown_fields: Dict[str, Any] + + def __init__( + self, + type: Optional[str] = None, + user: Optional[Union[User, Dict[str, Any]]] = None, + **kwargs, + ) -> None: + self.type = type + self.user = User(**user) if isinstance(user, dict) else user + self.unknown_fields = kwargs + + +class Location: + type: Optional[str] + id: Optional[str] + name: Optional[str] + domain: Optional[str] + unknown_fields: Dict[str, Any] + + def __init__( + self, + *, + type: Optional[str] = None, + id: Optional[str] = None, + name: Optional[str] = None, + domain: Optional[str] = None, + **kwargs, + ) -> None: + self.type = type + self.id = id + self.name = name + self.domain = domain + self.unknown_fields = kwargs + + +class Context: + location: Optional[Location] + ua: Optional[str] + ip_address: Optional[str] + session_id: Optional[str] + app: Optional[App] + unknown_fields: Dict[str, Any] + + def __init__( + self, + *, + location: Optional[Union[Location, Dict[str, Any]]] = None, + ua: Optional[str] = None, + ip_address: Optional[str] = None, + session_id: Optional[str] = None, + app: Optional[Union[App, Dict[str, Any]]] = None, + **kwargs, + ) -> None: + self.location = Location(**location) if isinstance(location, dict) else location + self.ua = ua + self.ip_address = ip_address + self.session_id = session_id + self.app = App(**app) if isinstance(app, dict) else app + self.unknown_fields = kwargs + + +class RetentionPolicy: + type: Optional[str] + duration_days: Optional[int] + + def __init__( + self, + *, + type: Optional[str] = None, + duration_days: Optional[int] = None, + **kwargs, + ) -> None: + self.type = type + self.duration_days = duration_days + self.unknown_fields = kwargs + + +class ConversationPref: + type: Optional[List[str]] + user: Optional[List[str]] + + def __init__( + self, + *, + type: Optional[List[str]] = None, + user: Optional[List[str]] = None, + **kwargs, + ) -> None: + self.type = type + self.user = user + self.unknown_fields = kwargs + + +class FeatureEnablement: + enabled: Optional[bool] + + def __init__( + self, + *, + enabled: Optional[bool] = None, + **kwargs, + ) -> None: + self.enabled = enabled + self.unknown_fields = kwargs + + +class SharedWith: + channel_id: Optional[str] + access_level: Optional[str] + + def __init__( + self, + *, + channel_id: Optional[str] = None, + access_level: Optional[str] = None, + **kwargs, + ) -> None: + self.channel_id = channel_id + self.access_level = access_level + self.unknown_fields = kwargs + + +class Profile: + real_name: Optional[str] + first_name: Optional[str] + last_name: Optional[str] + display_name: Optional[str] + image_original: Optional[str] + image_24: Optional[str] + image_32: Optional[str] + image_48: Optional[str] + image_72: Optional[str] + image_192: Optional[str] + image_512: Optional[str] + image_1024: Optional[str] + + def __init__( + self, + *, + real_name: Optional[str] = None, + first_name: Optional[str] = None, + last_name: Optional[str] = None, + display_name: Optional[str] = None, + image_original: Optional[str] = None, + image_24: Optional[str] = None, + image_32: Optional[str] = None, + image_48: Optional[str] = None, + image_72: Optional[str] = None, + image_192: Optional[str] = None, + image_512: Optional[str] = None, + image_1024: Optional[str] = None, + **kwargs, + ) -> None: + self.real_name = real_name + self.first_name = first_name + self.last_name = last_name + self.display_name = display_name + self.image_original = image_original + self.image_24 = image_24 + self.image_32 = image_32 + self.image_48 = image_48 + self.image_72 = image_72 + self.image_192 = image_192 + self.image_512 = image_512 + self.image_1024 = image_1024 + + +class SpaceFileId: + payload: Optional[str] + + def __init__( + self, + *, + payload: Optional[str] = None, + **kwargs, + ) -> None: + self.payload = payload + + +class AttributeItems: + type: Optional[str] + + def __init__( + self, + *, + type: Optional[str] = None, + **kwargs, + ) -> None: + self.type = type + + +class Attribute: + name: Optional[str] + type: Optional[str] + items: Optional[AttributeItems] + + def __init__( + self, + *, + name: Optional[str] = None, + type: Optional[str] = None, + items: Optional[AttributeItems] = None, + **kwargs, + ) -> None: + self.name = name + self.type = type + self.items = items + + +class AAARuleActionResolution: + value: Optional[str] + + def __init__( + self, + *, + value: Optional[str] = None, + **kwargs, + ) -> None: + self.value = value + + +class AAARuleActionNotify: + entity_type: Optional[str] + + def __init__( + self, + *, + entity_type: Optional[str] = None, + **kwargs, + ) -> None: + self.entity_type = entity_type + + +class AAARuleAction: + resolution: Optional[AAARuleActionResolution] + notify: Optional[List[AAARuleActionNotify]] + + def __init__( + self, + *, + resolution: Optional[Union[Dict[str, Any], AAARuleActionResolution]] = None, + notify: Optional[List[Union[Dict[str, Any], AAARuleActionNotify]]] = None, + **kwargs, + ) -> None: + self.resolution = ( + resolution + if resolution is None or isinstance(resolution, AAARuleActionResolution) + else AAARuleActionResolution(**resolution) + ) + self.notify = None + if notify is not None: + self.notify = [] + for a in notify: + if isinstance(a, dict): + self.notify.append(AAARuleActionNotify(**a)) + else: + self.notify.append(a) + + +class AAARuleConditionValue: + field: Optional[str] + values: Optional[List[str]] + datatype: Optional[str] + operator: Optional[str] + + def __init__( + self, + *, + field: Optional[str] = None, + values: Optional[List[str]] = None, + datatype: Optional[str] = None, + operator: Optional[str] = None, + **kwargs, + ) -> None: + self.field = field + self.values = values + self.datatype = datatype + self.operator = operator + + +class AAARuleCondition: + datatype: Optional[str] + operator: Optional[str] + values: Optional[List[AAARuleConditionValue]] + entity_type: Optional[str] + + def __init__( + self, + *, + datatype: Optional[str] = None, + operator: Optional[str] = None, + values: Optional[List[Union[Dict[str, Any], AAARuleConditionValue]]] = None, + entity_type: Optional[str] = None, + **kwargs, + ) -> None: + self.datatype = datatype + self.operator = operator + self.values = None + if values is not None: + self.values = [] + for a in values: + if isinstance(a, dict): + self.values.append(AAARuleConditionValue(**a)) + else: + self.values.append(a) + self.entity_type = entity_type + + +class AAARule: + id: Optional[str] + team_id: Optional[str] + title: Optional[str] + action: Optional[AAARuleAction] + condition: Optional[AAARuleCondition] + + def __init__( + self, + *, + id: Optional[str] = None, + team_id: Optional[str] = None, + title: Optional[str] = None, + action: Optional[Union[Dict[str, Any], AAARuleAction]] = None, + condition: Optional[Union[Dict[str, Any], AAARuleCondition]] = None, + **kwargs, + ) -> None: + self.id = id + self.team_id = team_id + self.title = title + self.action = action if action is None or isinstance(action, AAARuleAction) else AAARuleAction(**action) + self.condition = ( + condition if condition is None or isinstance(condition, AAARuleCondition) else AAARuleCondition(**condition) + ) + + +class AAARequest: + id: Optional[str] + team_id: Optional[str] + + def __init__( + self, + *, + id: Optional[str] = None, + team_id: Optional[str] = None, + **kwargs, + ) -> None: + self.id = id + self.team_id = team_id + + +class Details: + name: Optional[str] + new_value: Optional[Union[str, List[str], Dict[str, Any]]] + previous_value: Optional[Union[str, List[str], Dict[str, Any]]] + expires_on: Optional[int] + mobile_only: Optional[bool] + web_only: Optional[bool] + non_sso_only: Optional[bool] + type: Optional[str] + is_workflow: Optional[bool] + inviter: Optional[User] + kicker: Optional[User] + shared_to: Optional[str] + reason: Optional[str] + origin_team: Optional[str] + target_team: Optional[str] + is_internal_integration: Optional[bool] + cleared_resolution: Optional[str] + app_owner_id: Optional[str] + bot_scopes: Optional[List[str]] + new_scopes: Optional[List[str]] + previous_scopes: Optional[List[str]] + granular_bot_token: Optional[bool] + scopes: Optional[List[str]] + scopes_bot: Optional[List[str]] + resolution: Optional[str] + app_previously_resolved: Optional[bool] + admin_app_id: Optional[str] + bot_id: Optional[str] + installer_user_id: Optional[str] + approver_id: Optional[str] + approval_type: Optional[str] + app_previously_approved: Optional[bool] + old_scopes: Optional[List[str]] + channels: Optional[List[str]] + permissions: Optional[List[Dict[str, Any]]] + new_version_id: Optional[str] + trigger: Optional[str] + export_type: Optional[str] + export_start_ts: Optional[str] + export_end_ts: Optional[str] + barrier_id: Optional[str] + primary_usergroup_id: Optional[str] + barriered_from_usergroup_ids: Optional[List[str]] + restricted_subjects: Optional[List[str]] + duration: Optional[int] + desktop_app_browser_quit: Optional[bool] + invite_id: Optional[str] + external_organization_id: Optional[str] + external_organization_name: Optional[str] + external_user_id: Optional[str] + external_user_email: Optional[str] + channel_id: Optional[str] + added_team_id: Optional[str] + unknown_fields: Dict[str, Any] + is_token_rotation_enabled_app: Optional[bool] + old_retention_policy: Optional[RetentionPolicy] + new_retention_policy: Optional[RetentionPolicy] + who_can_post: Optional[ConversationPref] + can_thread: Optional[ConversationPref] + is_external_limited: Optional[bool] + exporting_team_id: Optional[int] + session_search_start: Optional[int] + deprecation_search_end: Optional[int] + is_error: Optional[bool] + creator: Optional[str] + team: Optional[str] + app_id: Optional[str] + enable_at_here: Optional[FeatureEnablement] + enable_at_channel: Optional[FeatureEnablement] + can_huddle: Optional[FeatureEnablement] + url_private: Optional[str] + shared_with: Optional[SharedWith] + initiated_by: Optional[str] + source_team: Optional[str] + destination_team: Optional[str] + succeeded_users: Optional[List[str]] + failed_users: Optional[List[str]] + enterprise: Optional[str] + subteam: Optional[str] + action: Optional[str] + idp_group_member_count: Optional[int] + workspace_member_count: Optional[int] + added_user_count: Optional[int] + added_user_error_count: Optional[int] + reactivated_user_count: Optional[int] + removed_user_count: Optional[int] + removed_user_error_count: Optional[int] + total_removal_count: Optional[int] + is_flagged: Optional[str] + target_user: Optional[str] + idp_config_id: Optional[str] + config_type: Optional[str] + idp_entity_id_hash: Optional[str] + label: Optional[str] + previous_profile: Optional[Profile] + new_profile: Optional[Profile] + target_user_id: Optional[str] + space_file_id: Optional[SpaceFileId] + target_entity: Optional[str] + target_entity_id: Optional[str] + changed_permissions: Optional[List[str]] + datastore_name: Optional[str] + attributes: Optional[List[Attribute]] + channel: Optional[str] + entity_type: Optional[str] + actor: Optional[str] + access_level: Optional[str] + functions: Optional[List[str]] + workflows: Optional[List[str]] + datastores: Optional[List[str]] + permissions_updated: Optional[bool] + matched_rule: Optional[AAARule] + request: Optional[AAARequest] + rules_checked: Optional[List[AAARule]] + disconnecting_team: Optional[str] + is_channel_canvas: Optional[bool] + linked_channel_id: Optional[str] + column_id: Optional[str] + row_id: Optional[str] + cell_date_updated: Optional[int] + view_id: Optional[str] + user: Optional[str] + + def __init__( + self, + *, + name: Optional[str] = None, + new_value: Optional[Union[str, List[str], Dict[str, Any]]] = None, + previous_value: Optional[Union[str, List[str], Dict[str, Any]]] = None, + expires_on: Optional[int] = None, + mobile_only: Optional[bool] = None, + web_only: Optional[bool] = None, + non_sso_only: Optional[bool] = None, + type: Optional[str] = None, + is_workflow: Optional[bool] = None, + inviter: Optional[Union[Dict[str, Any], User]] = None, + kicker: Optional[Union[Dict[str, Any], User]] = None, + shared_to: Optional[str] = None, + reason: Optional[str] = None, + origin_team: Optional[str] = None, + target_team: Optional[str] = None, + is_internal_integration: Optional[bool] = None, + cleared_resolution: Optional[str] = None, + app_owner_id: Optional[str] = None, + bot_scopes: Optional[List[str]] = None, + new_scopes: Optional[List[str]] = None, + previous_scopes: Optional[List[str]] = None, + granular_bot_token: Optional[bool] = None, + scopes: Optional[List[str]] = None, + scopes_bot: Optional[List[str]] = None, + resolution: Optional[str] = None, + app_previously_resolved: Optional[bool] = None, + admin_app_id: Optional[str] = None, + bot_id: Optional[str] = None, + installer_user_id: Optional[str] = None, + approver_id: Optional[str] = None, + approval_type: Optional[str] = None, + app_previously_approved: Optional[bool] = None, + old_scopes: Optional[List[str]] = None, + channels: Optional[List[str]] = None, + permissions: Optional[List[Dict[str, Any]]] = None, + new_version_id: Optional[str] = None, + trigger: Optional[str] = None, + export_type: Optional[str] = None, + export_start_ts: Optional[str] = None, + export_end_ts: Optional[str] = None, + barrier_id: Optional[str] = None, + primary_usergroup_id: Optional[str] = None, + barriered_from_usergroup_ids: Optional[List[str]] = None, + restricted_subjects: Optional[List[str]] = None, + duration: Optional[int] = None, + desktop_app_browser_quit: Optional[bool] = None, + invite_id: Optional[str] = None, + external_organization_id: Optional[str] = None, + external_organization_name: Optional[str] = None, + external_user_id: Optional[str] = None, + external_user_email: Optional[str] = None, + channel_id: Optional[str] = None, + added_team_id: Optional[str] = None, + is_token_rotation_enabled_app: Optional[bool] = None, + old_retention_policy: Optional[Union[Dict[str, Any], RetentionPolicy]] = None, + new_retention_policy: Optional[Union[Dict[str, Any], RetentionPolicy]] = None, + who_can_post: Optional[Union[Dict[str, List[str]], ConversationPref]] = None, + can_thread: Optional[Union[Dict[str, List[str]], ConversationPref]] = None, + is_external_limited: Optional[bool] = None, + exporting_team_id: Optional[int] = None, + session_search_start: Optional[int] = None, + deprecation_search_end: Optional[int] = None, + is_error: Optional[bool] = None, + creator: Optional[str] = None, + team: Optional[str] = None, + app_id: Optional[str] = None, + enable_at_here: Optional[Union[Dict[str, Any], FeatureEnablement]] = None, + enable_at_channel: Optional[Union[Dict[str, Any], FeatureEnablement]] = None, + can_huddle: Optional[Union[Dict[str, Any], FeatureEnablement]] = None, + url_private: Optional[str] = None, + shared_with: Optional[Union[Dict[str, Any], SharedWith]] = None, + initiated_by: Optional[str] = None, + source_team: Optional[str] = None, + destination_team: Optional[str] = None, + succeeded_users: Optional[Union[List[str], str]] = None, + failed_users: Optional[Union[List[str], str]] = None, + enterprise: Optional[str] = None, + subteam: Optional[str] = None, + action: Optional[str] = None, + idp_group_member_count: Optional[int] = None, + workspace_member_count: Optional[int] = None, + added_user_count: Optional[int] = None, + added_user_error_count: Optional[int] = None, + reactivated_user_count: Optional[int] = None, + removed_user_count: Optional[int] = None, + removed_user_error_count: Optional[int] = None, + total_removal_count: Optional[int] = None, + is_flagged: Optional[str] = None, + target_user: Optional[str] = None, + idp_config_id: Optional[str] = None, + config_type: Optional[str] = None, + idp_entity_id_hash: Optional[str] = None, + label: Optional[str] = None, + previous_profile: Optional[Union[Dict[str, Any], Profile]] = None, + new_profile: Optional[Union[Dict[str, Any], Profile]] = None, + target_user_id: Optional[str] = None, + space_file_id: Optional[Union[Dict[str, Any], SpaceFileId]] = None, + target_entity: Optional[str] = None, + target_entity_id: Optional[str] = None, + changed_permissions: Optional[List[str]] = None, + datastore_name: Optional[str] = None, + attributes: Optional[List[Union[Dict[str, str], Attribute]]] = None, + channel: Optional[str] = None, + entity_type: Optional[str] = None, + actor: Optional[str] = None, + access_level: Optional[str] = None, + functions: Optional[List[str]] = None, + workflows: Optional[List[str]] = None, + datastores: Optional[List[str]] = None, + permissions_updated: Optional[bool] = None, + matched_rule: Optional[Union[Dict[str, Any], AAARule]] = None, + request: Optional[Union[Dict[str, Any], AAARequest]] = None, + rules_checked: Optional[List[Union[Dict[str, Any], AAARule]]] = None, + disconnecting_team: Optional[str] = None, + is_channel_canvas: Optional[bool] = None, + linked_channel_id: Optional[str] = None, + column_id: Optional[str] = None, + row_id: Optional[str] = None, + cell_date_updated: Optional[int] = None, + view_id: Optional[str] = None, + user: Optional[str] = None, + **kwargs, + ) -> None: + self.name = name + self.new_value = new_value + self.previous_value = previous_value + self.expires_on = expires_on + self.mobile_only = mobile_only + self.web_only = web_only + self.non_sso_only = non_sso_only + self.type = type + self.is_workflow = is_workflow + self.inviter = inviter if inviter is None or isinstance(inviter, User) else User(**inviter) + self.kicker = kicker if kicker is None or isinstance(kicker, User) else User(**kicker) + self.shared_to = shared_to + self.reason = reason + self.origin_team = origin_team + self.target_team = target_team + self.is_internal_integration = is_internal_integration + self.cleared_resolution = cleared_resolution + self.app_owner_id = app_owner_id + self.bot_scopes = bot_scopes + self.new_scopes = new_scopes + self.previous_scopes = previous_scopes + self.granular_bot_token = granular_bot_token + self.scopes = scopes + self.scopes_bot = scopes_bot + self.resolution = resolution + self.app_previously_resolved = app_previously_resolved + self.admin_app_id = admin_app_id + self.bot_id = bot_id + self.unknown_fields = kwargs + self.installer_user_id = installer_user_id + self.approver_id = approver_id + self.approval_type = approval_type + self.app_previously_approved = app_previously_approved + self.old_scopes = old_scopes + self.channels = channels + self.permissions = permissions + self.new_version_id = new_version_id + self.trigger = trigger + self.export_type = export_type + self.export_start_ts = export_start_ts + self.export_end_ts = export_end_ts + self.barrier_id = barrier_id + self.primary_usergroup_id = primary_usergroup_id + self.barriered_from_usergroup_ids = barriered_from_usergroup_ids + self.restricted_subjects = restricted_subjects + self.duration = duration + self.desktop_app_browser_quit = desktop_app_browser_quit + self.invite_id = invite_id + self.external_organization_id = external_organization_id + self.external_organization_name = external_organization_name + self.external_user_id = external_user_id + self.external_user_email = external_user_email + self.channel_id = channel_id + self.added_team_id = added_team_id + self.is_token_rotation_enabled_app = is_token_rotation_enabled_app + self.old_retention_policy = ( + old_retention_policy + if old_retention_policy is None or isinstance(old_retention_policy, RetentionPolicy) + else RetentionPolicy(**old_retention_policy) + ) + self.new_retention_policy = ( + new_retention_policy + if new_retention_policy is None or isinstance(new_retention_policy, RetentionPolicy) + else RetentionPolicy(**new_retention_policy) + ) + self.who_can_post = ( + who_can_post + if who_can_post is None or isinstance(who_can_post, ConversationPref) + else ConversationPref(**who_can_post) + ) + self.can_thread = ( + can_thread if can_thread is None or isinstance(can_thread, ConversationPref) else ConversationPref(**can_thread) + ) + self.is_external_limited = is_external_limited + self.exporting_team_id = exporting_team_id + self.session_search_start = session_search_start + self.deprecation_search_end = deprecation_search_end + self.is_error = is_error + self.creator = creator + self.team = team + self.app_id = app_id + self.enable_at_here = ( + enable_at_here + if enable_at_here is None or isinstance(enable_at_here, FeatureEnablement) + else FeatureEnablement(**enable_at_here) + ) + self.enable_at_channel = ( + enable_at_channel + if enable_at_channel is None or isinstance(enable_at_channel, FeatureEnablement) + else FeatureEnablement(**enable_at_channel) + ) + self.can_huddle = ( + can_huddle + if can_huddle is None or isinstance(can_huddle, FeatureEnablement) + else FeatureEnablement(**can_huddle) + ) + self.url_private = url_private + self.shared_with = ( + shared_with if shared_with is None or isinstance(shared_with, SharedWith) else SharedWith(**shared_with) + ) + self.initiated_by = initiated_by + self.source_team = source_team + self.destination_team = destination_team + self.succeeded_users = ( + succeeded_users if succeeded_users is None or isinstance(succeeded_users, list) else json.loads(succeeded_users) + ) + self.failed_users = ( + failed_users if failed_users is None or isinstance(failed_users, list) else json.loads(failed_users) + ) + self.enterprise = enterprise + self.subteam = subteam + self.action = action + self.idp_group_member_count = idp_group_member_count + self.workspace_member_count = workspace_member_count + self.added_user_count = added_user_count + self.added_user_error_count = added_user_error_count + self.reactivated_user_count = reactivated_user_count + self.removed_user_count = removed_user_count + self.removed_user_error_count = removed_user_error_count + self.total_removal_count = total_removal_count + self.is_flagged = is_flagged + self.target_user = target_user + self.idp_config_id = idp_config_id + self.config_type = config_type + self.idp_entity_id_hash = idp_entity_id_hash + self.label = label + self.previous_profile = ( + previous_profile + if previous_profile is None or isinstance(previous_profile, Profile) + else Profile(**previous_profile) + ) + self.new_profile = new_profile if new_profile is None or isinstance(new_profile, Profile) else Profile(**new_profile) + self.target_user_id = target_user_id + self.space_file_id = ( + space_file_id + if space_file_id is None or isinstance(space_file_id, SpaceFileId) + else SpaceFileId(**space_file_id) + ) + self.target_entity = target_entity + self.target_entity_id = target_entity_id + self.changed_permissions = changed_permissions + self.datastore_name = datastore_name + self.attributes = None + if attributes is not None: + self.attributes = [] + for a in attributes: + if isinstance(a, dict): + self.attributes.append(Attribute(**a)) # type: ignore[arg-type] + else: + self.attributes.append(a) + self.channel = channel + self.entity_type = entity_type + self.actor = actor + self.access_level = access_level + self.functions = functions + self.workflows = workflows + self.datastores = datastores + self.permissions_updated = permissions_updated + self.matched_rule = ( + matched_rule if matched_rule is None or isinstance(matched_rule, AAARule) else AAARule(**matched_rule) + ) + self.request = request if request is None or isinstance(request, AAARequest) else AAARequest(**request) + self.rules_checked = None + if rules_checked is not None: + self.rules_checked = [] + for a in rules_checked: # type: ignore[assignment] + if isinstance(a, dict): + self.rules_checked.append(AAARule(**a)) # type: ignore[arg-type] + else: + self.rules_checked.append(a) # type: ignore[arg-type] + self.disconnecting_team = disconnecting_team + self.is_channel_canvas = is_channel_canvas + self.linked_channel_id = linked_channel_id + self.column_id = column_id + self.row_id = row_id + self.cell_date_updated = cell_date_updated + self.view_id = view_id + self.user = user + + +class Channel: + id: Optional[str] + privacy: Optional[str] + name: Optional[str] + is_shared: Optional[bool] + is_org_shared: Optional[bool] + teams_shared_with: Optional[List[str]] + original_connected_channel_id: Optional[str] + is_salesforce_channel: Optional[bool] + unknown_fields: Dict[str, Any] + + def __init__( + self, + *, + id: Optional[str] = None, + privacy: Optional[str] = None, + name: Optional[str] = None, + is_shared: Optional[bool] = None, + is_org_shared: Optional[bool] = None, + teams_shared_with: Optional[List[str]] = None, + original_connected_channel_id: Optional[str] = None, + is_salesforce_channel: Optional[bool] = None, + **kwargs, + ) -> None: + self.id = id + self.privacy = privacy + self.name = name + self.is_shared = is_shared + self.is_org_shared = is_org_shared + self.teams_shared_with = teams_shared_with + self.original_connected_channel_id = original_connected_channel_id + self.is_salesforce_channel = is_salesforce_channel + self.unknown_fields = kwargs + + +class File: + id: Optional[str] + name: Optional[str] + filetype: Optional[str] + title: Optional[str] + unknown_fields: Dict[str, Any] + + def __init__( + self, + *, + id: Optional[str] = None, + name: Optional[str] = None, + filetype: Optional[str] = None, + title: Optional[str] = None, + **kwargs, + ) -> None: + self.id = id + self.name = name + self.filetype = filetype + self.title = title + self.unknown_fields = kwargs + + +class Usergroup: + id: Optional[str] + name: Optional[str] + unknown_fields: Dict[str, Any] + + def __init__( + self, + *, + id: Optional[str] = None, + name: Optional[str] = None, + **kwargs, + ) -> None: + self.id = id + self.name = name + self.unknown_fields = kwargs + + +class Message: + channel: Optional[str] + team: Optional[str] + timestamp: Optional[str] + unknown_fields: Dict[str, Any] + + def __init__( + self, + *, + channel: Optional[str] = None, + team: Optional[str] = None, + timestamp: Optional[str] = None, + **kwargs, + ) -> None: + self.channel = channel + self.team = team + self.timestamp = timestamp + self.unknown_fields = kwargs + + +class Huddle: + id: Optional[str] + date_start: Optional[int] + date_end: Optional[int] + participants: Optional[List[str]] + unknown_fields: Dict[str, Any] + + def __init__( + self, + *, + id: Optional[str] = None, + date_start: Optional[int] = None, + date_end: Optional[int] = None, + participants: Optional[List[str]] = None, + **kwargs, + ) -> None: + self.id = id + self.date_start = date_start + self.date_end = date_end + self.participants = participants + self.unknown_fields = kwargs + + +class Role: + id: Optional[str] + name: Optional[str] + type: Optional[str] + unknown_fields: Dict[str, Any] + + def __init__( + self, + *, + id: Optional[str] = None, + name: Optional[str] = None, + type: Optional[str] = None, + **kwargs, + ) -> None: + self.id = id + self.name = name + self.type = type + self.unknown_fields = kwargs + + +class Workflow: + id: Optional[str] + name: Optional[str] + domain: Optional[str] + unknown_fields: Dict[str, Any] + + def __init__( + self, + *, + id: Optional[str] = None, + name: Optional[str] = None, + domain: Optional[str] = None, + **kwargs, + ) -> None: + self.id = id + self.name = name + self.domain = domain + self.unknown_fields = kwargs + + +class InformationBarrier: + id: Optional[str] + primary_usergroup: Optional[str] + barriered_from_usergroups: Optional[List[str]] + restricted_subjects: Optional[List[str]] + unknown_fields: Dict[str, Any] + + def __init__( + self, + *, + id: Optional[str] = None, + primary_usergroup: Optional[str] = None, + barriered_from_usergroups: Optional[List[str]] = None, + restricted_subjects: Optional[List[str]] = None, + **kwargs, + ) -> None: + self.id = id + self.primary_usergroup = primary_usergroup + self.barriered_from_usergroups = barriered_from_usergroups + self.restricted_subjects = restricted_subjects + self.unknown_fields = kwargs + + +class WorkflowV2StepConfiguration: + name: Optional[str] + step_function_type: Optional[str] + step_function_app_id: Optional[str] + unknown_fields: Dict[str, Any] + + def __init__( + self, + *, + name: Optional[str] = None, + step_function_type: Optional[str] = None, + step_function_app_id: Optional[str] = None, + **kwargs, + ) -> None: + self.name = name + self.step_function_type = step_function_type + self.step_function_app_id = step_function_app_id + self.unknown_fields = kwargs + + +class WorkflowV2: + id: Optional[str] + app_id: Optional[str] + date_updated: Optional[int] + callback_id: Optional[str] + name: Optional[str] + updated_by: Optional[str] + step_configuration: Optional[List[WorkflowV2StepConfiguration]] + unknown_fields: Dict[str, Any] + + def __init__( + self, + *, + id: Optional[str] = None, + app_id: Optional[str] = None, + date_updated: Optional[int] = None, + callback_id: Optional[str] = None, + name: Optional[str] = None, + updated_by: Optional[str] = None, + step_configuration: Optional[List[Union[Dict[str, Any], WorkflowV2StepConfiguration]]] = None, + **kwargs, + ) -> None: + self.id = id + self.app_id = app_id + self.date_updated = date_updated + self.callback_id = callback_id + self.name = name + self.updated_by = updated_by + self.step_configuration = None + if step_configuration is not None: + self.step_configuration = [] + for a in step_configuration: + if isinstance(a, dict): + self.step_configuration.append(WorkflowV2StepConfiguration(**a)) + else: + self.step_configuration.append(a) + self.unknown_fields = kwargs + + +class AccountTypeRole: + id: Optional[str] + name: Optional[str] + unknown_fields: Dict[str, Any] + + def __init__( + self, + *, + id: Optional[str] = None, + name: Optional[str] = None, + **kwargs, + ) -> None: + self.id = id + self.name = name + self.unknown_fields = kwargs + + +class SlackList: + id: Optional[str] + unknown_fields: Dict[str, Any] + + def __init__( + self, + *, + id: Optional[str] = None, + **kwargs, + ) -> None: + self.id = id + self.unknown_fields = kwargs + + +class Entity: + type: Optional[str] + user: Optional[User] + workspace: Optional[Location] + enterprise: Optional[Location] + channel: Optional[Channel] + file: Optional[File] + app: Optional[App] + message: Optional[Message] + huddle: Optional[Huddle] + role: Optional[Role] + usergroup: Optional[Usergroup] + workflow: Optional[Workflow] + barrier: Optional[InformationBarrier] + workflow_v2: Optional[WorkflowV2] + account_type_role: Optional[AccountTypeRole] + list: Optional[SlackList] + unknown_fields: Dict[str, Any] + + def __init__( + self, + *, + type: Optional[str] = None, + user: Optional[Union[User, Dict[str, Any]]] = None, + workspace: Optional[Union[Location, Dict[str, Any]]] = None, + enterprise: Optional[Union[Location, Dict[str, Any]]] = None, + channel: Optional[Union[Channel, Dict[str, Any]]] = None, + file: Optional[Union[File, Dict[str, Any]]] = None, + app: Optional[Union[App, Dict[str, Any]]] = None, + message: Optional[Union[Message, Dict[str, Any]]] = None, + huddle: Optional[Union[Huddle, Dict[str, Any]]] = None, + role: Optional[Union[Role, Dict[str, Any]]] = None, + usergroup: Optional[Union[Usergroup, Dict[str, Any]]] = None, + workflow: Optional[Union[Workflow, Dict[str, Any]]] = None, + barrier: Optional[Union[InformationBarrier, Dict[str, Any]]] = None, + workflow_v2: Optional[Union[WorkflowV2, Dict[str, Any]]] = None, + account_type_role: Optional[Union[AccountTypeRole, Dict[str, Any]]] = None, + list: Optional[Union[SlackList, Dict[str, Any]]] = None, + **kwargs, + ) -> None: + self.type = type + self.user = User(**user) if isinstance(user, dict) else user + self.workspace = Location(**workspace) if isinstance(workspace, dict) else workspace + self.enterprise = Location(**enterprise) if isinstance(enterprise, dict) else enterprise + self.channel = Channel(**channel) if isinstance(channel, dict) else channel + self.file = File(**file) if isinstance(file, dict) else file + self.app = App(**app) if isinstance(app, dict) else app + self.message = Message(**message) if isinstance(message, dict) else message + self.huddle = Huddle(**huddle) if isinstance(huddle, dict) else huddle + self.role = Role(**role) if isinstance(role, dict) else role + self.usergroup = Usergroup(**usergroup) if isinstance(usergroup, dict) else usergroup + self.workflow = Workflow(**workflow) if isinstance(workflow, dict) else workflow + self.barrier = InformationBarrier(**barrier) if isinstance(barrier, dict) else barrier + self.workflow_v2 = WorkflowV2(**workflow_v2) if isinstance(workflow_v2, dict) else workflow_v2 + self.account_type_role = ( + AccountTypeRole(**account_type_role) if isinstance(account_type_role, dict) else account_type_role + ) + self.list = SlackList(**list) if isinstance(list, dict) else list + self.unknown_fields = kwargs + + +class Entry: + id: Optional[str] + date_create: Optional[int] + action: Optional[str] + actor: Optional[Actor] + entity: Optional[Entity] + context: Optional[Context] + details: Optional[Details] + unknown_fields: Dict[str, Any] + + def __init__( + self, + *, + id: Optional[str] = None, + date_create: Optional[int] = None, + action: Optional[str] = None, + actor: Optional[Union[Actor, Dict[str, Any]]] = None, + entity: Optional[Union[Entity, Dict[str, Any]]] = None, + context: Optional[Union[Context, Dict[str, Any]]] = None, + details: Optional[Union[Details, Dict[str, Any]]] = None, + **kwargs, + ) -> None: + self.id = id + self.date_create = date_create + self.action = action + self.actor = Actor(**actor) if isinstance(actor, dict) else actor + self.entity = Entity(**entity) if isinstance(entity, dict) else entity + self.context = Context(**context) if isinstance(context, dict) else context + self.details = Details(**details) if isinstance(details, dict) else details + self.unknown_fields = kwargs + + +class ResponseMetadata: + next_cursor: Optional[str] + unknown_fields: Dict[str, Any] + + def __init__( + self, + *, + next_cursor: Optional[str] = None, + **kwargs, + ) -> None: + self.next_cursor = next_cursor + self.unknown_fields = kwargs + + +class LogsResponse: + entries: Optional[List[Entry]] + response_metadata: Optional[ResponseMetadata] + ok: Optional[bool] + error: Optional[str] + needed: Optional[str] + provided: Optional[str] + unknown_fields: Dict[str, Any] + + def __init__( + self, + *, + entries: Optional[List[Union[Entry, Dict[str, Any]]]] = None, + response_metadata: Optional[Union[ResponseMetadata, Dict[str, Any]]] = None, + ok: Optional[bool] = None, + error: Optional[str] = None, + needed: Optional[str] = None, + provided: Optional[str] = None, + **kwargs, + ) -> None: + self.entries = [Entry(**e) if isinstance(e, dict) else e for e in entries] # type: ignore[union-attr] + self.response_metadata = ( + ResponseMetadata(**response_metadata) if isinstance(response_metadata, dict) else response_metadata + ) + self.ok = ok + self.error = error + self.needed = needed + self.provided = provided + self.unknown_fields = kwargs diff --git a/slack_sdk/audit_logs/v1/response.py b/slack_sdk/audit_logs/v1/response.py new file mode 100644 index 000000000..a2e0705d5 --- /dev/null +++ b/slack_sdk/audit_logs/v1/response.py @@ -0,0 +1,34 @@ +import json +from typing import Dict, Any, Optional + +from slack_sdk.audit_logs.v1.logs import LogsResponse + + +# TODO: Unlike WebClient's responses, this class has not yet provided __iter__ method +class AuditLogsResponse: + url: str + status_code: int + headers: Dict[str, Any] + raw_body: Optional[str] + body: Optional[Dict[str, Any]] + typed_body: Optional[LogsResponse] + + @property # type: ignore[no-redef] + def typed_body(self) -> Optional[LogsResponse]: + if self.body is None: + return None + return LogsResponse(**self.body) + + def __init__( + self, + *, + url: str, + status_code: int, + raw_body: Optional[str], + headers: dict, + ): + self.url = url + self.status_code = status_code + self.headers = headers + self.raw_body = raw_body + self.body = json.loads(raw_body) if raw_body is not None and raw_body.startswith("{") else None diff --git a/slack_sdk/errors/__init__.py b/slack_sdk/errors/__init__.py new file mode 100644 index 000000000..51b9a04f6 --- /dev/null +++ b/slack_sdk/errors/__init__.py @@ -0,0 +1,58 @@ +"""Errors that can be raised by this SDK""" + + +class SlackClientError(Exception): + """Base class for Client errors""" + + +class BotUserAccessError(SlackClientError): + """Error raised when an 'xoxb-*' token is + being used for a Slack API method that only accepts 'xoxp-*' tokens. + """ + + +class SlackRequestError(SlackClientError): + """Error raised when there's a problem with the request that's being submitted.""" + + +class SlackApiError(SlackClientError): + """Error raised when Slack does not send the expected response. + + Attributes: + response (SlackResponse): The SlackResponse object containing all of the data sent back from the API. + + Note: + The message (str) passed into the exception is used when + a user converts the exception to a str. + i.e. str(SlackApiError("This text will be sent as a string.")) + """ + + def __init__(self, message, response): + msg = f"{message}\nThe server responded with: {response}" + self.response = response + super(SlackApiError, self).__init__(msg) + + +class SlackTokenRotationError(SlackClientError): + """Error raised when the oauth.v2.access call for token rotation fails""" + + api_error: SlackApiError + + def __init__(self, api_error: SlackApiError): + self.api_error = api_error + + +class SlackClientNotConnectedError(SlackClientError): + """Error raised when attempting to send messages over the websocket when the + connection is closed.""" + + +class SlackObjectFormationError(SlackClientError): + """Error raised when a constructed object is not valid/malformed""" + + +class SlackClientConfigurationError(SlackClientError): + """Error raised because of invalid configuration on the client side: + * when attempting to send messages over the websocket when the connection is closed. + * when external system (e.g., Amazon S3) configuration / credentials are not correct + """ diff --git a/slack_sdk/http_retry/__init__.py b/slack_sdk/http_retry/__init__.py new file mode 100644 index 000000000..b7116dad9 --- /dev/null +++ b/slack_sdk/http_retry/__init__.py @@ -0,0 +1,48 @@ +from typing import List + +from .handler import RetryHandler +from .builtin_handlers import ( + ConnectionErrorRetryHandler, + RateLimitErrorRetryHandler, +) +from .interval_calculator import RetryIntervalCalculator +from .builtin_interval_calculators import ( + FixedValueRetryIntervalCalculator, + BackoffRetryIntervalCalculator, +) +from .jitter import Jitter +from .request import HttpRequest +from .response import HttpResponse +from .state import RetryState + +connect_error_retry_handler = ConnectionErrorRetryHandler() +rate_limit_error_retry_handler = RateLimitErrorRetryHandler() + + +def default_retry_handlers() -> List[RetryHandler]: + return [connect_error_retry_handler] + + +def all_builtin_retry_handlers() -> List[RetryHandler]: + return [ + connect_error_retry_handler, + rate_limit_error_retry_handler, + ] + + +__all__ = [ + "RetryHandler", + "ConnectionErrorRetryHandler", + "RateLimitErrorRetryHandler", + "RetryIntervalCalculator", + "FixedValueRetryIntervalCalculator", + "BackoffRetryIntervalCalculator", + "Jitter", + "HttpRequest", + "HttpResponse", + "RetryState", + "connect_error_retry_handler", + "rate_limit_error_retry_handler", + "default_retry_handlers", + "all_builtin_retry_handlers", +] diff --git a/slack_sdk/http_retry/async_handler.py b/slack_sdk/http_retry/async_handler.py new file mode 100644 index 000000000..ed9f6115a --- /dev/null +++ b/slack_sdk/http_retry/async_handler.py @@ -0,0 +1,89 @@ +"""asyncio compatible RetryHandler interface. +You can pass an array of handlers to customize retry logics in supported API clients. +""" + +import asyncio +from typing import Optional + +from slack_sdk.http_retry.state import RetryState +from slack_sdk.http_retry.request import HttpRequest +from slack_sdk.http_retry.response import HttpResponse +from slack_sdk.http_retry.interval_calculator import RetryIntervalCalculator +from slack_sdk.http_retry.builtin_interval_calculators import ( + BackoffRetryIntervalCalculator, +) + +default_interval_calculator = BackoffRetryIntervalCalculator() + + +class AsyncRetryHandler: + """asyncio compatible RetryHandler interface. + You can pass an array of handlers to customize retry logics in supported API clients. + """ + + max_retry_count: int + interval_calculator: RetryIntervalCalculator + + def __init__( + self, + max_retry_count: int = 1, + interval_calculator: RetryIntervalCalculator = default_interval_calculator, + ): + """RetryHandler interface. + + Args: + max_retry_count: The maximum times to do retries + interval_calculator: Pass an interval calculator for customizing the logic + """ + self.max_retry_count = max_retry_count + self.interval_calculator = interval_calculator + + async def can_retry_async( + self, + *, + state: RetryState, + request: HttpRequest, + response: Optional[HttpResponse] = None, + error: Optional[Exception] = None, + ) -> bool: + if state.current_attempt >= self.max_retry_count: + return False + return await self._can_retry_async( + state=state, + request=request, + response=response, + error=error, + ) + + async def _can_retry_async( + self, + *, + state: RetryState, + request: HttpRequest, + response: Optional[HttpResponse] = None, + error: Optional[Exception] = None, + ) -> bool: + raise NotImplementedError() + + async def prepare_for_next_attempt_async( + self, + *, + state: RetryState, + request: HttpRequest, + response: Optional[HttpResponse] = None, + error: Optional[Exception] = None, + ) -> None: + state.next_attempt_requested = True + duration = self.interval_calculator.calculate_sleep_duration(state.current_attempt) + await asyncio.sleep(duration) + state.increment_current_attempt() + + +__all__ = [ + "RetryState", + "HttpRequest", + "HttpResponse", + "RetryIntervalCalculator", + "BackoffRetryIntervalCalculator", + "default_interval_calculator", +] diff --git a/slack_sdk/http_retry/builtin_async_handlers.py b/slack_sdk/http_retry/builtin_async_handlers.py new file mode 100644 index 000000000..0d99d67a7 --- /dev/null +++ b/slack_sdk/http_retry/builtin_async_handlers.py @@ -0,0 +1,111 @@ +import asyncio +import random +from typing import Optional, List, Type + +from aiohttp import ServerDisconnectedError, ServerConnectionError, ClientOSError + +from slack_sdk.http_retry.async_handler import AsyncRetryHandler +from slack_sdk.http_retry.interval_calculator import RetryIntervalCalculator +from slack_sdk.http_retry.state import RetryState +from slack_sdk.http_retry.request import HttpRequest +from slack_sdk.http_retry.response import HttpResponse +from slack_sdk.http_retry.handler import default_interval_calculator + + +class AsyncConnectionErrorRetryHandler(AsyncRetryHandler): + """RetryHandler that does retries for connectivity issues.""" + + def __init__( + self, + max_retry_count: int = 1, + interval_calculator: RetryIntervalCalculator = default_interval_calculator, + error_types: List[Type[Exception]] = [ + ServerConnectionError, + ServerDisconnectedError, + # ClientOSError: [Errno 104] Connection reset by peer + ClientOSError, + ], + ): + super().__init__(max_retry_count, interval_calculator) + self.error_types_to_do_retries = error_types + + async def _can_retry_async( + self, + *, + state: RetryState, + request: HttpRequest, + response: Optional[HttpResponse] = None, + error: Optional[Exception] = None, + ) -> bool: + if error is None: + return False + + for error_type in self.error_types_to_do_retries: + if isinstance(error, error_type): + return True + return False + + +class AsyncRateLimitErrorRetryHandler(AsyncRetryHandler): + """RetryHandler that does retries for rate limited errors.""" + + async def _can_retry_async( + self, + *, + state: RetryState, + request: HttpRequest, + response: Optional[HttpResponse] = None, + error: Optional[Exception] = None, + ) -> bool: + return response is not None and response.status_code == 429 + + async def prepare_for_next_attempt_async( + self, + *, + state: RetryState, + request: HttpRequest, + response: Optional[HttpResponse] = None, + error: Optional[Exception] = None, + ) -> None: + if response is None: + raise error # type: ignore[misc] + + state.next_attempt_requested = True + retry_after_header_name: Optional[str] = None + for k in response.headers.keys(): + if k.lower() == "retry-after": + retry_after_header_name = k + break + duration = 1 + if retry_after_header_name is None: + # This situation usually does not arise. Just in case. + duration += random.random() # type: ignore[assignment] + else: + duration = int(response.headers.get(retry_after_header_name)[0]) + random.random() # type: ignore[assignment, index] # noqa: E501 + await asyncio.sleep(duration) + state.increment_current_attempt() + + +class AsyncServerErrorRetryHandler(AsyncRetryHandler): + """RetryHandler that does retries for server errors.""" + + def __init__( + self, + max_retry_count: int = 1, + interval_calculator: RetryIntervalCalculator = default_interval_calculator, + ): + super().__init__(max_retry_count, interval_calculator) + + async def _can_retry_async( + self, + *, + state: RetryState, + request: HttpRequest, + response: Optional[HttpResponse] = None, + error: Optional[Exception] = None, + ) -> bool: + return response is not None and response.status_code in [500, 503] + + +def async_default_handlers() -> List[AsyncRetryHandler]: + return [AsyncConnectionErrorRetryHandler()] diff --git a/slack_sdk/http_retry/builtin_handlers.py b/slack_sdk/http_retry/builtin_handlers.py new file mode 100644 index 000000000..8755d7c89 --- /dev/null +++ b/slack_sdk/http_retry/builtin_handlers.py @@ -0,0 +1,110 @@ +import random +import time +from http.client import RemoteDisconnected +from typing import Optional, List, Type +from urllib.error import URLError + +from slack_sdk.http_retry.interval_calculator import RetryIntervalCalculator +from slack_sdk.http_retry.state import RetryState +from slack_sdk.http_retry.request import HttpRequest +from slack_sdk.http_retry.response import HttpResponse +from slack_sdk.http_retry.handler import RetryHandler, default_interval_calculator + + +class ConnectionErrorRetryHandler(RetryHandler): + """RetryHandler that does retries for connectivity issues.""" + + def __init__( + self, + max_retry_count: int = 1, + interval_calculator: RetryIntervalCalculator = default_interval_calculator, + error_types: List[Type[Exception]] = [ + # To cover URLError: + URLError, + ConnectionResetError, + RemoteDisconnected, + ], + ): + super().__init__(max_retry_count, interval_calculator) + self.error_types_to_do_retries = error_types + + def _can_retry( + self, + *, + state: RetryState, + request: HttpRequest, + response: Optional[HttpResponse] = None, + error: Optional[Exception] = None, + ) -> bool: + if error is None: + return False + + if isinstance(error, URLError): + if response is not None: + return False # status 40x + + for error_type in self.error_types_to_do_retries: + if isinstance(error, error_type): + return True + return False + + +class RateLimitErrorRetryHandler(RetryHandler): + """RetryHandler that does retries for rate limited errors.""" + + def _can_retry( + self, + *, + state: RetryState, + request: HttpRequest, + response: Optional[HttpResponse] = None, + error: Optional[Exception] = None, + ) -> bool: + return response is not None and response.status_code == 429 + + def prepare_for_next_attempt( + self, + *, + state: RetryState, + request: HttpRequest, + response: Optional[HttpResponse] = None, + error: Optional[Exception] = None, + ) -> None: + if response is None: + raise error # type: ignore[misc] + + state.next_attempt_requested = True + retry_after_header_name: Optional[str] = None + for k in response.headers.keys(): + if k.lower() == "retry-after": + retry_after_header_name = k + break + duration = 1 + if retry_after_header_name is None: + # This situation usually does not arise. Just in case. + duration += random.random() # type: ignore[assignment] + else: + duration = int(response.headers.get(retry_after_header_name)[0]) + random.random() # type: ignore[index, assignment] # noqa: E501 + time.sleep(duration) + state.increment_current_attempt() + + +class ServerErrorRetryHandler(RetryHandler): + """RetryHandler that does retries for server errors.""" + + def __init__( + self, + max_retry_count: int = 1, + interval_calculator: RetryIntervalCalculator = default_interval_calculator, + ): + super().__init__(max_retry_count, interval_calculator) + + def _can_retry( + self, + *, + state: RetryState, + request: HttpRequest, + response: Optional[HttpResponse] = None, + error: Optional[Exception] = None, + ) -> bool: + return response is not None and response.status_code in [500, 503] diff --git a/slack_sdk/http_retry/builtin_interval_calculators.py b/slack_sdk/http_retry/builtin_interval_calculators.py new file mode 100644 index 000000000..6354171f5 --- /dev/null +++ b/slack_sdk/http_retry/builtin_interval_calculators.py @@ -0,0 +1,44 @@ +from typing import Optional +from .jitter import Jitter, RandomJitter +from .interval_calculator import RetryIntervalCalculator + + +class FixedValueRetryIntervalCalculator(RetryIntervalCalculator): + """Retry interval calculator that uses a fixed value.""" + + fixed_interval: float + + def __init__(self, fixed_internal: float = 0.5): + """Retry interval calculator that uses a fixed value. + + Args: + fixed_internal: The fixed interval seconds + """ + self.fixed_interval = fixed_internal + + def calculate_sleep_duration(self, current_attempt: int) -> float: + return self.fixed_interval + + +class BackoffRetryIntervalCalculator(RetryIntervalCalculator): + """Retry interval calculator that calculates in the manner of Exponential Backoff And Jitter + see also: https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/ + """ + + backoff_factor: float + jitter: Jitter + + def __init__(self, backoff_factor: float = 0.5, jitter: Optional[Jitter] = None): + """Retry interval calculator that calculates in the manner of Exponential Backoff And Jitter + + Args: + backoff_factor: The factor for the backoff interval calculation + jitter: The jitter logic implementation + """ + self.backoff_factor = backoff_factor + self.jitter = jitter if jitter is not None else RandomJitter() + + def calculate_sleep_duration(self, current_attempt: int) -> float: + interval = self.backoff_factor * (2 ** (current_attempt)) + sleep_duration = self.jitter.recalculate(interval) + return sleep_duration diff --git a/slack_sdk/http_retry/handler.py b/slack_sdk/http_retry/handler.py new file mode 100644 index 000000000..7c8aa46bd --- /dev/null +++ b/slack_sdk/http_retry/handler.py @@ -0,0 +1,80 @@ +"""RetryHandler interface. +You can pass an array of handlers to customize retry logics in supported API clients. +""" + +import time +from typing import Optional + +from slack_sdk.http_retry.state import RetryState +from slack_sdk.http_retry.request import HttpRequest +from slack_sdk.http_retry.response import HttpResponse +from slack_sdk.http_retry.interval_calculator import RetryIntervalCalculator +from slack_sdk.http_retry.builtin_interval_calculators import ( + BackoffRetryIntervalCalculator, +) + +default_interval_calculator = BackoffRetryIntervalCalculator() + + +# Note that you cannot add aiohttp to this class as the external dependency is optional +class RetryHandler: + """RetryHandler interface. + You can pass an array of handlers to customize retry logics in supported API clients. + """ + + max_retry_count: int + interval_calculator: RetryIntervalCalculator + + def __init__( + self, + max_retry_count: int = 1, + interval_calculator: RetryIntervalCalculator = default_interval_calculator, + ): + """RetryHandler interface. + + Args: + max_retry_count: The maximum times to do retries + interval_calculator: Pass an interval calculator for customizing the logic + """ + self.max_retry_count = max_retry_count + self.interval_calculator = interval_calculator + + def can_retry( + self, + *, + state: RetryState, + request: HttpRequest, + response: Optional[HttpResponse] = None, + error: Optional[Exception] = None, + ) -> bool: + if state.current_attempt >= self.max_retry_count: + return False + return self._can_retry( + state=state, + request=request, + response=response, + error=error, + ) + + def _can_retry( + self, + *, + state: RetryState, + request: HttpRequest, + response: Optional[HttpResponse] = None, + error: Optional[Exception] = None, + ) -> bool: + raise NotImplementedError() + + def prepare_for_next_attempt( + self, + *, + state: RetryState, + request: HttpRequest, + response: Optional[HttpResponse] = None, + error: Optional[Exception] = None, + ) -> None: + state.next_attempt_requested = True + duration = self.interval_calculator.calculate_sleep_duration(state.current_attempt) + time.sleep(duration) + state.increment_current_attempt() diff --git a/slack_sdk/http_retry/interval_calculator.py b/slack_sdk/http_retry/interval_calculator.py new file mode 100644 index 000000000..3911dd338 --- /dev/null +++ b/slack_sdk/http_retry/interval_calculator.py @@ -0,0 +1,12 @@ +class RetryIntervalCalculator: + """Retry interval calculator interface.""" + + def calculate_sleep_duration(self, current_attempt: int) -> float: + """Calculates an interval duration in seconds. + + Args: + current_attempt: the number of the current attempt (zero-origin; 0 means no retries are done so far) + Returns: + calculated interval duration in seconds + """ + raise NotImplementedError() diff --git a/slack_sdk/http_retry/jitter.py b/slack_sdk/http_retry/jitter.py new file mode 100644 index 000000000..852eaac60 --- /dev/null +++ b/slack_sdk/http_retry/jitter.py @@ -0,0 +1,24 @@ +import random + + +class Jitter: + """Jitter interface""" + + def recalculate(self, duration: float) -> float: + """Recalculate the given duration. + see also: https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/ + + Args: + duration: the duration in seconds + + Returns: + A new duration that the jitter amount is added + """ + raise NotImplementedError() + + +class RandomJitter(Jitter): + """Random jitter implementation""" + + def recalculate(self, duration: float) -> float: + return duration + random.random() diff --git a/slack_sdk/http_retry/request.py b/slack_sdk/http_retry/request.py new file mode 100644 index 000000000..420c7181a --- /dev/null +++ b/slack_sdk/http_retry/request.py @@ -0,0 +1,36 @@ +from typing import Dict, Optional, List, Union, Any +from urllib.request import Request + + +class HttpRequest: + """HTTP request representation""" + + method: str + url: str + headers: Dict[str, Union[str, List[str]]] + body_params: Optional[Dict[str, Any]] + data: Optional[bytes] + + def __init__( + self, + *, + method: str, + url: str, + headers: Dict[str, Union[str, List[str]]], + body_params: Optional[Dict[str, Any]] = None, + data: Optional[bytes] = None, + ): + self.method = method + self.url = url + self.headers = {k: v if isinstance(v, list) else [v] for k, v in headers.items()} + self.body_params = body_params + self.data = data + + @classmethod + def from_urllib_http_request(cls, req: Request) -> "HttpRequest": + return HttpRequest( + method=req.method, # type: ignore[arg-type] + url=req.full_url, + headers={k: v if isinstance(v, list) else [v] for k, v in req.headers.items()}, + data=req.data, # type: ignore[arg-type] + ) diff --git a/slack_sdk/http_retry/response.py b/slack_sdk/http_retry/response.py new file mode 100644 index 000000000..cb3ca6cef --- /dev/null +++ b/slack_sdk/http_retry/response.py @@ -0,0 +1,23 @@ +from typing import Dict, Optional, List, Union, Any + + +class HttpResponse: + """HTTP response representation""" + + status_code: int + headers: Dict[str, Union[List[str], str]] + body: Optional[Dict[str, Any]] + data: Optional[bytes] + + def __init__( + self, + *, + status_code: Union[int, str], + headers: Dict[str, Union[str, List[str]]], + body: Optional[Dict[str, Any]] = None, + data: Optional[bytes] = None, + ): + self.status_code = int(status_code) + self.headers = {k: v if isinstance(v, list) else [v] for k, v in headers.items()} + self.body = body + self.data = data diff --git a/slack_sdk/http_retry/state.py b/slack_sdk/http_retry/state.py new file mode 100644 index 000000000..4217eb571 --- /dev/null +++ b/slack_sdk/http_retry/state.py @@ -0,0 +1,21 @@ +from typing import Optional, Any, Dict + + +class RetryState: + next_attempt_requested: bool + current_attempt: int # zero-origin + custom_values: Optional[Dict[str, Any]] + + def __init__( + self, + *, + current_attempt: int = 0, + custom_values: Optional[Dict[str, Any]] = None, + ): + self.next_attempt_requested = False + self.current_attempt = current_attempt + self.custom_values = custom_values + + def increment_current_attempt(self) -> int: + self.current_attempt += 1 + return self.current_attempt diff --git a/slack_sdk/models/__init__.py b/slack_sdk/models/__init__.py new file mode 100644 index 000000000..25ecfe998 --- /dev/null +++ b/slack_sdk/models/__init__.py @@ -0,0 +1,57 @@ +"""Classes for constructing Slack-specific data structure""" + +import logging +from typing import Union, Dict, Any, Sequence, List + +from .basic_objects import BaseObject +from .basic_objects import EnumValidator +from .basic_objects import JsonObject +from .basic_objects import JsonValidator + + +# NOTE: used only for legacy components - don't use this for Block Kit +def extract_json( + item_or_items: Union[JsonObject, Sequence[JsonObject]], *format_args +) -> Union[Dict[Any, Any], List[Dict[Any, Any]], Sequence[JsonObject]]: + """ + Given a sequence (or single item), attempt to call the to_dict() method on each + item and return a plain list. If item is not the expected type, return it + unmodified, in case it's already a plain dict or some other user created class. + + Args: + item_or_items: item(s) to go through + format_args: Any formatting specifiers to pass into the object's to_dict + method + """ + try: + return [ + elem.to_dict(*format_args) if isinstance(elem, JsonObject) else elem + for elem in item_or_items # type: ignore[union-attr] + ] + except TypeError: # not iterable, so try returning it as a single item + return item_or_items.to_dict(*format_args) if isinstance(item_or_items, JsonObject) else item_or_items + + +def show_unknown_key_warning(name: Union[str, object], others: dict): + if "type" in others: + others.pop("type") + if len(others) > 0: + keys = ", ".join(others.keys()) + logger = logging.getLogger(__name__) + if isinstance(name, object): + name = name.__class__.__name__ + logger.debug( + f"!!! {name}'s constructor args ({keys}) were ignored." + f"If they should be supported by this library, report this issue to the project :bow: " + f"https://github.com/slackapi/python-slack-sdk/issues" + ) + + +__all__ = [ + "BaseObject", + "EnumValidator", + "JsonObject", + "JsonValidator", + "extract_json", + "show_unknown_key_warning", +] diff --git a/slack_sdk/models/attachments/__init__.py b/slack_sdk/models/attachments/__init__.py new file mode 100644 index 000000000..85695cfb1 --- /dev/null +++ b/slack_sdk/models/attachments/__init__.py @@ -0,0 +1,594 @@ +import re +from abc import ABCMeta, abstractmethod +from typing import List, Optional, Set, Sequence + +from slack_sdk.models import extract_json +from slack_sdk.models.basic_objects import ( + EnumValidator, + JsonObject, + JsonValidator, +) +from slack_sdk.models.blocks import ( + Block, + Option, + ConfirmObject, + ButtonStyles, + DynamicSelectElementTypes, +) + + +class Action(JsonObject): + """Action in attachments + https://docs.slack.dev/messaging/formatting-message-text/#rich-layouts + https://docs.slack.dev/legacy/legacy-messaging/legacy-interactive-message-field-guide/#message_action_fields + """ + + attributes = {"name", "text", "url"} + + def __init__( + self, + *, + text: str, + subtype: str, + name: Optional[str] = None, + url: Optional[str] = None, + ): + self.name = name + self.url = url + self.text = text + self.subtype = subtype + + @JsonValidator("name or url attribute is required") + def name_or_url_present(self): + return self.name is not None or self.url is not None + + def to_dict(self) -> dict: + json = super().to_dict() + json["type"] = self.subtype + return json + + +class ActionButton(Action): + @property + def attributes(self): + return super().attributes.union({"style", "value"}) + + value_max_length = 2000 + + def __init__( + self, + *, + name: str, + text: str, + value: str, + confirm: Optional[ConfirmObject] = None, + style: Optional[str] = None, + ): + """Simple button for use inside attachments + + https://docs.slack.dev/legacy/legacy-messaging/legacy-message-buttons/ + + Args: + name: Name this specific action. The name will be returned to your + Action URL along with the message's callback_id when this action is + invoked. Use it to identify this particular response path. + text: The user-facing label for the message button or menu + representing this action. Cannot contain markup. + value: Provide a string identifying this specific action. It will be + sent to your Action URL along with the name and attachment's + callback_id . If providing multiple actions with the same name, value + can be strategically used to differentiate intent. Cannot exceed 2000 + characters. + confirm: a ConfirmObject that will appear in a dialog to confirm + user's choice. + style: Leave blank to indicate that this is an ordinary button. Use + "primary" or "danger" to mark important buttons. + """ + super().__init__(name=name, text=text, subtype="button") + self.value = value + self.confirm = confirm + self.style = style + + @JsonValidator(f"value attribute cannot exceed {value_max_length} characters") + def value_length(self): + return len(self.value) <= self.value_max_length + + @EnumValidator("style", ButtonStyles) + def style_valid(self): + return self.style is None or self.style in ButtonStyles + + def to_dict(self) -> dict: + json = super().to_dict() + if self.confirm is not None: + json["confirm"] = extract_json(self.confirm, "action") + return json + + +class ActionLinkButton(Action): + def __init__(self, *, text: str, url: str): + """A simple interactive button that just opens a URL + + https://docs.slack.dev/messaging/formatting-message-text/#rich-layouts + + Args: + text: text to display on the button, eg 'Click Me!" + url: the URL to open + """ + super().__init__(text=text, url=url, subtype="button") + + +class AbstractActionSelector(Action, metaclass=ABCMeta): + DataSourceTypes = DynamicSelectElementTypes.union({"external", "static"}) + + attributes = {"data_source", "name", "text", "type"} + + @property + @abstractmethod + def data_source(self) -> str: + pass + + def __init__(self, *, name: str, text: str, selected_option: Optional[Option] = None): + super().__init__(text=text, name=name, subtype="select") + self.selected_option = selected_option + + @EnumValidator("data_source", DataSourceTypes) + def data_source_valid(self): + return self.data_source in self.DataSourceTypes + + def to_dict(self) -> dict: + json = super().to_dict() + if self.selected_option is not None: + # this is a special case for ExternalActionSelectElement - in that case, + # you pass the initial value of the selector as a selected_options array + json["selected_options"] = extract_json([self.selected_option], "action") + return json + + +class ActionUserSelector(AbstractActionSelector): + data_source = "users" + + def __init__(self, name: str, text: str, selected_user: Optional[Option] = None): + """Automatically populate the selector with a list of users in the workspace. + + https://docs.slack.dev/legacy/legacy-messaging/legacy-adding-menus-to-messages/#menu_team_members + + Args: + name: Name this specific action. The name will be returned to your + Action URL along with the message's callback_id when this action is + invoked. Use it to identify this particular response path. + text: The user-facing label for the message button or menu + representing this action. Cannot contain markup. + selected_user: An Option object to pre-select as the default + value. + """ + super().__init__(name=name, text=text, selected_option=selected_user) + + +class ActionChannelSelector(AbstractActionSelector): + data_source = "channels" + + def __init__(self, name: str, text: str, selected_channel: Optional[Option] = None): + """ + Automatically populate the selector with a list of public channels in the + workspace. + + https://docs.slack.dev/legacy/legacy-messaging/legacy-adding-menus-to-messages/#menu_channels + + Args: + name: Name this specific action. The name will be returned to your + Action URL along with the message's callback_id when this action is + invoked. Use it to identify this particular response path. + text: The user-facing label for the message button or menu + representing this action. Cannot contain markup. + selected_channel: An Option object to pre-select as the default + value. + """ + super().__init__(name=name, text=text, selected_option=selected_channel) + + +class ActionConversationSelector(AbstractActionSelector): + data_source = "conversations" + + def __init__(self, name: str, text: str, selected_conversation: Optional[Option] = None): + """ + Automatically populate the selector with a list of conversations they have in + the workspace. + + https://docs.slack.dev/legacy/legacy-messaging/legacy-adding-menus-to-messages/#menu_conversations + + Args: + name: Name this specific action. The name will be returned to your + Action URL along with the message's callback_id when this action is + invoked. Use it to identify this particular response path. + text: The user-facing label for the message button or menu + representing this action. Cannot contain markup. + selected_conversation: An Option object to pre-select as the default + value. + """ + super().__init__(name=name, text=text, selected_option=selected_conversation) + + +class ActionExternalSelector(AbstractActionSelector): + data_source = "external" + + @property + def attributes(self) -> Set[str]: # type: ignore[override] + return super().attributes.union({"min_query_length"}) + + def __init__( + self, + *, + name: str, + text: str, + selected_option: Optional[Option] = None, + min_query_length: Optional[int] = None, + ): + """ + Populate a message select menu from your own application dynamically. + + https://docs.slack.dev/legacy/legacy-messaging/legacy-adding-menus-to-messages/#menu_dynamic + + Args: + name: Name this specific action. The name will be returned to your + Action URL along with the message's callback_id when this action is + invoked. Use it to identify this particular response path. + text: The user-facing label for the message button or menu + representing this action. Cannot contain markup. + selected_option: An Option object to pre-select as the default + value. + min_query_length: Specify the number of characters that must be typed + by a user into a dynamic select menu before dispatching to the app. + """ + super().__init__(name=name, text=text, selected_option=selected_option) + self.min_query_length = min_query_length + + +SeededColors = {"danger", "good", "warning"} + + +class AttachmentField(JsonObject): + attributes = {"short", "title", "value"} + + def __init__( + self, + *, + title: Optional[str] = None, + value: Optional[str] = None, + short: bool = True, + ): + self.title = title + self.value = value + self.short = short + + +class Attachment(JsonObject): + attributes = { + "author_icon", + "author_link", + "author_name", + "author_subname", + "color", + "fallback", + "fields", + "footer", + "footer_icon", + "image_url", + "pretext", + "text", + "thumb_url", + "title", + "title_link", + "ts", + } + + fields: Sequence[AttachmentField] + + MarkdownFields = {"fields", "pretext", "text"} + + footer_max_length = 300 + + def __init__( + self, + *, + text: str, + fallback: Optional[str] = None, + fields: Optional[Sequence[AttachmentField]] = None, + color: Optional[str] = None, + markdown_in: Optional[Sequence[str]] = None, + title: Optional[str] = None, + title_link: Optional[str] = None, + pretext: Optional[str] = None, + author_name: Optional[str] = None, + author_subname: Optional[str] = None, + author_link: Optional[str] = None, + author_icon: Optional[str] = None, + image_url: Optional[str] = None, + thumb_url: Optional[str] = None, + footer: Optional[str] = None, + footer_icon: Optional[str] = None, + ts: Optional[int] = None, + ): + """ + A supplemental object that will display after the rest of the message. + Considered legacy - recommended replacement is to use message blocks instead. + + https://docs.slack.dev/legacy/legacy-messaging/legacy-secondary-message-attachments#fields + + Args: + text: The main body text of the attachment. It can be formatted as + plain text, or with markdown by including it in the markdown_in + parameter. The content will automatically collapse if it contains 700+ + characters or 5+ linebreaks, and will display a "Show more..." link to + expand the content. + fallback: A plain text summary of the attachment used in clients that + don't show formatted text (eg. IRC, mobile notifications). + fields: An array of AttachmentField objects that get displayed in a + table-like way. For best results, include no more than 2-3 field + objects. + color: Changes the color of the border on the left side of this attachment + from the default gray. Can be any hex color code (eg. #439FE0) + markdown_in: An array of field names that should be formatted by + markdown syntax - allowed values: "pretext", "text", "fields" + title: Large title text near the top of the attachment. + title_link: A valid URL that turns the title text into a hyperlink. + pretext: Text that appears above the message attachment block. It can + be formatted as plain text, or with markdown by including it in the + markdown_in parameter. + author_name: Small text used to display the author's name. + author_subname: Small text used to display the author's sub name. + author_link: A valid URL that will hyperlink the author_name text. + Will only work if author_name is present. + author_icon: A valid URL that displays a small 16px by 16px image to + the left of the author_name text. Will only work if author_name is + present. + image_url: A valid URL to an image file that will be displayed at the + bottom of the attachment. We support GIF, JPEG, PNG, and BMP formats. + Large images will be resized to a maximum width of 360px or a maximum + height of 500px, while still maintaining the original aspect ratio. + Cannot be used with thumb_url. + thumb_url: A valid URL to an image file that will be displayed as a + thumbnail on the right side of a message attachment. We currently + support the following formats: GIF, JPEG, PNG, and BMP. The thumbnail's + longest dimension will be scaled down to 75px while maintaining the + aspect ratio of the image. The filesize of the image must also be less + than 500 KB. For best results, please use images that are already 75px + by 75px. + footer: Some brief text to help contextualize and identify an + attachment. Limited to 300 characters, and may be truncated further when + displayed to users in environments with limited screen real estate. + footer_icon: A valid URL to an image file that will be displayed + beside the footer text. Will only work if footer is present. We'll + render what you provide at 16px by 16px. It's best to use an image that + is similarly sized. + ts: An integer Unix timestamp that is used to related your attachment + to a specific time. The attachment will display the additional timestamp + value as part of the attachment's footer. Your message's timestamp will + be displayed in varying ways, depending on how far in the past or future + it is, relative to the present. Form factors, like mobile versus + desktop may also transform its rendered appearance. + """ + self.text = text + self.title = title + self.fallback = fallback + self.pretext = pretext + self.title_link = title_link + self.color = color + self.author_name = author_name + self.author_subname = author_subname + self.author_link = author_link + self.author_icon = author_icon + self.image_url = image_url + self.thumb_url = thumb_url + self.footer = footer + self.footer_icon = footer_icon + self.ts = ts + self.fields = fields or [] + self.markdown_in = markdown_in or [] + + @JsonValidator(f"footer attribute cannot exceed {footer_max_length} characters") + def footer_length(self) -> bool: + return self.footer is None or len(self.footer) <= self.footer_max_length + + @JsonValidator("ts attribute cannot be present if footer attribute is absent") + def ts_without_footer(self) -> bool: + return self.ts is None or self.footer is not None + + @EnumValidator("markdown_in", MarkdownFields) + def markdown_in_valid(self): + return not self.markdown_in or all(e in self.MarkdownFields for e in self.markdown_in) + + @JsonValidator("color attribute must be 'good', 'warning', 'danger', or a hex color code") + def color_valid(self) -> bool: + return ( + self.color is None + or self.color in SeededColors + or re.match("^#(?:[0-9A-F]{2}){3}$", self.color, re.IGNORECASE) is not None + ) + + @JsonValidator("image_url attribute cannot be present if thumb_url is populated") + def image_url_and_thumb_url_populated(self) -> bool: + return self.image_url is None or self.thumb_url is None + + @JsonValidator("name must be present if link is present") + def author_link_without_author_name(self) -> bool: + return self.author_link is None or self.author_name is not None + + @JsonValidator("icon must be present if link is present") + def author_link_without_author_icon(self) -> bool: + return self.author_link is None or self.author_icon is not None + + def to_dict(self) -> dict: + json = super().to_dict() + if self.fields is not None: + json["fields"] = extract_json(self.fields) + if self.markdown_in: + json["mrkdwn_in"] = self.markdown_in + return json + + +class BlockAttachment(Attachment): + blocks: List[Block] + + @property + def attributes(self): + return super().attributes.union({"blocks", "color"}) + + def __init__( + self, + *, + blocks: Sequence[Block], + color: Optional[str] = None, + fallback: Optional[str] = None, + ): + """ + A bridge between legacy attachments and Block Kit formatting - pass a list of + Block objects directly to this attachment. + + https://docs.slack.dev/legacy/legacy-messaging/legacy-secondary-message-attachments#fields + + Args: + blocks: a sequence of Block objects + color: Changes the color of the border on the left side of this + attachment from the default gray. Can either be one of "good" (green), + "warning" (yellow), "danger" (red), or any hex color code (eg. #439FE0) + fallback: fallback text + """ + super().__init__(text="", fallback=fallback, color=color) + self.blocks = list(blocks) + + @JsonValidator("fields attribute cannot be populated on BlockAttachment") + def fields_attribute_absent(self) -> bool: + return not self.fields + + def to_dict(self) -> dict: + json = super().to_dict() + json.update({"blocks": extract_json(self.blocks)}) + del json["fields"] # cannot supply fields and blocks at the same time + return json + + +class InteractiveAttachment(Attachment): + @property + def attributes(self) -> Set[str]: # type: ignore[override] + return super().attributes.union({"callback_id"}) + + actions_max_length = 5 + + def __init__( + self, + *, + actions: Sequence[Action], + callback_id: str, + text: str, + fallback: Optional[str] = None, + fields: Optional[Sequence[AttachmentField]] = None, + color: Optional[str] = None, + markdown_in: Optional[Sequence[str]] = None, + title: Optional[str] = None, + title_link: Optional[str] = None, + pretext: Optional[str] = None, + author_name: Optional[str] = None, + author_subname: Optional[str] = None, + author_link: Optional[str] = None, + author_icon: Optional[str] = None, + image_url: Optional[str] = None, + thumb_url: Optional[str] = None, + footer: Optional[str] = None, + footer_icon: Optional[str] = None, + ts: Optional[int] = None, + ): + """ + An Attachment, but designed to contain interactive Actions + Considered legacy - recommended replacement is to use message blocks instead. + + https://docs.slack.dev/legacy/legacy-messaging/legacy-interactive-message-field-guide/#attachment_fields + https://docs.slack.dev/legacy/legacy-messaging/legacy-secondary-message-attachments#fields + + Args: + actions: A collection of Action objects to include in the attachment. + Cannot exceed 5 elements. + callback_id: The ID used to identify this attachment. Will be part of the + payload sent back to your application. + text: The main body text of the attachment. It can be formatted as + plain text, or with markdown by including it in the markdown_in + parameter. The content will automatically collapse if it contains 700+ + characters or 5+ linebreaks, and will display a "Show more..." link to + expand the content. + fallback: A plain text summary of the attachment used in clients that + don't show formatted text (eg. IRC, mobile notifications). + fields: An array of AttachmentField objects that get displayed in a + table-like way. For best results, include no more than 2-3 field + objects. + color: Changes the color of the border on the left side of this attachment + from the default gray. Can either be one of "good" (green), "warning" + (yellow), "danger" (red), or any hex color code (eg. #439FE0) + markdown_in: An array of field names that should be formatted by + markdown syntax - allowed values: "pretext", "text", "fields" + title: Large title text near the top of the attachment. + title_link: A valid URL that turns the title text into a hyperlink. + pretext: Text that appears above the message attachment block. It can + be formatted as plain text, or with markdown by including it in the + markdown_in parameter. + author_name: Small text used to display the author's name. + author_subname: Small text used to display the author's sub name. + author_link: A valid URL that will hyperlink the author_name text. + Will only work if author_name is present. + author_icon: A valid URL that displays a small 16px by 16px image to + the left of the author_name text. Will only work if author_name is + present. + image_url: A valid URL to an image file that will be displayed at the + bottom of the attachment. We support GIF, JPEG, PNG, and BMP formats. + Large images will be resized to a maximum width of 360px or a maximum + height of 500px, while still maintaining the original aspect ratio. + Cannot be used with thumb_url. + thumb_url: A valid URL to an image file that will be displayed as a + thumbnail on the right side of a message attachment. We currently + support the following formats: GIF, JPEG, PNG, and BMP. The thumbnail's + longest dimension will be scaled down to 75px while maintaining the + aspect ratio of the image. The filesize of the image must also be less + than 500 KB. For best results, please use images that are already 75px + by 75px. + footer: Some brief text to help contextualize and identify an + attachment. Limited to 300 characters, and may be truncated further when + displayed to users in environments with limited screen real estate. + footer_icon: A valid URL to an image file that will be displayed + beside the footer text. Will only work if footer is present. We'll + render what you provide at 16px by 16px. It's best to use an image that + is similarly sized. + ts: An integer Unix timestamp that is used to related your attachment + to a specific time. The attachment will display the additional timestamp + value as part of the attachment's footer. Your message's timestamp will + be displayed in varying ways, depending on how far in the past or future + it is, relative to the present. Form factors, like mobile versus + desktop may also transform its rendered appearance. + """ + super().__init__( + text=text, + title=title, + fallback=fallback, + fields=fields, + pretext=pretext, + title_link=title_link, + color=color, + author_name=author_name, + author_subname=author_subname, + author_link=author_link, + author_icon=author_icon, + image_url=image_url, + thumb_url=thumb_url, + footer=footer, + footer_icon=footer_icon, + ts=ts, + markdown_in=markdown_in, + ) + self.callback_id = callback_id + self.actions = actions or [] + + @JsonValidator(f"actions attribute cannot exceed {actions_max_length} elements") + def actions_length(self) -> bool: + return len(self.actions) <= self.actions_max_length + + def to_dict(self) -> dict: + json = super().to_dict() + json["actions"] = extract_json(self.actions) + return json diff --git a/slack_sdk/models/basic_objects.py b/slack_sdk/models/basic_objects.py new file mode 100644 index 000000000..4feefe3f6 --- /dev/null +++ b/slack_sdk/models/basic_objects.py @@ -0,0 +1,135 @@ +from abc import ABCMeta, abstractmethod +from functools import wraps +from typing import Callable, Iterable, Set, Union, Any + +from slack_sdk.errors import SlackObjectFormationError + + +class BaseObject: + """The base class for all model objects in this module""" + + def __str__(self): + return f"" + + +# Usually, Block Kit components do not allow an empty array for a property value, but there are some exceptions. +EMPTY_ALLOWED_TYPE_AND_PROPERTY_LIST = [ + {"type": "rich_text_section", "property": "elements"}, + {"type": "rich_text_list", "property": "elements"}, + {"type": "rich_text_preformatted", "property": "elements"}, + {"type": "rich_text_quote", "property": "elements"}, +] + + +class JsonObject(BaseObject, metaclass=ABCMeta): + """The base class for JSON serializable class objects""" + + @property + @abstractmethod + def attributes(self) -> Set[str]: + """Provide a set of attributes of this object that will make up its JSON structure""" + return set() + + def validate_json(self) -> None: + """ + Raises: + SlackObjectFormationError if the object was not valid + """ + for attribute in (func for func in dir(self) if not func.startswith("__")): + method = getattr(self, attribute, None) + if callable(method) and hasattr(method, "validator"): + method() + + def get_object_attribute(self, key: str): + return getattr(self, key, None) + + def get_non_null_attributes(self) -> dict: + """ + Construct a dictionary out of non-null keys (from attributes property) + present on this object + """ + + def to_dict_compatible(value: Union[dict, list, object, tuple]) -> Union[dict, list, Any]: + if isinstance(value, (list, tuple)): + return [to_dict_compatible(v) for v in value] + else: + to_dict = getattr(value, "to_dict", None) + if to_dict and callable(to_dict): + return {k: to_dict_compatible(v) for k, v in value.to_dict().items()} # type: ignore[attr-defined] + else: + return value + + def is_not_empty(self, key: str) -> bool: + value = self.get_object_attribute(key) + if value is None: + return False + + # Usually, Block Kit components do not allow an empty array for a property value, but there are some exceptions. + # The following code deals with these exceptions: + type_value = getattr(self, "type", None) + for empty_allowed in EMPTY_ALLOWED_TYPE_AND_PROPERTY_LIST: + if type_value == empty_allowed["type"] and key == empty_allowed["property"]: + return True + + has_len = getattr(value, "__len__", None) is not None + if has_len: + return len(value) > 0 + else: + return value is not None + + return { + key: to_dict_compatible(value=self.get_object_attribute(key)) + for key in sorted(self.attributes) + if is_not_empty(self, key) + } + + def to_dict(self, *args) -> dict: + """ + Extract this object as a JSON-compatible, Slack-API-valid dictionary + + Args: + *args: Any specific formatting args (rare; generally not required) + + Raises: + SlackObjectFormationError if the object was not valid + """ + self.validate_json() + return self.get_non_null_attributes() + + def __repr__(self): + dict_value = self.get_non_null_attributes() + if dict_value: + return f"" + else: + return self.__str__() + + def __eq__(self, other: Any) -> bool: + if not isinstance(other, JsonObject): + return False + return self.to_dict() == other.to_dict() + + +class JsonValidator: + def __init__(self, message: str): + """ + Decorate a method on a class to mark it as a JSON validator. Validation + functions should return true if valid, false if not. + + Args: + message: Message to be attached to the thrown SlackObjectFormationError + """ + self.message = message + + def __call__(self, func: Callable) -> Callable[..., None]: + @wraps(func) + def wrapped_f(*args, **kwargs): + if not func(*args, **kwargs): + raise SlackObjectFormationError(self.message) + + wrapped_f.validator = True # type: ignore[attr-defined] + return wrapped_f + + +class EnumValidator(JsonValidator): + def __init__(self, attribute: str, enum: Iterable[str]): + super().__init__(f"{attribute} attribute must be one of the following values: " f"{', '.join(enum)}") diff --git a/slack_sdk/models/blocks/__init__.py b/slack_sdk/models/blocks/__init__.py new file mode 100644 index 000000000..d2776a9dc --- /dev/null +++ b/slack_sdk/models/blocks/__init__.py @@ -0,0 +1,142 @@ +"""Block Kit data model objects + +To learn more about Block Kit, please check the following resources and tools: + +* https://docs.slack.dev/block-kit/ +* https://docs.slack.dev/reference/block-kit/blocks +* https://app.slack.com/block-kit-builder +""" + +from .basic_components import ( + ButtonStyles, + ConfirmObject, + DynamicSelectElementTypes, + FeedbackButtonObject, + MarkdownTextObject, + Option, + OptionGroup, + PlainTextObject, + RawTextObject, + TextObject, +) +from .block_elements import ( + BlockElement, + ButtonElement, + ChannelMultiSelectElement, + ChannelSelectElement, + CheckboxesElement, + ConversationFilter, + ConversationMultiSelectElement, + ConversationSelectElement, + DatePickerElement, + DateTimePickerElement, + EmailInputElement, + ExternalDataMultiSelectElement, + ExternalDataSelectElement, + FeedbackButtonsElement, + IconButtonElement, + ImageElement, + InputInteractiveElement, + InteractiveElement, + LinkButtonElement, + NumberInputElement, + OverflowMenuElement, + PlainTextInputElement, + RadioButtonsElement, + RichTextElement, + RichTextElementParts, + RichTextInputElement, + RichTextListElement, + RichTextPreformattedElement, + RichTextQuoteElement, + RichTextSectionElement, + SelectElement, + StaticMultiSelectElement, + StaticSelectElement, + TimePickerElement, + UrlInputElement, + UserMultiSelectElement, + UserSelectElement, +) +from .blocks import ( + ActionsBlock, + Block, + CallBlock, + ContextActionsBlock, + ContextBlock, + DividerBlock, + FileBlock, + HeaderBlock, + ImageBlock, + InputBlock, + MarkdownBlock, + RichTextBlock, + SectionBlock, + TableBlock, + VideoBlock, +) + +__all__ = [ + "ButtonStyles", + "ConfirmObject", + "DynamicSelectElementTypes", + "FeedbackButtonObject", + "MarkdownTextObject", + "Option", + "OptionGroup", + "PlainTextObject", + "RawTextObject", + "TextObject", + "BlockElement", + "ButtonElement", + "ChannelMultiSelectElement", + "ChannelSelectElement", + "CheckboxesElement", + "ConversationFilter", + "ConversationMultiSelectElement", + "ConversationSelectElement", + "DatePickerElement", + "TimePickerElement", + "DateTimePickerElement", + "ExternalDataMultiSelectElement", + "ExternalDataSelectElement", + "FeedbackButtonsElement", + "IconButtonElement", + "ImageElement", + "InputInteractiveElement", + "InteractiveElement", + "LinkButtonElement", + "OverflowMenuElement", + "RichTextInputElement", + "PlainTextInputElement", + "EmailInputElement", + "UrlInputElement", + "NumberInputElement", + "RadioButtonsElement", + "SelectElement", + "StaticMultiSelectElement", + "StaticSelectElement", + "UserMultiSelectElement", + "UserSelectElement", + "RichTextElement", + "RichTextElementParts", + "RichTextListElement", + "RichTextPreformattedElement", + "RichTextQuoteElement", + "RichTextSectionElement", + "ActionsBlock", + "Block", + "CallBlock", + "ContextActionsBlock", + "ContextBlock", + "DividerBlock", + "FileBlock", + "HeaderBlock", + "ImageBlock", + "InputBlock", + "MarkdownBlock", + "SectionBlock", + "TableBlock", + "VideoBlock", + "RichTextBlock", +] diff --git a/slack_sdk/models/blocks/basic_components.py b/slack_sdk/models/blocks/basic_components.py new file mode 100644 index 000000000..b6e71683a --- /dev/null +++ b/slack_sdk/models/blocks/basic_components.py @@ -0,0 +1,682 @@ +import copy +import logging +import warnings +from typing import Any, Dict, List, Optional, Sequence, Set, Union + +from slack_sdk.models import show_unknown_key_warning +from slack_sdk.models.basic_objects import JsonObject, JsonValidator +from slack_sdk.models.messages import Link + +ButtonStyles = {"danger", "primary"} +DynamicSelectElementTypes = {"channels", "conversations", "users"} + + +class TextObject(JsonObject): + """The interface for text objects (types: plain_text, mrkdwn)""" + + attributes = {"text", "type", "emoji"} + logger = logging.getLogger(__name__) + + def _subtype_warning(self): + warnings.warn( + "subtype is deprecated since slackclient 2.6.0, use type instead", + DeprecationWarning, + ) + + @property + def subtype(self) -> Optional[str]: + return self.type + + @classmethod + def parse( + cls, + text: Union[str, Dict[str, Any], "TextObject"], + default_type: str = "mrkdwn", + ) -> Optional["TextObject"]: + if not text: + return None + elif isinstance(text, str): + if default_type == PlainTextObject.type: + return PlainTextObject.from_str(text) + else: + return MarkdownTextObject.from_str(text) + elif isinstance(text, dict): + d = copy.copy(text) + t = d.pop("type") + if t == PlainTextObject.type: + return PlainTextObject(**d) + else: + return MarkdownTextObject(**d) + elif isinstance(text, TextObject): + return text + else: + cls.logger.warning(f"Unknown type ({type(text)}) detected when parsing a TextObject") + return None + + def __init__( + self, + text: str, + type: Optional[str] = None, + subtype: Optional[str] = None, + emoji: Optional[bool] = None, + **kwargs, + ): + """Super class for new text "objects" used in Block kit""" + if subtype: + self._subtype_warning() + + self.text = text + self.type = type if type else subtype + self.emoji = emoji + + +class PlainTextObject(TextObject): + """plain_text typed text object""" + + type = "plain_text" + + @property + def attributes(self) -> Set[str]: # type: ignore[override] + return super().attributes.union({"emoji"}) + + def __init__(self, *, text: str, emoji: Optional[bool] = None): + """A plain text object, meaning markdown characters will not be parsed as + formatting information. + https://docs.slack.dev/reference/block-kit/composition-objects/text-object + + Args: + text (required): The text for the block. This field accepts any of the standard text formatting markup + when type is mrkdwn. + emoji: Indicates whether emojis in a text field should be escaped into the colon emoji format. + This field is only usable when type is plain_text. + """ + super().__init__(text=text, type=self.type) + self.emoji = emoji + + @staticmethod + def from_str(text: str) -> "PlainTextObject": + return PlainTextObject(text=text, emoji=True) + + @staticmethod + def direct_from_string(text: str) -> Dict[str, Any]: + """Transforms a string into the required object shape to act as a PlainTextObject""" + return PlainTextObject.from_str(text).to_dict() + + +class MarkdownTextObject(TextObject): + """mrkdwn typed text object""" + + type = "mrkdwn" + + @property + def attributes(self) -> Set[str]: # type: ignore[override] + return super().attributes.union({"verbatim"}) + + def __init__(self, *, text: str, verbatim: Optional[bool] = None): + """A Markdown text object, meaning markdown characters will be parsed as + formatting information. + https://docs.slack.dev/reference/block-kit/composition-objects/text-object + + Args: + text (required): The text for the block. This field accepts any of the standard text formatting markup + when type is mrkdwn. + verbatim: When set to false (as is default) URLs will be auto-converted into links, + conversation names will be link-ified, and certain mentions will be automatically parsed. + Using a value of true will skip any preprocessing of this nature, + although you can still include manual parsing strings. This field is only usable when type is mrkdwn. + """ + super().__init__(text=text, type=self.type) + self.verbatim = verbatim + + @staticmethod + def from_str(text: str) -> "MarkdownTextObject": + """Transforms a string into the required object shape to act as a MarkdownTextObject""" + return MarkdownTextObject(text=text) + + @staticmethod + def direct_from_string(text: str) -> Dict[str, Any]: + """Transforms a string into the required object shape to act as a MarkdownTextObject""" + return MarkdownTextObject.from_str(text).to_dict() + + @staticmethod + def from_link(link: Link, title: str = "") -> "MarkdownTextObject": + """ + Transform a Link object directly into the required object shape + to act as a MarkdownTextObject + """ + if title: + title = f": {title}" + return MarkdownTextObject(text=f"{link}{title}") + + @staticmethod + def direct_from_link(link: Link, title: str = "") -> Dict[str, Any]: + """ + Transform a Link object directly into the required object shape + to act as a MarkdownTextObject + """ + return MarkdownTextObject.from_link(link, title).to_dict() + + +class RawTextObject(TextObject): + """raw_text typed text object""" + + type = "raw_text" + + @property + def attributes(self) -> Set[str]: # type: ignore[override] + return {"text", "type"} + + def __init__(self, *, text: str): + """A raw text object used in table block cells. + https://docs.slack.dev/reference/block-kit/composition-objects/text-object/ + https://docs.slack.dev/reference/block-kit/blocks/table-block + + Args: + text (required): The text content for the table block cell. + """ + super().__init__(text=text, type=self.type) + + @staticmethod + def from_str(text: str) -> "RawTextObject": + """Transforms a string into a RawTextObject""" + return RawTextObject(text=text) + + @staticmethod + def direct_from_string(text: str) -> Dict[str, Any]: + """Transforms a string into the required object shape to act as a RawTextObject""" + return RawTextObject.from_str(text).to_dict() + + @JsonValidator("text attribute must have at least 1 character") + def _validate_text_min_length(self): + return len(self.text) >= 1 + + +class Option(JsonObject): + """Option object used in dialogs, legacy message actions (interactivity in attachments), + and blocks. JSON must be retrieved with an explicit option_type - the Slack API has + different required formats in different situations + """ + + attributes: Set[str] = set() + logger = logging.getLogger(__name__) + + label_max_length = 75 + value_max_length = 150 + + def __init__( + self, + *, + value: str, + label: Optional[str] = None, + text: Optional[Union[str, Dict[str, Any], TextObject]] = None, # Block Kit + description: Optional[Union[str, Dict[str, Any], TextObject]] = None, + url: Optional[str] = None, + **others: Dict[str, Any], + ): + """ + An object that represents a single selectable item in a block element ( + SelectElement, OverflowMenuElement) or dialog element + (StaticDialogSelectElement) + + Blocks: + https://docs.slack.dev/reference/block-kit/composition-objects/option-object + + Dialogs: + https://docs.slack.dev/legacy/legacy-dialogs/#select_elements + + Legacy interactive attachments: + https://docs.slack.dev/legacy/legacy-messaging/legacy-interactive-message-field-guide/#option_fields + + Args: + label: A short, user-facing string to label this option to users. + Cannot exceed 75 characters. + value: A short string that identifies this particular option to your + application. It will be part of the payload when this option is selected + . Cannot exceed 150 characters. + description: A user-facing string that provides more details about + this option. Only supported in legacy message actions, not in blocks or + dialogs. + """ + if text: + # For better compatibility with Block Kit ("mrkdwn" does not work for it), + # we've changed the default text object type to plain_text since version 3.10.0 + self._text: Optional[TextObject] = TextObject.parse( + text=text, # "text" here can be either a str or a TextObject + default_type=PlainTextObject.type, + ) + self._label: Optional[str] = None + else: + self._text = None + self._label = label + + # for backward-compatibility with version 2.0-2.5, the following fields return str values + self.text: Optional[str] = self._text.text if self._text else None + self.label: Optional[str] = self._label + + self.value: str = value + + # for backward-compatibility with version 2.0-2.5, the following fields return str values + if isinstance(description, str): + self.description = description + self._block_description = PlainTextObject.from_str(description) + elif isinstance(description, dict): + self.description = description["text"] + self._block_description = TextObject.parse(description) # type: ignore[assignment] + elif isinstance(description, TextObject): + self.description = description.text + self._block_description = description # type: ignore[assignment] + else: + self.description = None # type: ignore[assignment] + self._block_description = None # type: ignore[assignment] + + # A URL to load in the user's browser when the option is clicked. + # The url attribute is only available in overflow menus. + # Maximum length for this field is 3000 characters. + # If you're using url, you'll still receive an interaction payload + # and will need to send an acknowledgement response. + self.url: Optional[str] = url + show_unknown_key_warning(self, others) + + @JsonValidator(f"label attribute cannot exceed {label_max_length} characters") + def _validate_label_length(self) -> bool: + return self._label is None or len(self._label) <= self.label_max_length + + @JsonValidator(f"text attribute cannot exceed {label_max_length} characters") + def _validate_text_length(self) -> bool: + return self._text is None or self._text.text is None or len(self._text.text) <= self.label_max_length + + @JsonValidator(f"value attribute cannot exceed {value_max_length} characters") + def _validate_value_length(self) -> bool: + return len(self.value) <= self.value_max_length + + @classmethod + def parse_all(cls, options: Optional[Sequence[Union[Dict[str, Any], "Option"]]]) -> Optional[List["Option"]]: + if options is None: + return None + option_objects: List[Option] = [] + for o in options: + if isinstance(o, dict): + d = copy.copy(o) + option_objects.append(Option(**d)) + elif isinstance(o, Option): + option_objects.append(o) + else: + cls.logger.warning(f"Unknown option object detected and skipped ({o})") + return option_objects + + def to_dict(self, option_type: str = "block") -> Dict[str, Any]: + """ + Different parent classes must call this with a valid value from OptionTypes - + either "dialog", "action", or "block", so that JSON is returned in the + correct shape. + """ + self.validate_json() + if option_type == "dialog": + return {"label": self.label, "value": self.value} + elif option_type == "action" or option_type == "attachment": + # "action" can be confusing but it means a legacy message action in attachments + # we don't remove the type name for backward compatibility though + json: Dict[str, Any] = {"text": self.label, "value": self.value} + if self.description is not None: + json["description"] = self.description + return json + else: # if option_type == "block"; this should be the most common case + text: TextObject = self._text or PlainTextObject.from_str(self.label) # type: ignore[arg-type] + json = { + "text": text.to_dict(), + "value": self.value, + } + if self._block_description: + json["description"] = self._block_description.to_dict() + if self.url: + json["url"] = self.url + return json + + @staticmethod + def from_single_value(value_and_label: str): + """Creates a simple Option instance with the same value and label""" + return Option(value=value_and_label, label=value_and_label) + + +class OptionGroup(JsonObject): + """ + JSON must be retrieved with an explicit option_type - the Slack API has + different required formats in different situations + """ + + attributes: Set[str] = set() + label_max_length = 75 + options_max_length = 100 + logger = logging.getLogger(__name__) + + def __init__( + self, + *, + label: Optional[Union[str, Dict[str, Any], TextObject]] = None, + options: Sequence[Union[Dict[str, Any], Option]], + **others: Dict[str, Any], + ): + """ + Create a group of Option objects - pass in a label (that will be part of the + UI) and a list of Option objects. + + Blocks: + https://docs.slack.dev/reference/block-kit/composition-objects/option-group-object + + Dialogs: + https://docs.slack.dev/legacy/legacy-dialogs/#select_elements + + Legacy interactive attachments: + https://docs.slack.dev/legacy/legacy-messaging/legacy-interactive-message-field-guide/#option_groups + + Args: + label: Text to display at the top of this group of options. + options: A list of no more than 100 Option objects. + """ # noqa prevent flake8 blowing up on the long URL + # default_type=PlainTextObject.type is for backward-compatibility + self._label: Optional[TextObject] = TextObject.parse(label, default_type=PlainTextObject.type) # type: ignore[arg-type] # noqa: E501 + self.label: Optional[str] = self._label.text if self._label else None + self.options = Option.parse_all(options) # compatible with version 2.5 + show_unknown_key_warning(self, others) + + @JsonValidator(f"label attribute cannot exceed {label_max_length} characters") + def _validate_label_length(self): + return self.label is None or len(self.label) <= self.label_max_length + + @JsonValidator(f"options attribute cannot exceed {options_max_length} elements") + def _validate_options_length(self): + return self.options is None or len(self.options) <= self.options_max_length + + @classmethod + def parse_all( + cls, option_groups: Optional[Sequence[Union[Dict[str, Any], "OptionGroup"]]] + ) -> Optional[List["OptionGroup"]]: + if option_groups is None: + return None + option_group_objects = [] + for o in option_groups: + if isinstance(o, dict): + d = copy.copy(o) + option_group_objects.append(OptionGroup(**d)) + elif isinstance(o, OptionGroup): + option_group_objects.append(o) + else: + cls.logger.warning(f"Unknown option group object detected and skipped ({o})") + return option_group_objects + + def to_dict(self, option_type: str = "block") -> Dict[str, Any]: + self.validate_json() + dict_options = [o.to_dict(option_type) for o in self.options] # type: ignore[union-attr] + if option_type == "dialog": + return { + "label": self.label, + "options": dict_options, + } + elif option_type == "action": + return { + "text": self.label, + "options": dict_options, + } + else: # if option_type == "block"; this should be the most common case + dict_label: Dict[str, Any] = self._label.to_dict() # type: ignore[union-attr] + return { + "label": dict_label, + "options": dict_options, + } + + +class ConfirmObject(JsonObject): + attributes: Set[str] = set() + + title_max_length = 100 + text_max_length = 300 + confirm_max_length = 30 + deny_max_length = 30 + + @classmethod + def parse(cls, confirm: Union["ConfirmObject", Dict[str, Any]]): + if confirm: + if isinstance(confirm, ConfirmObject): + return confirm + elif isinstance(confirm, dict): + return ConfirmObject(**confirm) + else: + # Not yet implemented: show some warning here + return None + return None + + def __init__( + self, + *, + title: Union[str, Dict[str, Any], PlainTextObject], + text: Union[str, Dict[str, Any], TextObject], + confirm: Union[str, Dict[str, Any], PlainTextObject] = "Yes", + deny: Union[str, Dict[str, Any], PlainTextObject] = "No", + style: Optional[str] = None, + ): + """ + An object that defines a dialog that provides a confirmation step to any + interactive element. This dialog will ask the user to confirm their action by + offering a confirm and deny button. + https://docs.slack.dev/reference/block-kit/composition-objects/confirmation-dialog-object/ + """ + self._title = TextObject.parse(title, default_type=PlainTextObject.type) + self._text = TextObject.parse(text, default_type=MarkdownTextObject.type) + self._confirm = TextObject.parse(confirm, default_type=PlainTextObject.type) + self._deny = TextObject.parse(deny, default_type=PlainTextObject.type) + self._style = style + + # for backward-compatibility with version 2.0-2.5, the following fields return str values + self.title = self._title.text if self._title else None + self.text = self._text.text if self._text else None + self.confirm = self._confirm.text if self._confirm else None + self.deny = self._deny.text if self._deny else None + self.style = self._style + + @JsonValidator(f"title attribute cannot exceed {title_max_length} characters") + def title_length(self) -> bool: + return self._title is None or len(self._title.text) <= self.title_max_length + + @JsonValidator(f"text attribute cannot exceed {text_max_length} characters") + def text_length(self) -> bool: + return self._text is None or len(self._text.text) <= self.text_max_length + + @JsonValidator(f"confirm attribute cannot exceed {confirm_max_length} characters") + def confirm_length(self) -> bool: + return self._confirm is None or len(self._confirm.text) <= self.confirm_max_length + + @JsonValidator(f"deny attribute cannot exceed {deny_max_length} characters") + def deny_length(self) -> bool: + return self._deny is None or len(self._deny.text) <= self.deny_max_length + + @JsonValidator('style for confirm must be either "primary" or "danger"') + def _validate_confirm_style(self) -> bool: + return self._style is None or self._style in ["primary", "danger"] + + def to_dict(self, option_type: str = "block") -> Dict[str, Any]: + if option_type == "action": + # deliberately skipping JSON validators here - can't find documentation + # on actual limits here + json: Dict[str, Union[str, dict]] = { + "ok_text": self._confirm.text if self._confirm and self._confirm.text != "Yes" else "Okay", + "dismiss_text": self._deny.text if self._deny and self._deny.text != "No" else "Cancel", + } + if self._title: + json["title"] = self._title.text + if self._text: + json["text"] = self._text.text + return json + + else: + self.validate_json() + json = {} + if self._title: + json["title"] = self._title.to_dict() + if self._text: + json["text"] = self._text.to_dict() + if self._confirm: + json["confirm"] = self._confirm.to_dict() + if self._deny: + json["deny"] = self._deny.to_dict() + if self._style: + json["style"] = self._style + return json + + +class DispatchActionConfig(JsonObject): + attributes = {"trigger_actions_on"} + + @classmethod + def parse(cls, config: Union["DispatchActionConfig", Dict[str, Any]]): + if config: + if isinstance(config, DispatchActionConfig): + return config + elif isinstance(config, dict): + return DispatchActionConfig(**config) + else: + # Not yet implemented: show some warning here + return None + return None + + def __init__( + self, + *, + trigger_actions_on: Optional[List[Any]] = None, + ): + """ + Determines when a plain-text input element will return a block_actions interaction payload. + https://docs.slack.dev/reference/block-kit/composition-objects/dispatch-action-configuration-object + """ + self._trigger_actions_on = trigger_actions_on or [] + + def to_dict(self) -> Dict[str, Any]: + self.validate_json() + json = {} + if self._trigger_actions_on: + json["trigger_actions_on"] = self._trigger_actions_on + return json + + +class FeedbackButtonObject(JsonObject): + attributes: Set[str] = set() + + text_max_length = 75 + value_max_length = 2000 + + @classmethod + def parse(cls, feedback_button: Union["FeedbackButtonObject", Dict[str, Any]]): + if feedback_button: + if isinstance(feedback_button, FeedbackButtonObject): + return feedback_button + elif isinstance(feedback_button, dict): + return FeedbackButtonObject(**feedback_button) + else: + # Not yet implemented: show some warning here + return None + return None + + def __init__( + self, + *, + text: Union[str, Dict[str, Any], PlainTextObject], + accessibility_label: Optional[str] = None, + value: str, + **others: Dict[str, Any], + ): + """ + A feedback button element object for either positive or negative feedback. + https://docs.slack.dev/reference/block-kit/block-elements/feedback-buttons-element#button-object-fields + + Args: + text (required): An object containing some text. Maximum length for this field is 75 characters. + accessibility_label: A label for longer descriptive text about a button element. This label will be read out by + screen readers instead of the button `text` object. + value (required): The button value. Maximum length for this field is 2000 characters. + """ + self._text: Optional[TextObject] = PlainTextObject.parse(text, default_type=PlainTextObject.type) + self._accessibility_label: Optional[str] = accessibility_label + self._value: Optional[str] = value + show_unknown_key_warning(self, others) + + @JsonValidator(f"text attribute cannot exceed {text_max_length} characters") + def text_length(self) -> bool: + return self._text is None or len(self._text.text) <= self.text_max_length + + @JsonValidator(f"value attribute cannot exceed {value_max_length} characters") + def value_length(self) -> bool: + return self._value is None or len(self._value) <= self.value_max_length + + def to_dict(self) -> Dict[str, Any]: + self.validate_json() + json: Dict[str, Union[str, dict]] = {} + if self._text: + json["text"] = self._text.to_dict() + if self._accessibility_label: + json["accessibility_label"] = self._accessibility_label + if self._value: + json["value"] = self._value + return json + + +class WorkflowTrigger(JsonObject): + attributes = {"trigger"} + + def __init__(self, *, url: str, customizable_input_parameters: Optional[List[Dict[str, str]]] = None): + self._url = url + self._customizable_input_parameters = customizable_input_parameters + + def to_dict(self) -> Dict[str, Any]: + self.validate_json() + json = {"url": self._url} + if self._customizable_input_parameters is not None: + json.update({"customizable_input_parameters": self._customizable_input_parameters}) # type: ignore[dict-item] + return json + + +class Workflow(JsonObject): + attributes = {"trigger"} + + def __init__( + self, + *, + trigger: Union[WorkflowTrigger, dict], + ): + self._trigger = trigger + + def to_dict(self) -> Dict[str, Any]: + self.validate_json() + json = {} + if isinstance(self._trigger, WorkflowTrigger): + json["trigger"] = self._trigger.to_dict() + else: + json["trigger"] = self._trigger + return json + + +class SlackFile(JsonObject): + attributes = {"id", "url"} + + def __init__( + self, + *, + id: Optional[str] = None, + url: Optional[str] = None, + ): + """An object containing Slack file information to be used in an image block or image element. + https://docs.slack.dev/reference/block-kit/composition-objects/slack-file-object + + Args: + id: Slack ID of the file. + url: This URL can be the url_private or the permalink of the Slack file. + """ + self._id = id + self._url = url + + def to_dict(self) -> Dict[str, Any]: + self.validate_json() + json = {} + if self._id is not None: + json["id"] = self._id + if self._url is not None: + json["url"] = self._url + return json diff --git a/slack_sdk/models/blocks/block_elements.py b/slack_sdk/models/blocks/block_elements.py new file mode 100644 index 000000000..89f0a7994 --- /dev/null +++ b/slack_sdk/models/blocks/block_elements.py @@ -0,0 +1,2249 @@ +import copy +import logging +import re +import warnings +from abc import ABCMeta +from typing import Any, Dict, Iterator, List, Optional, Sequence, Set, Type, Union + +from slack_sdk.models import show_unknown_key_warning +from slack_sdk.models.basic_objects import EnumValidator, JsonObject, JsonValidator + +from .basic_components import ( + ButtonStyles, + ConfirmObject, + DispatchActionConfig, + FeedbackButtonObject, + MarkdownTextObject, + Option, + OptionGroup, + PlainTextObject, + SlackFile, + TextObject, + Workflow, +) + +# ------------------------------------------------- +# Block Elements +# ------------------------------------------------- + + +class BlockElement(JsonObject, metaclass=ABCMeta): + """Block Elements are things that exists inside of your Blocks. + https://docs.slack.dev/reference/block-kit/block-elements/ + """ + + attributes = {"type"} + logger = logging.getLogger(__name__) + + def _subtype_warning(self): + warnings.warn( + "subtype is deprecated since slackclient 2.6.0, use type instead", + DeprecationWarning, + ) + + @property + def subtype(self) -> Optional[str]: + return self.type + + def __init__( + self, + *, + type: Optional[str] = None, + subtype: Optional[str] = None, + **others: dict, + ): + if subtype: + self._subtype_warning() + self.type = type if type else subtype + show_unknown_key_warning(self, others) + + @classmethod + def parse(cls, block_element: Union[dict, "BlockElement"]) -> Optional[Union["BlockElement", TextObject]]: + if block_element is None: + return None + elif isinstance(block_element, dict): + if "type" in block_element: + d = copy.copy(block_element) + t = d.pop("type") + for subclass in cls._get_sub_block_elements(): + if t == subclass.type: + return subclass(**d) + if t == PlainTextObject.type: + return PlainTextObject(**d) + elif t == MarkdownTextObject.type: + return MarkdownTextObject(**d) + elif isinstance(block_element, (TextObject, BlockElement)): + return block_element + cls.logger.warning(f"Unknown element detected and skipped ({block_element})") + return None + + @classmethod + def parse_all( + cls, block_elements: Sequence[Union[dict, "BlockElement", TextObject]] + ) -> List[Union["BlockElement", TextObject]]: + return [cls.parse(e) for e in block_elements or []] # type: ignore[arg-type, misc] + + @classmethod + def _get_sub_block_elements(cls: Type["BlockElement"]) -> Iterator[Type["BlockElement"]]: + for subclass in cls.__subclasses__(): + if hasattr(subclass, "type"): + yield subclass + yield from subclass._get_sub_block_elements() + + +# ------------------------------------------------- +# Interactive Block Elements +# ------------------------------------------------- + + +# This is a base class +class InteractiveElement(BlockElement): + action_id_max_length = 255 + + @property + def attributes(self) -> Set[str]: # type: ignore[override] + return super().attributes.union({"alt_text", "action_id"}) + + def __init__( + self, + *, + action_id: Optional[str] = None, + type: Optional[str] = None, + subtype: Optional[str] = None, + **others: dict, + ): + """An interactive block element. + + We generally recommend using the concrete subclasses for better supports of available properties. + """ + if subtype: + self._subtype_warning() + super().__init__(type=type or subtype) + + # Note that we don't intentionally have show_unknown_key_warning for the unknown key warnings here. + # It's fine to pass any kwargs to the held dict here although the class does not do any validation. + # show_unknown_key_warning(self, others) + + self.action_id = action_id + + @JsonValidator(f"action_id attribute cannot exceed {action_id_max_length} characters") + def _validate_action_id_length(self) -> bool: + return self.action_id is None or len(self.action_id) <= self.action_id_max_length + + +# This is a base class +class InputInteractiveElement(InteractiveElement, metaclass=ABCMeta): + placeholder_max_length = 150 + + attributes = {"type", "action_id", "placeholder", "confirm", "focus_on_load"} + + @property + def subtype(self) -> Optional[str]: + return self.type + + def __init__( + self, + *, + action_id: Optional[str] = None, + placeholder: Optional[Union[str, TextObject]] = None, + type: Optional[str] = None, + subtype: Optional[str] = None, + confirm: Optional[Union[dict, ConfirmObject]] = None, + focus_on_load: Optional[bool] = None, + **others: dict, + ): + """InteractiveElement that is usable in input blocks + + We generally recommend using the concrete subclasses for better supports of available properties. + """ + if subtype: + self._subtype_warning() + super().__init__(action_id=action_id, type=type or subtype) + + # Note that we don't intentionally have show_unknown_key_warning for the unknown key warnings here. + # It's fine to pass any kwargs to the held dict here although the class does not do any validation. + # show_unknown_key_warning(self, others) + + self.placeholder = TextObject.parse(placeholder) # type: ignore[arg-type] + self.confirm = ConfirmObject.parse(confirm) # type: ignore[arg-type] + self.focus_on_load = focus_on_load + + @JsonValidator(f"placeholder attribute cannot exceed {placeholder_max_length} characters") + def _validate_placeholder_length(self) -> bool: + return ( + self.placeholder is None + or self.placeholder.text is None + or len(self.placeholder.text) <= self.placeholder_max_length + ) + + +# ------------------------------------------------- +# Button +# ------------------------------------------------- + + +class ButtonElement(InteractiveElement): + type = "button" + text_max_length = 75 + url_max_length = 3000 + value_max_length = 2000 + + @property + def attributes(self) -> Set[str]: # type: ignore[override] + return super().attributes.union({"text", "url", "value", "style", "confirm", "accessibility_label"}) + + def __init__( + self, + *, + text: Union[str, dict, TextObject], + action_id: Optional[str] = None, + url: Optional[str] = None, + value: Optional[str] = None, + style: Optional[str] = None, # primary, danger + confirm: Optional[Union[dict, ConfirmObject]] = None, + accessibility_label: Optional[str] = None, + **others: dict, + ): + """An interactive element that inserts a button. The button can be a trigger for + anything from opening a simple link to starting a complex workflow. + https://docs.slack.dev/reference/block-kit/block-elements/button-element/ + + Args: + text (required): A text object that defines the button's text. + Can only be of type: plain_text. + Maximum length for the text in this field is 75 characters. + action_id (required): An identifier for this action. + You can use this when you receive an interaction payload to identify the source of the action. + Should be unique among all other action_ids in the containing block. + Maximum length for this field is 255 characters. + url: A URL to load in the user's browser when the button is clicked. + Maximum length for this field is 3000 characters. + If you're using url, you'll still receive an interaction payload + and will need to send an acknowledgement response. + value: The value to send along with the interaction payload. + Maximum length for this field is 2000 characters. + style: Decorates buttons with alternative visual color schemes. Use this option with restraint. + "primary" gives buttons a green outline and text, ideal for affirmation or confirmation actions. + "primary" should only be used for one button within a set. + "danger" gives buttons a red outline and text, and should be used when the action is destructive. + Use "danger" even more sparingly than "primary". + If you don't include this field, the default button style will be used. + confirm: A confirm object that defines an optional confirmation dialog after the button is clicked. + accessibility_label: A label for longer descriptive text about a button element. + This label will be read out by screen readers instead of the button text object. + Maximum length for this field is 75 characters. + """ + super().__init__(action_id=action_id, type=self.type) + show_unknown_key_warning(self, others) + + # NOTE: default_type=PlainTextObject.type here is only for backward-compatibility with version 2.5.0 + self.text = TextObject.parse(text, default_type=PlainTextObject.type) + self.url = url + self.value = value + self.style = style + self.confirm = ConfirmObject.parse(confirm) # type: ignore[arg-type] + self.accessibility_label = accessibility_label + + @JsonValidator(f"text attribute cannot exceed {text_max_length} characters") + def _validate_text_length(self) -> bool: + return self.text is None or self.text.text is None or len(self.text.text) <= self.text_max_length + + @JsonValidator(f"url attribute cannot exceed {url_max_length} characters") + def _validate_url_length(self) -> bool: + return self.url is None or len(self.url) <= self.url_max_length + + @JsonValidator(f"value attribute cannot exceed {value_max_length} characters") + def _validate_value_length(self) -> bool: + return self.value is None or len(self.value) <= self.value_max_length + + @EnumValidator("style", ButtonStyles) + def _validate_style_valid(self): + return self.style is None or self.style in ButtonStyles + + @JsonValidator(f"accessibility_label attribute cannot exceed {text_max_length} characters") + def _validate_accessibility_label_length(self) -> bool: + return self.accessibility_label is None or len(self.accessibility_label) <= self.text_max_length + + +class LinkButtonElement(ButtonElement): + def __init__( + self, + *, + text: Union[str, dict, PlainTextObject], + url: str, + action_id: Optional[str] = None, + style: Optional[str] = None, + **others: dict, + ): + """A simple button that simply opens a given URL. You will still receive an + interaction payload and will need to send an acknowledgement response. + This is a helper class that makes creating links simpler. + https://docs.slack.dev/reference/block-kit/block-elements/button-element/ + + Args: + text (required): A text object that defines the button's text. + Can only be of type: plain_text. + Maximum length for the text in this field is 75 characters. + url (required): A URL to load in the user's browser when the button is clicked. + Maximum length for this field is 3000 characters. + If you're using url, you'll still receive an interaction payload + and will need to send an acknowledgement response. + action_id (required): An identifier for this action. + You can use this when you receive an interaction payload to identify the source of the action. + Should be unique among all other action_ids in the containing block. + Maximum length for this field is 255 characters. + style: Decorates buttons with alternative visual color schemes. Use this option with restraint. + "primary" gives buttons a green outline and text, ideal for affirmation or confirmation actions. + "primary" should only be used for one button within a set. + "danger" gives buttons a red outline and text, and should be used when the action is destructive. + Use "danger" even more sparingly than "primary". + If you don't include this field, the default button style will be used. + """ + super().__init__( + # NOTE: value must be always absent + text=text, + url=url, + action_id=action_id, + value=None, + style=style, + ) + show_unknown_key_warning(self, others) + + +# ------------------------------------------------- +# Checkboxes +# ------------------------------------------------- + + +class CheckboxesElement(InputInteractiveElement): + type = "checkboxes" + + @property + def attributes(self) -> Set[str]: # type: ignore[override] + return super().attributes.union({"options", "initial_options"}) + + def __init__( + self, + *, + action_id: Optional[str] = None, + options: Optional[Sequence[Union[dict, Option]]] = None, + initial_options: Optional[Sequence[Union[dict, Option]]] = None, + confirm: Optional[Union[dict, ConfirmObject]] = None, + focus_on_load: Optional[bool] = None, + **others: dict, + ): + """A checkbox group that allows a user to choose multiple items from a list of possible options. + https://docs.slack.dev/reference/block-kit/block-elements/checkboxes-element/ + + Args: + action_id (required): An identifier for the action triggered when the checkbox group is changed. + You can use this when you receive an interaction payload to identify the source of the action. + Should be unique among all other action_ids in the containing block. + Maximum length for this field is 255 characters. + options (required): An array of option objects. A maximum of 10 options are allowed. + initial_options: An array of option objects that exactly matches one or more of the options. + These options will be selected when the checkbox group initially loads. + confirm: A confirm object that defines an optional confirmation dialog that appears + after clicking one of the checkboxes in this element. + focus_on_load: Indicates whether the element will be set to auto focus within the view object. + Only one element can be set to true. Defaults to false. + """ + super().__init__( + type=self.type, + action_id=action_id, + confirm=ConfirmObject.parse(confirm), # type: ignore[arg-type] + focus_on_load=focus_on_load, + ) + show_unknown_key_warning(self, others) + + self.options = Option.parse_all(options) + self.initial_options = Option.parse_all(initial_options) + + +# ------------------------------------------------- +# DatePicker +# ------------------------------------------------- + + +class DatePickerElement(InputInteractiveElement): + type = "datepicker" + + @property + def attributes(self) -> Set[str]: # type: ignore[override] + return super().attributes.union({"initial_date"}) + + def __init__( + self, + *, + action_id: Optional[str] = None, + placeholder: Optional[Union[str, dict, TextObject]] = None, + initial_date: Optional[str] = None, + confirm: Optional[Union[dict, ConfirmObject]] = None, + focus_on_load: Optional[bool] = None, + **others: dict, + ): + """ + An element which lets users easily select a date from a calendar style UI. + Date picker elements can be used inside of SectionBlocks and ActionsBlocks. + https://docs.slack.dev/reference/block-kit/block-elements/date-picker-element + + Args: + action_id (required): An identifier for the action triggered when a menu option is selected. + You can use this when you receive an interaction payload to identify the source of the action. + Should be unique among all other action_ids in the containing block. + Maximum length for this field is 255 characters. + placeholder: A plain_text only text object that defines the placeholder text shown on the datepicker. + Maximum length for the text in this field is 150 characters. + initial_date: The initial date that is selected when the element is loaded. + This should be in the format YYYY-MM-DD. + confirm: A confirm object that defines an optional confirmation dialog + that appears after a date is selected. + focus_on_load: Indicates whether the element will be set to auto focus within the view object. + Only one element can be set to true. Defaults to false. + """ + super().__init__( + type=self.type, + action_id=action_id, + placeholder=TextObject.parse(placeholder, PlainTextObject.type), # type: ignore[arg-type] + confirm=ConfirmObject.parse(confirm), # type: ignore[arg-type] + focus_on_load=focus_on_load, + ) + show_unknown_key_warning(self, others) + + self.initial_date = initial_date + + @JsonValidator("initial_date attribute must be in format 'YYYY-MM-DD'") + def _validate_initial_date_valid(self) -> bool: + return ( + self.initial_date is None + or re.match(r"\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])", self.initial_date) is not None + ) + + +# ------------------------------------------------- +# TimePicker +# ------------------------------------------------- + + +class TimePickerElement(InputInteractiveElement): + type = "timepicker" + + @property + def attributes(self) -> Set[str]: # type: ignore[override] + return super().attributes.union({"initial_time", "timezone"}) + + def __init__( + self, + *, + action_id: Optional[str] = None, + placeholder: Optional[Union[str, dict, TextObject]] = None, + initial_time: Optional[str] = None, + confirm: Optional[Union[dict, ConfirmObject]] = None, + focus_on_load: Optional[bool] = None, + timezone: Optional[str] = None, + **others: dict, + ): + """ + An element which allows selection of a time of day. + On desktop clients, this time picker will take the form of a dropdown list + with free-text entry for precise choices. + On mobile clients, the time picker will use native time picker UIs. + https://docs.slack.dev/reference/block-kit/block-elements/time-picker-element + + Args: + action_id (required): An identifier for the action triggered when a time is selected. + You can use this when you receive an interaction payload to identify the source of the action. + Should be unique among all other action_ids in the containing block. + Maximum length for this field is 255 characters. + placeholder: A plain_text only text object that defines the placeholder text shown on the timepicker. + Maximum length for the text in this field is 150 characters. + initial_time: The initial time that is selected when the element is loaded. + This should be in the format HH:mm, where HH is the 24-hour format of an hour (00 to 23) + and mm is minutes with leading zeros (00 to 59), for example 22:25 for 10:25pm. + confirm: A confirm object that defines an optional confirmation dialog + that appears after a time is selected. + focus_on_load: Indicates whether the element will be set to auto focus within the view object. + Only one element can be set to true. Defaults to false. + timezone: The timezone to consider for this input value. + """ + super().__init__( + type=self.type, + action_id=action_id, + placeholder=TextObject.parse(placeholder, PlainTextObject.type), # type: ignore[arg-type] + confirm=ConfirmObject.parse(confirm), # type: ignore[arg-type] + focus_on_load=focus_on_load, + ) + show_unknown_key_warning(self, others) + + self.initial_time = initial_time + self.timezone = timezone + + @JsonValidator("initial_time attribute must be in format 'HH:mm'") + def _validate_initial_time_valid(self) -> bool: + return self.initial_time is None or re.match(r"([0-1][0-9]|2[0-3]):([0-5][0-9])", self.initial_time) is not None + + +# ------------------------------------------------- +# DateTimePicker +# ------------------------------------------------- + + +class DateTimePickerElement(InputInteractiveElement): + type = "datetimepicker" + + @property + def attributes(self) -> Set[str]: # type: ignore[override] + return super().attributes.union({"initial_date_time"}) + + def __init__( + self, + *, + action_id: Optional[str] = None, + initial_date_time: Optional[int] = None, + confirm: Optional[Union[dict, ConfirmObject]] = None, + focus_on_load: Optional[bool] = None, + **others: dict, + ): + """ + An element that allows the selection of a time of day formatted as a UNIX timestamp. + On desktop clients, this time picker will take the form of a dropdown list and the + date picker will take the form of a dropdown calendar. Both options will have free-text + entry for precise choices. On mobile clients, the time picker and date + picker will use native UIs. + https://docs.slack.dev/reference/block-kit/block-elements/date-picker-element/ + + Args: + action_id (required): An identifier for the action triggered when a time is selected. You can use this + when you receive an interaction payload to identify the source of the action. Should be unique among + all other action_ids in the containing block. Maximum length for this field is 255 characters. + initial_date_time: The initial date and time that is selected when the element is loaded, represented as + a UNIX timestamp in seconds. This should be in the format of 10 digits, for example 1628633820 + represents the date and time August 10th, 2021 at 03:17pm PST. + and mm is minutes with leading zeros (00 to 59), for example 22:25 for 10:25pm. + confirm: A confirm object that defines an optional confirmation dialog + that appears after a time is selected. + focus_on_load: Indicates whether the element will be set to auto focus within the view object. + Only one element can be set to true. Defaults to false. + """ + super().__init__( + type=self.type, + action_id=action_id, + confirm=ConfirmObject.parse(confirm), # type: ignore[arg-type] + focus_on_load=focus_on_load, + ) + show_unknown_key_warning(self, others) + + self.initial_date_time = initial_date_time + + @JsonValidator("initial_date_time attribute must be between 0 and 99999999 seconds") + def _validate_initial_date_time_valid(self) -> bool: + return self.initial_date_time is None or (0 <= self.initial_date_time <= 9999999999) + + +# ------------------------------------------------- +# Feedback Buttons Element +# ------------------------------------------------- + + +class FeedbackButtonsElement(InteractiveElement): + type = "feedback_buttons" + + @property + def attributes(self) -> Set[str]: # type: ignore[override] + return super().attributes.union({"positive_button", "negative_button"}) + + def __init__( + self, + *, + action_id: Optional[str] = None, + positive_button: Union[dict, FeedbackButtonObject], + negative_button: Union[dict, FeedbackButtonObject], + **others: dict, + ): + """Buttons to indicate positive or negative feedback. + https://docs.slack.dev/reference/block-kit/block-elements/feedback-buttons-element + + Args: + action_id (required): An identifier for this action. + You can use this when you receive an interaction payload to identify the source of the action. + Should be unique among all other action_ids in the containing block. + Maximum length for this field is 255 characters. + positive_button (required): A button to indicate positive feedback. + negative_button (required): A button to indicate negative feedback. + """ + super().__init__(action_id=action_id, type=self.type) + show_unknown_key_warning(self, others) + + self.positive_button = FeedbackButtonObject.parse(positive_button) + self.negative_button = FeedbackButtonObject.parse(negative_button) + + +# ------------------------------------------------- +# Image +# ------------------------------------------------- + + +class ImageElement(BlockElement): + type = "image" + image_url_max_length = 3000 + alt_text_max_length = 2000 + + @property + def attributes(self) -> Set[str]: # type: ignore[override] + return super().attributes.union({"alt_text", "image_url", "slack_file"}) + + def __init__( + self, + *, + alt_text: Optional[str] = None, + image_url: Optional[str] = None, + slack_file: Optional[Union[Dict[str, Any], SlackFile]] = None, + **others: dict, + ): + """An element to insert an image - this element can be used in section and + context blocks only. If you want a block with only an image in it, + you're looking for the image block. + https://docs.slack.dev/reference/block-kit/block-elements/image-element + + Args: + alt_text (required): A plain-text summary of the image. This should not contain any markup. + image_url: The URL of the image to be displayed. + slack_file: A Slack image file object that defines the source of the image. + """ + super().__init__(type=self.type) + show_unknown_key_warning(self, others) + + self.image_url = image_url + self.alt_text = alt_text + self.slack_file = slack_file if slack_file is None or isinstance(slack_file, SlackFile) else SlackFile(**slack_file) + + @JsonValidator(f"image_url attribute cannot exceed {image_url_max_length} characters") + def _validate_image_url_length(self) -> bool: + return self.image_url is None or len(self.image_url) <= self.image_url_max_length + + @JsonValidator(f"alt_text attribute cannot exceed {alt_text_max_length} characters") + def _validate_alt_text_length(self) -> bool: + return len(self.alt_text) <= self.alt_text_max_length # type: ignore[arg-type] + + +# ------------------------------------------------- +# Icon Button Element +# ------------------------------------------------- + + +class IconButtonElement(InteractiveElement): + type = "icon_button" + + @property + def attributes(self) -> Set[str]: # type: ignore[override] + return super().attributes.union({"icon", "text", "accessibility_label", "value", "visible_to_user_ids", "confirm"}) + + def __init__( + self, + *, + action_id: Optional[str] = None, + icon: str, + text: Union[str, dict, TextObject], + accessibility_label: Optional[str] = None, + value: Optional[str] = None, + visible_to_user_ids: Optional[List[str]] = None, + confirm: Optional[Union[dict, ConfirmObject]] = None, + **others: dict, + ): + """An icon button to perform actions. + https://docs.slack.dev/reference/block-kit/block-elements/icon-button-element + + Args: + action_id: An identifier for this action. + You can use this when you receive an interaction payload to identify the source of the action. + Should be unique among all other action_ids in the containing block. + Maximum length for this field is 255 characters. + icon (required): The icon to show (e.g., 'trash'). + text (required): Defines an object containing some text. + accessibility_label: A label for longer descriptive text about a button element. + This label will be read out by screen readers instead of the button text object. + Maximum length for this field is 75 characters. + value: The button value. + Maximum length for this field is 2000 characters. + visible_to_user_ids: User IDs for which the icon appears. + Maximum length for this field is 10 user IDs. + confirm: A confirm object that defines an optional confirmation dialog after the button is clicked. + """ + super().__init__(action_id=action_id, type=self.type) + show_unknown_key_warning(self, others) + + self.icon = icon + self.text = TextObject.parse(text, PlainTextObject.type) + self.accessibility_label = accessibility_label + self.value = value + self.visible_to_user_ids = visible_to_user_ids + self.confirm = ConfirmObject.parse(confirm) if confirm else None + + +# ------------------------------------------------- +# Static Select +# ------------------------------------------------- + + +class StaticSelectElement(InputInteractiveElement): + type = "static_select" + options_max_length = 100 + option_groups_max_length = 100 + + @property + def attributes(self) -> Set[str]: # type: ignore[override] + return super().attributes.union({"options", "option_groups", "initial_option"}) + + def __init__( + self, + *, + placeholder: Optional[Union[str, dict, TextObject]] = None, + action_id: Optional[str] = None, + options: Optional[Sequence[Union[dict, Option]]] = None, + option_groups: Optional[Sequence[Union[dict, OptionGroup]]] = None, + initial_option: Optional[Union[dict, Option]] = None, + confirm: Optional[Union[dict, ConfirmObject]] = None, + focus_on_load: Optional[bool] = None, + **others: dict, + ): + """This is the simplest form of select menu, with a static list of options passed in when defining the element. + https://docs.slack.dev/reference/block-kit/block-elements/select-menu-element#static_select + + Args: + placeholder (required): A plain_text only text object that defines the placeholder text shown on the menu. + Maximum length for the text in this field is 150 characters. + action_id (required): An identifier for the action triggered when a menu option is selected. + You can use this when you receive an interaction payload to identify the source of the action. + Should be unique among all other action_ids in the containing block. + Maximum length for this field is 255 characters. + options (either options or option_groups is required): An array of option objects. + Maximum number of options is 100. + If option_groups is specified, this field should not be. + option_groups (either options or option_groups is required): An array of option group objects. + Maximum number of option groups is 100. + If options is specified, this field should not be. + initial_option: A single option that exactly matches one of the options or option_groups. + This option will be selected when the menu initially loads. + confirm: A confirm object that defines an optional confirmation dialog + that appears after a menu item is selected. + focus_on_load: Indicates whether the element will be set to auto focus within the view object. + Only one element can be set to true. Defaults to false. + """ + super().__init__( + type=self.type, + action_id=action_id, + placeholder=TextObject.parse(placeholder, PlainTextObject.type), # type: ignore[arg-type] + confirm=ConfirmObject.parse(confirm), # type: ignore[arg-type] + focus_on_load=focus_on_load, + ) + show_unknown_key_warning(self, others) + + self.options = options + self.option_groups = option_groups + self.initial_option = initial_option + + @JsonValidator(f"options attribute cannot exceed {options_max_length} elements") + def _validate_options_length(self) -> bool: + return self.options is None or len(self.options) <= self.options_max_length + + @JsonValidator(f"option_groups attribute cannot exceed {option_groups_max_length} elements") + def _validate_option_groups_length(self) -> bool: + return self.option_groups is None or len(self.option_groups) <= self.option_groups_max_length + + @JsonValidator("options and option_groups cannot both be specified") + def _validate_options_and_option_groups_both_specified(self) -> bool: + return not (self.options is not None and self.option_groups is not None) + + @JsonValidator("options or option_groups must be specified") + def _validate_neither_options_or_option_groups_is_specified(self) -> bool: + return self.options is not None or self.option_groups is not None + + +class StaticMultiSelectElement(InputInteractiveElement): + type = "multi_static_select" + options_max_length = 100 + option_groups_max_length = 100 + + @property + def attributes(self) -> Set[str]: # type: ignore[override] + return super().attributes.union({"options", "option_groups", "initial_options", "max_selected_items"}) + + def __init__( + self, + *, + placeholder: Optional[Union[str, dict, TextObject]] = None, + action_id: Optional[str] = None, + options: Optional[Sequence[Option]] = None, + option_groups: Optional[Sequence[OptionGroup]] = None, + initial_options: Optional[Sequence[Option]] = None, + confirm: Optional[Union[dict, ConfirmObject]] = None, + max_selected_items: Optional[int] = None, + focus_on_load: Optional[bool] = None, + **others: dict, + ): + """ + This is the simplest form of select menu, with a static list of options passed in when defining the element. + https://docs.slack.dev/reference/block-kit/block-elements/multi-select-menu-element#static_multi_select + + Args: + placeholder (required): A plain_text only text object that defines the placeholder text shown on the menu. + Maximum length for the text in this field is 150 characters. + action_id (required): An identifier for the action triggered when a menu option is selected. + You can use this when you receive an interaction payload to identify the source of the action. + Should be unique among all other action_ids in the containing block. + Maximum length for this field is 255 characters. + options (either options or option_groups is required): An array of option objects. + Maximum number of options is 100. + If option_groups is specified, this field should not be. + option_groups (either options or option_groups is required): An array of option group objects. + Maximum number of option groups is 100. + If options is specified, this field should not be. + initial_options: An array of option objects that exactly match one or more of the options + within options or option_groups. These options will be selected when the menu initially loads. + confirm: A confirm object that defines an optional confirmation dialog + that appears before the multi-select choices are submitted. + max_selected_items: Specifies the maximum number of items that can be selected in the menu. + Minimum number is 1. + focus_on_load: Indicates whether the element will be set to auto focus within the view object. + Only one element can be set to true. Defaults to false. + """ + super().__init__( + type=self.type, + action_id=action_id, + placeholder=TextObject.parse(placeholder, PlainTextObject.type), # type: ignore[arg-type] + confirm=ConfirmObject.parse(confirm), # type: ignore[arg-type] + focus_on_load=focus_on_load, + ) + show_unknown_key_warning(self, others) + + self.options = Option.parse_all(options) + self.option_groups = OptionGroup.parse_all(option_groups) + self.initial_options = Option.parse_all(initial_options) + self.max_selected_items = max_selected_items + + @JsonValidator(f"options attribute cannot exceed {options_max_length} elements") + def _validate_options_length(self) -> bool: + return self.options is None or len(self.options) <= self.options_max_length + + @JsonValidator(f"option_groups attribute cannot exceed {option_groups_max_length} elements") + def _validate_option_groups_length(self) -> bool: + return self.option_groups is None or len(self.option_groups) <= self.option_groups_max_length + + @JsonValidator("options and option_groups cannot both be specified") + def _validate_options_and_option_groups_both_specified(self) -> bool: + return self.options is None or self.option_groups is None + + @JsonValidator("options or option_groups must be specified") + def _validate_neither_options_or_option_groups_is_specified(self) -> bool: + return self.options is not None or self.option_groups is not None + + +# SelectElement will be deprecated in version 3, use StaticSelectElement instead +class SelectElement(InputInteractiveElement): + type = "static_select" + options_max_length = 100 + option_groups_max_length = 100 + + @property + def attributes(self) -> Set[str]: # type: ignore[override] + return super().attributes.union({"options", "option_groups", "initial_option"}) + + def __init__( + self, + *, + action_id: Optional[str] = None, + placeholder: Optional[str] = None, + options: Optional[Sequence[Option]] = None, + option_groups: Optional[Sequence[OptionGroup]] = None, + initial_option: Optional[Option] = None, + confirm: Optional[Union[dict, ConfirmObject]] = None, + focus_on_load: Optional[bool] = None, + **others: dict, + ): + """This is the simplest form of select menu, with a static list of options passed in when defining the element. + https://docs.slack.dev/reference/block-kit/block-elements/select-menu-element#static_select + + Args: + action_id (required): An identifier for the action triggered when a menu option is selected. + You can use this when you receive an interaction payload to identify the source of the action. + Should be unique among all other action_ids in the containing block. + Maximum length for this field is 255 characters. + placeholder (required): A plain_text only text object that defines the placeholder text shown on the menu. + Maximum length for the text in this field is 150 characters. + options (either options or option_groups is required): An array of option objects. + Maximum number of options is 100. + If option_groups is specified, this field should not be. + option_groups (either options or option_groups is required): An array of option group objects. + Maximum number of option groups is 100. + If options is specified, this field should not be. + initial_option: A single option that exactly matches one of the options or option_groups. + This option will be selected when the menu initially loads. + confirm: A confirm object that defines an optional confirmation dialog + that appears after a menu item is selected. + focus_on_load: Indicates whether the element will be set to auto focus within the view object. + Only one element can be set to true. Defaults to false. + """ + super().__init__( + type=self.type, + action_id=action_id, + placeholder=TextObject.parse(placeholder, PlainTextObject.type), # type: ignore[arg-type] + confirm=ConfirmObject.parse(confirm), # type: ignore[arg-type] + focus_on_load=focus_on_load, + ) + show_unknown_key_warning(self, others) + + self.options = options + self.option_groups = option_groups + self.initial_option = initial_option + + @JsonValidator(f"options attribute cannot exceed {options_max_length} elements") + def _validate_options_length(self) -> bool: + return self.options is None or len(self.options) <= self.options_max_length + + @JsonValidator(f"option_groups attribute cannot exceed {option_groups_max_length} elements") + def _validate_option_groups_length(self) -> bool: + return self.option_groups is None or len(self.option_groups) <= self.option_groups_max_length + + @JsonValidator("options and option_groups cannot both be specified") + def _validate_options_and_option_groups_both_specified(self) -> bool: + return not (self.options is not None and self.option_groups is not None) + + @JsonValidator("options or option_groups must be specified") + def _validate_neither_options_or_option_groups_is_specified(self) -> bool: + return self.options is not None or self.option_groups is not None + + +# ------------------------------------------------- +# External Data Source Select +# ------------------------------------------------- + + +class ExternalDataSelectElement(InputInteractiveElement): + type = "external_select" + + @property + def attributes(self) -> Set[str]: # type: ignore[override] + return super().attributes.union({"min_query_length", "initial_option"}) + + def __init__( + self, + *, + action_id: Optional[str] = None, + placeholder: Optional[Union[str, TextObject]] = None, + initial_option: Union[Optional[Option], Optional[OptionGroup]] = None, + min_query_length: Optional[int] = None, + confirm: Optional[Union[dict, ConfirmObject]] = None, + focus_on_load: Optional[bool] = None, + **others: dict, + ): + """ + This select menu will load its options from an external data source, allowing + for a dynamic list of options. + https://docs.slack.dev/reference/block-kit/block-elements/select-menu-element#external_select + + Args: + action_id (required): An identifier for the action triggered when a menu option is selected. + You can use this when you receive an interaction payload to identify the source of the action. + Should be unique among all other action_ids in the containing block. + Maximum length for this field is 255 characters. + placeholder (required): A plain_text only text object that defines the placeholder text shown on the menu. + Maximum length for the text in this field is 150 characters. + initial_option: A single option that exactly matches one of the options + within the options or option_groups loaded from the external data source. + This option will be selected when the menu initially loads. + min_query_length: When the typeahead field is used, a request will be sent on every character change. + If you prefer fewer requests or more fully ideated queries, + use the min_query_length attribute to tell Slack + the fewest number of typed characters required before dispatch. + The default value is 3. + confirm: A confirm object that defines an optional confirmation dialog + that appears after a menu item is selected. + focus_on_load: Indicates whether the element will be set to auto focus within the view object. + Only one element can be set to true. Defaults to false. + """ + super().__init__( + type=self.type, + action_id=action_id, + placeholder=TextObject.parse(placeholder, PlainTextObject.type), # type: ignore[arg-type] + confirm=ConfirmObject.parse(confirm), # type: ignore[arg-type] + focus_on_load=focus_on_load, + ) + show_unknown_key_warning(self, others) + + self.min_query_length = min_query_length + self.initial_option = initial_option + + +class ExternalDataMultiSelectElement(InputInteractiveElement): + type = "multi_external_select" + + @property + def attributes(self) -> Set[str]: # type: ignore[override] + return super().attributes.union({"min_query_length", "initial_options", "max_selected_items"}) + + def __init__( + self, + *, + placeholder: Optional[Union[str, dict, TextObject]] = None, + action_id: Optional[str] = None, + min_query_length: Optional[int] = None, + initial_options: Optional[Sequence[Union[dict, Option]]] = None, + confirm: Optional[Union[dict, ConfirmObject]] = None, + max_selected_items: Optional[int] = None, + focus_on_load: Optional[bool] = None, + **others: dict, + ): + """ + This select menu will load its options from an external data source, allowing + for a dynamic list of options. + https://docs.slack.dev/reference/block-kit/block-elements/multi-select-menu-element#external_multi_select + + Args: + placeholder (required): A plain_text only text object that defines the placeholder text shown on the menu. + Maximum length for the text in this field is 150 characters. + action_id (required): An identifier for the action triggered when a menu option is selected. + You can use this when you receive an interaction payload to identify the source of the action. + Should be unique among all other action_ids in the containing block. + Maximum length for this field is 255 characters. + min_query_length: When the typeahead field is used, a request will be sent on every character change. + If you prefer fewer requests or more fully ideated queries, + use the min_query_length attribute to tell Slack + the fewest number of typed characters required before dispatch. + The default value is 3 + initial_options: An array of option objects that exactly match one or more of the options + within options or option_groups. These options will be selected when the menu initially loads. + confirm: A confirm object that defines an optional confirmation dialog that appears + before the multi-select choices are submitted. + max_selected_items: Specifies the maximum number of items that can be selected in the menu. + Minimum number is 1. + focus_on_load: Indicates whether the element will be set to auto focus within the view object. + Only one element can be set to true. Defaults to false. + """ + super().__init__( + type=self.type, + action_id=action_id, + placeholder=TextObject.parse(placeholder, PlainTextObject.type), # type: ignore[arg-type] + confirm=ConfirmObject.parse(confirm), # type: ignore[arg-type] + focus_on_load=focus_on_load, + ) + show_unknown_key_warning(self, others) + + self.min_query_length = min_query_length + self.initial_options = Option.parse_all(initial_options) + self.max_selected_items = max_selected_items + + +# ------------------------------------------------- +# Users Select +# ------------------------------------------------- + + +class UserSelectElement(InputInteractiveElement): + type = "users_select" + + @property + def attributes(self) -> Set[str]: # type: ignore[override] + return super().attributes.union({"initial_user"}) + + def __init__( + self, + *, + placeholder: Optional[Union[str, dict, TextObject]] = None, + action_id: Optional[str] = None, + initial_user: Optional[str] = None, + confirm: Optional[Union[dict, ConfirmObject]] = None, + focus_on_load: Optional[bool] = None, + **others: dict, + ): + """ + This select menu will populate its options with a list of Slack users visible to + the current user in the active workspace. + https://docs.slack.dev/reference/block-kit/block-elements/select-menu-element#users_select + + Args: + placeholder (required): A plain_text only text object that defines the placeholder text shown on the menu. + Maximum length for the text in this field is 150 characters. + action_id (required): An identifier for the action triggered when a menu option is selected. + You can use this when you receive an interaction payload to identify the source of the action. + Should be unique among all other action_ids in the containing block. + Maximum length for this field is 255 characters. + initial_user: The user ID of any valid user to be pre-selected when the menu loads. + confirm: A confirm object that defines an optional confirmation dialog + that appears after a menu item is selected. + focus_on_load: Indicates whether the element will be set to auto focus within the view object. + Only one element can be set to true. Defaults to false. + """ + super().__init__( + type=self.type, + action_id=action_id, + placeholder=TextObject.parse(placeholder, PlainTextObject.type), # type: ignore[arg-type] + confirm=ConfirmObject.parse(confirm), # type: ignore[arg-type] + focus_on_load=focus_on_load, + ) + show_unknown_key_warning(self, others) + + self.initial_user = initial_user + + +class UserMultiSelectElement(InputInteractiveElement): + type = "multi_users_select" + + @property + def attributes(self) -> Set[str]: # type: ignore[override] + return super().attributes.union({"initial_users", "max_selected_items"}) + + def __init__( + self, + *, + action_id: Optional[str] = None, + placeholder: Optional[Union[str, dict, TextObject]] = None, + initial_users: Optional[Sequence[str]] = None, + confirm: Optional[Union[dict, ConfirmObject]] = None, + max_selected_items: Optional[int] = None, + focus_on_load: Optional[bool] = None, + **others: dict, + ): + """ + This select menu will populate its options with a list of Slack users visible to + the current user in the active workspace. + https://docs.slack.dev/reference/block-kit/block-elements/multi-select-menu-element#users_multi_select + + Args: + action_id (required): An identifier for the action triggered when a menu option is selected. + You can use this when you receive an interaction payload to identify the source of the action. + Should be unique among all other action_ids in the containing block. + Maximum length for this field is 255 characters. + placeholder (required): A plain_text only text object that defines the placeholder text shown on the menu. + Maximum length for the text in this field is 150 characters. + initial_users: An array of user IDs of any valid users to be pre-selected when the menu loads. + confirm: A confirm object that defines an optional confirmation dialog that appears + before the multi-select choices are submitted. + max_selected_items: Specifies the maximum number of items that can be selected in the menu. + Minimum number is 1. + focus_on_load: Indicates whether the element will be set to auto focus within the view object. + Only one element can be set to true. Defaults to false. + """ + super().__init__( + type=self.type, + action_id=action_id, + placeholder=TextObject.parse(placeholder, PlainTextObject.type), # type: ignore[arg-type] + confirm=ConfirmObject.parse(confirm), # type: ignore[arg-type] + focus_on_load=focus_on_load, + ) + show_unknown_key_warning(self, others) + + self.initial_users = initial_users + self.max_selected_items = max_selected_items + + +# ------------------------------------------------- +# Conversations Select +# ------------------------------------------------- + + +class ConversationFilter(JsonObject): + attributes = {"include", "exclude_bot_users", "exclude_external_shared_channels"} + logger = logging.getLogger(__name__) + + def __init__( + self, + *, + include: Optional[Sequence[str]] = None, + exclude_bot_users: Optional[bool] = None, + exclude_external_shared_channels: Optional[bool] = None, + ): + """Provides a way to filter the list of options in a conversations select menu + or conversations multi-select menu. + https://docs.slack.dev/reference/block-kit/composition-objects/conversation-filter-object + + Args: + include: Indicates which type of conversations should be included in the list. + When this field is provided, any conversations that do not match will be excluded. + You should provide an array of strings from the following options: + "im", "mpim", "private", and "public". The array cannot be empty. + exclude_bot_users: Indicates whether to exclude bot users from conversation lists. Defaults to false. + exclude_external_shared_channels: Indicates whether to exclude external shared channels + from conversation lists. Defaults to false. + """ + self.include = include + self.exclude_bot_users = exclude_bot_users + self.exclude_external_shared_channels = exclude_external_shared_channels + + @classmethod + def parse(cls, filter: Union[dict, "ConversationFilter"]): + if filter is None: + return None + elif isinstance(filter, ConversationFilter): + return filter + elif isinstance(filter, dict): + d = copy.copy(filter) + return ConversationFilter(**d) + else: + cls.logger.warning(f"Unknown conversation filter object detected and skipped ({filter})") + return None + + +class ConversationSelectElement(InputInteractiveElement): + type = "conversations_select" + + @property + def attributes(self) -> Set[str]: # type: ignore[override] + return super().attributes.union( + { + "initial_conversation", + "response_url_enabled", + "filter", + "default_to_current_conversation", + } + ) + + def __init__( + self, + *, + placeholder: Optional[Union[str, dict, TextObject]] = None, + action_id: Optional[str] = None, + initial_conversation: Optional[str] = None, + confirm: Optional[Union[dict, ConfirmObject]] = None, + response_url_enabled: Optional[bool] = None, + default_to_current_conversation: Optional[bool] = None, + filter: Optional[ConversationFilter] = None, + focus_on_load: Optional[bool] = None, + **others: dict, + ): + """ + This select menu will populate its options with a list of public and private + channels, DMs, and MPIMs visible to the current user in the active workspace. + https://docs.slack.dev/reference/block-kit/block-elements/select-menu-element/#conversations_select + + Args: + placeholder (required): A plain_text only text object that defines the placeholder text shown on the menu. + Maximum length for the text in this field is 150 characters. + action_id (required): An identifier for the action triggered when a menu option is selected. + You can use this when you receive an interaction payload to identify the source of the action. + Should be unique among all other action_ids in the containing block. + Maximum length for this field is 255 characters. + initial_conversation: The ID of any valid conversation to be pre-selected when the menu loads. + If default_to_current_conversation is also supplied, initial_conversation will take precedence. + confirm: A confirm object that defines an optional confirmation dialog + that appears after a menu item is selected. + response_url_enabled: This field only works with menus in input blocks in modals. + When set to true, the view_submission payload from the menu's parent view will contain a response_url. + This response_url can be used for message responses. The target conversation for the message + will be determined by the value of this select menu. + default_to_current_conversation: Pre-populates the select menu with the conversation + that the user was viewing when they opened the modal, if available. Default is false. + filter: A filter object that reduces the list of available conversations using the specified criteria. + focus_on_load: Indicates whether the element will be set to auto focus within the view object. + Only one element can be set to true. Defaults to false. + """ + super().__init__( + type=self.type, + action_id=action_id, + placeholder=TextObject.parse(placeholder, PlainTextObject.type), # type: ignore[arg-type] + confirm=ConfirmObject.parse(confirm), # type: ignore[arg-type] + focus_on_load=focus_on_load, + ) + show_unknown_key_warning(self, others) + + self.initial_conversation = initial_conversation + self.response_url_enabled = response_url_enabled + self.default_to_current_conversation = default_to_current_conversation + self.filter = filter + + +class ConversationMultiSelectElement(InputInteractiveElement): + type = "multi_conversations_select" + + @property + def attributes(self) -> Set[str]: # type: ignore[override] + return super().attributes.union( + { + "initial_conversations", + "max_selected_items", + "default_to_current_conversation", + "filter", + } + ) + + def __init__( + self, + *, + placeholder: Optional[Union[str, dict, TextObject]] = None, + action_id: Optional[str] = None, + initial_conversations: Optional[Sequence[str]] = None, + confirm: Optional[Union[dict, ConfirmObject]] = None, + max_selected_items: Optional[int] = None, + default_to_current_conversation: Optional[bool] = None, + filter: Optional[Union[dict, ConversationFilter]] = None, + focus_on_load: Optional[bool] = None, + **others: dict, + ): + """ + This multi-select menu will populate its options with a list of public and private channels, + DMs, and MPIMs visible to the current user in the active workspace. + https://docs.slack.dev/reference/block-kit/block-elements/multi-select-menu-element/#conversation_multi_select + + Args: + placeholder (required): A plain_text only text object that defines the placeholder text shown on the menu. + Maximum length for the text in this field is 150 characters. + action_id (required): An identifier for the action triggered when a menu option is selected. + You can use this when you receive an interaction payload to identify the source of the action. + Should be unique among all other action_ids in the containing block. + Maximum length for this field is 255 characters. + initial_conversations: An array of one or more IDs of any valid conversations to be pre-selected + when the menu loads. If default_to_current_conversation is also supplied, + initial_conversations will be ignored. + confirm: A confirm object that defines an optional confirmation dialog that appears + before the multi-select choices are submitted. + max_selected_items: Specifies the maximum number of items that can be selected in the menu. + Minimum number is 1. + default_to_current_conversation: Pre-populates the select menu with the conversation that + the user was viewing when they opened the modal, if available. Default is false. + filter: A filter object that reduces the list of available conversations using the specified criteria. + focus_on_load: Indicates whether the element will be set to auto focus within the view object. + Only one element can be set to true. Defaults to false. + """ + super().__init__( + type=self.type, + action_id=action_id, + placeholder=TextObject.parse(placeholder, PlainTextObject.type), # type: ignore[arg-type] + confirm=ConfirmObject.parse(confirm), # type: ignore[arg-type] + focus_on_load=focus_on_load, + ) + show_unknown_key_warning(self, others) + + self.initial_conversations = initial_conversations + self.max_selected_items = max_selected_items + self.default_to_current_conversation = default_to_current_conversation + self.filter = ConversationFilter.parse(filter) # type: ignore[arg-type] + + +# ------------------------------------------------- +# Channels Select +# ------------------------------------------------- + + +class ChannelSelectElement(InputInteractiveElement): + type = "channels_select" + + @property + def attributes(self) -> Set[str]: # type: ignore[override] + return super().attributes.union({"initial_channel", "response_url_enabled"}) + + def __init__( + self, + *, + placeholder: Optional[Union[str, dict, TextObject]] = None, + action_id: Optional[str] = None, + initial_channel: Optional[str] = None, + confirm: Optional[Union[dict, ConfirmObject]] = None, + response_url_enabled: Optional[bool] = None, + focus_on_load: Optional[bool] = None, + **others: dict, + ): + """ + This select menu will populate its options with a list of public channels + visible to the current user in the active workspace. + https://docs.slack.dev/reference/block-kit/block-elements/select-menu-element/#channels_select + + Args: + placeholder (required): A plain_text only text object that defines the placeholder text shown on the menu. + Maximum length for the text in this field is 150 characters. + action_id (required): An identifier for the action triggered when a menu option is selected. + You can use this when you receive an interaction payload to identify the source of the action. + Should be unique among all other action_ids in the containing block. + Maximum length for this field is 255 characters. + initial_channel: The ID of any valid public channel to be pre-selected when the menu loads. + confirm: A confirm object that defines an optional confirmation dialog that appears + after a menu item is selected. + response_url_enabled: This field only works with menus in input blocks in modals. + When set to true, the view_submission payload from the menu's parent view will contain a response_url. + This response_url can be used for message responses. + The target channel for the message will be determined by the value of this select menu + focus_on_load: Indicates whether the element will be set to auto focus within the view object. + Only one element can be set to true. Defaults to false. + """ + super().__init__( + type=self.type, + action_id=action_id, + placeholder=TextObject.parse(placeholder, PlainTextObject.type), # type: ignore[arg-type] + confirm=ConfirmObject.parse(confirm), # type: ignore[arg-type] + focus_on_load=focus_on_load, + ) + show_unknown_key_warning(self, others) + + self.initial_channel = initial_channel + self.response_url_enabled = response_url_enabled + + +class ChannelMultiSelectElement(InputInteractiveElement): + type = "multi_channels_select" + + @property + def attributes(self) -> Set[str]: # type: ignore[override] + return super().attributes.union({"initial_channels", "max_selected_items"}) + + def __init__( + self, + *, + placeholder: Optional[Union[str, dict, TextObject]] = None, + action_id: Optional[str] = None, + initial_channels: Optional[Sequence[str]] = None, + confirm: Optional[Union[dict, ConfirmObject]] = None, + max_selected_items: Optional[int] = None, + focus_on_load: Optional[bool] = None, + **others: dict, + ): + """ + This multi-select menu will populate its options with a list of public channels visible + to the current user in the active workspace. + https://docs.slack.dev/reference/block-kit/block-elements/multi-select-menu-element#channel_multi_select + + Args: + placeholder (required): A plain_text only text object that defines the placeholder text shown on the menu. + Maximum length for the text in this field is 150 characters. + action_id (required): An identifier for the action triggered when a menu option is selected. + You can use this when you receive an interaction payload to identify the source of the action. + Should be unique among all other action_ids in the containing block. + Maximum length for this field is 255 characters. + initial_channels: An array of one or more IDs of any valid public channel + to be pre-selected when the menu loads. + confirm: A confirm object that defines an optional confirmation dialog that appears + before the multi-select choices are submitted. + max_selected_items: Specifies the maximum number of items that can be selected in the menu. + Minimum number is 1. + focus_on_load: Indicates whether the element will be set to auto focus within the view object. + Only one element can be set to true. Defaults to false. + """ + super().__init__( + type=self.type, + action_id=action_id, + placeholder=TextObject.parse(placeholder, PlainTextObject.type), # type: ignore[arg-type] + confirm=ConfirmObject.parse(confirm), # type: ignore[arg-type] + focus_on_load=focus_on_load, + ) + show_unknown_key_warning(self, others) + + self.initial_channels = initial_channels + self.max_selected_items = max_selected_items + + +# ------------------------------------------------- +# Rich Text Input Element +# ------------------------------------------------- + + +class RichTextInputElement(InputInteractiveElement): + type = "rich_text_input" + + @property + def attributes(self) -> Set[str]: # type: ignore[override] + return super().attributes.union( + { + "initial_value", + "dispatch_action_config", + } + ) + + def __init__( + self, + *, + action_id: Optional[str] = None, + placeholder: Optional[Union[str, dict, TextObject]] = None, + # To avoid circular imports, the RichTextBlock type here is intentionally a string + initial_value: Optional[Union[Dict[str, Any], "RichTextBlock"]] = None, # type: ignore[name-defined] # noqa: F821 + dispatch_action_config: Optional[Union[dict, DispatchActionConfig]] = None, + focus_on_load: Optional[bool] = None, + **others: dict, + ): + super().__init__( + type=self.type, + action_id=action_id, + placeholder=TextObject.parse(placeholder, PlainTextObject.type), # type: ignore[arg-type] + focus_on_load=focus_on_load, + ) + show_unknown_key_warning(self, others) + + self.initial_value = initial_value + self.dispatch_action_config = dispatch_action_config + + +# ------------------------------------------------- +# Plain Text Input Element +# ------------------------------------------------- + + +class PlainTextInputElement(InputInteractiveElement): + type = "plain_text_input" + + @property + def attributes(self) -> Set[str]: # type: ignore[override] + return super().attributes.union( + { + "initial_value", + "multiline", + "min_length", + "max_length", + "dispatch_action_config", + } + ) + + def __init__( + self, + *, + action_id: Optional[str] = None, + placeholder: Optional[Union[str, dict, TextObject]] = None, + initial_value: Optional[str] = None, + multiline: Optional[bool] = None, + min_length: Optional[int] = None, + max_length: Optional[int] = None, + dispatch_action_config: Optional[Union[dict, DispatchActionConfig]] = None, + focus_on_load: Optional[bool] = None, + **others: dict, + ): + """ + A plain-text input, similar to the HTML tag, creates a field + where a user can enter freeform data. It can appear as a single-line + field or a larger textarea using the multiline flag. Plain-text input + elements can be used inside of SectionBlocks and ActionsBlocks. + https://docs.slack.dev/reference/block-kit/block-elements/plain-text-input-element + + Args: + action_id (required): An identifier for the input value when the parent modal is submitted. + You can use this when you receive a view_submission payload to identify the value of the input element. + Should be unique among all other action_ids in the containing block. + Maximum length for this field is 255 characters. + placeholder: A plain_text only text object that defines the placeholder text shown + in the plain-text input. Maximum length for the text in this field is 150 characters. + initial_value: The initial value in the plain-text input when it is loaded. + multiline: Indicates whether the input will be a single line (false) or a larger textarea (true). + Defaults to false. + min_length: The minimum length of input that the user must provide. If the user provides less, + they will receive an error. Maximum value is 3000. + max_length: The maximum length of input that the user can provide. If the user provides more, + they will receive an error. + dispatch_action_config: A dispatch configuration object that determines when + during text input the element returns a block_actions payload. + focus_on_load: Indicates whether the element will be set to auto focus within the view object. + Only one element can be set to true. Defaults to false. + """ + super().__init__( + type=self.type, + action_id=action_id, + placeholder=TextObject.parse(placeholder, PlainTextObject.type), # type: ignore[arg-type] + focus_on_load=focus_on_load, + ) + show_unknown_key_warning(self, others) + + self.initial_value = initial_value + self.multiline = multiline + self.min_length = min_length + self.max_length = max_length + self.dispatch_action_config = dispatch_action_config + + +# ------------------------------------------------- +# Email Input Element +# ------------------------------------------------- + + +class EmailInputElement(InputInteractiveElement): + type = "email_text_input" + + @property + def attributes(self) -> Set[str]: # type: ignore[override] + return super().attributes.union( + { + "initial_value", + "dispatch_action_config", + } + ) + + def __init__( + self, + *, + action_id: Optional[str] = None, + initial_value: Optional[str] = None, + dispatch_action_config: Optional[Union[dict, DispatchActionConfig]] = None, + focus_on_load: Optional[bool] = None, + placeholder: Optional[Union[str, dict, TextObject]] = None, + **others: dict, + ): + """ + https://docs.slack.dev/reference/block-kit/block-elements/email-input-element + + Args: + action_id (required): An identifier for the input value when the parent modal is submitted. + You can use this when you receive a view_submission payload to identify the value of the input element. + Should be unique among all other action_ids in the containing block. + Maximum length for this field is 255 characters. + initial_value: The initial value in the email input when it is loaded. + dispatch_action_config: dispatch configuration object that determines when during + text input the element returns a block_actions payload. + focus_on_load: Indicates whether the element will be set to auto focus within the view object. + Only one element can be set to true. Defaults to false. + placeholder: A plain_text only text object that defines the placeholder text shown in the + email input. Maximum length for the text in this field is 150 characters. + """ + super().__init__( + type=self.type, + action_id=action_id, + placeholder=TextObject.parse(placeholder, PlainTextObject.type), # type: ignore[arg-type] + focus_on_load=focus_on_load, + ) + show_unknown_key_warning(self, others) + + self.initial_value = initial_value + self.dispatch_action_config = dispatch_action_config + + +# ------------------------------------------------- +# Url Input Element +# ------------------------------------------------- + + +class UrlInputElement(InputInteractiveElement): + type = "url_text_input" + + @property + def attributes(self) -> Set[str]: # type: ignore[override] + return super().attributes.union( + { + "initial_value", + "dispatch_action_config", + } + ) + + def __init__( + self, + *, + action_id: Optional[str] = None, + initial_value: Optional[str] = None, + dispatch_action_config: Optional[Union[dict, DispatchActionConfig]] = None, + focus_on_load: Optional[bool] = None, + placeholder: Optional[Union[str, dict, TextObject]] = None, + **others: dict, + ): + """ + A URL input element, similar to the Plain-text input element, + creates a single line field where a user can enter URL-encoded data. + https://docs.slack.dev/reference/block-kit/block-elements/url-input-element + + Args: + action_id (required): An identifier for the input value when the parent modal is submitted. + You can use this when you receive a view_submission payload to identify the value of the input element. + Should be unique among all other action_ids in the containing block. + Maximum length for this field is 255 characters. + initial_value: The initial value in the URL input when it is loaded. + dispatch_action_config: A dispatch configuration object that determines when during text input + the element returns a block_actions payload. + focus_on_load: Indicates whether the element will be set to auto focus within the view object. + Only one element can be set to true. Defaults to false. + placeholder: A plain_text only text object that defines the placeholder text shown in the URL input. + Maximum length for the text in this field is 150 characters. + """ + super().__init__( + type=self.type, + action_id=action_id, + placeholder=TextObject.parse(placeholder, PlainTextObject.type), # type: ignore[arg-type] + focus_on_load=focus_on_load, + ) + show_unknown_key_warning(self, others) + + self.initial_value = initial_value + self.dispatch_action_config = dispatch_action_config + + +# ------------------------------------------------- +# Number Input Element +# ------------------------------------------------- + + +class NumberInputElement(InputInteractiveElement): + type = "number_input" + + @property + def attributes(self) -> Set[str]: # type: ignore[override] + return super().attributes.union( + { + "initial_value", + "is_decimal_allowed", + "min_value", + "max_value", + "dispatch_action_config", + } + ) + + def __init__( + self, + *, + action_id: Optional[str] = None, + is_decimal_allowed: Optional[bool] = False, + initial_value: Optional[Union[int, float, str]] = None, + min_value: Optional[Union[int, float, str]] = None, + max_value: Optional[Union[int, float, str]] = None, + dispatch_action_config: Optional[Union[dict, DispatchActionConfig]] = None, + focus_on_load: Optional[bool] = None, + placeholder: Optional[Union[str, dict, TextObject]] = None, + **others: dict, + ): + """ + https://docs.slack.dev/reference/block-kit/block-elements/number-input-element/ + + Args: + action_id (required): An identifier for the input value when the parent modal is submitted. + You can use this when you receive a view_submission payload to identify the value of the input element. + Should be unique among all other action_ids in the containing block. + Maximum length for this field is 255 characters. + is_decimal_allowed (required): Decimal numbers are allowed if is_decimal_allowed= true, set the value to + false otherwise. + initial_value: The initial value in the number input when it is loaded. + min_value: The minimum value, cannot be greater than max_value. + max_value: The maximum value, cannot be less than min_value. + dispatch_action_config: A dispatch configuration object that determines when + during text input the element returns a block_actions payload. + focus_on_load: Indicates whether the element will be set to auto focus within the view object. + Only one element can be set to true. Defaults to false. + placeholder: A plain_text only text object that defines the placeholder text shown + in the plain-text input. Maximum length for the text in this field is 150 characters. + """ + super().__init__( + type=self.type, + action_id=action_id, + placeholder=TextObject.parse(placeholder, PlainTextObject.type), # type: ignore[arg-type] + focus_on_load=focus_on_load, + ) + show_unknown_key_warning(self, others) + + self.initial_value = str(initial_value) if initial_value is not None else None + self.is_decimal_allowed = is_decimal_allowed + self.min_value = str(min_value) if min_value is not None else None + self.max_value = str(max_value) if max_value is not None else None + self.dispatch_action_config = dispatch_action_config + + +# ------------------------------------------------- +# File Input Element +# ------------------------------------------------- + + +class FileInputElement(InputInteractiveElement): + type = "file_input" + + @property + def attributes(self) -> Set[str]: # type: ignore[override] + return super().attributes.union( + { + "filetypes", + "max_files", + } + ) + + def __init__( + self, + *, + action_id: Optional[str] = None, + filetypes: Optional[List[str]] = None, + max_files: Optional[int] = None, + **others: dict, + ): + """ + https://docs.slack.dev/reference/block-kit/block-elements/file-input-element + + Args: + action_id (required): An identifier for the input value when the parent modal is submitted. + You can use this when you receive a view_submission payload to identify the value of the input element. + Should be unique among all other action_ids in the containing block. Maximum length is 255 characters. + filetypes: An array of valid file extensions that will be accepted for this element. + All file extensions will be accepted if filetypes is not specified. + This validation is provided for convenience only, + and you should perform your own file type validation based on what you expect to receive. + max_files: Maximum number of files that can be uploaded for this file_input element. + Minimum of 1, maximum of 10. Defaults to 10 if not specified. + """ + super().__init__( + type=self.type, + action_id=action_id, + ) + show_unknown_key_warning(self, others) + + self.filetypes = filetypes + self.max_files = max_files + + +# ------------------------------------------------- +# Radio Buttons Select +# ------------------------------------------------- + + +class RadioButtonsElement(InputInteractiveElement): + type = "radio_buttons" + + @property + def attributes(self) -> Set[str]: # type: ignore[override] + return super().attributes.union({"options", "initial_option"}) + + def __init__( + self, + *, + action_id: Optional[str] = None, + options: Optional[Sequence[Union[dict, Option]]] = None, + initial_option: Optional[Union[dict, Option]] = None, + confirm: Optional[Union[dict, ConfirmObject]] = None, + focus_on_load: Optional[bool] = None, + **others: dict, + ): + """A radio button group that allows a user to choose one item from a list of possible options. + https://docs.slack.dev/reference/block-kit/block-elements/radio-button-group-element + + Args: + action_id (required): An identifier for the action triggered when the radio button group is changed. + You can use this when you receive an interaction payload to identify the source of the action. + Should be unique among all other action_ids in the containing block. + Maximum length for this field is 255 characters. + options (required): An array of option objects. A maximum of 10 options are allowed. + initial_option: An option object that exactly matches one of the options. + This option will be selected when the radio button group initially loads. + confirm: A confirm object that defines an optional confirmation dialog that appears + after clicking one of the radio buttons in this element. + focus_on_load: Indicates whether the element will be set to auto focus within the view object. + Only one element can be set to true. Defaults to false. + """ + super().__init__( + type=self.type, + action_id=action_id, + confirm=ConfirmObject.parse(confirm), # type: ignore[arg-type] + focus_on_load=focus_on_load, + ) + show_unknown_key_warning(self, others) + + self.options = options + self.initial_option = initial_option + + +# ------------------------------------------------- +# Overflow Menu Select +# ------------------------------------------------- + + +class OverflowMenuElement(InteractiveElement): + type = "overflow" + options_min_length = 1 + options_max_length = 5 + + @property + def attributes(self) -> Set[str]: # type: ignore[override] + return super().attributes.union({"confirm", "options"}) + + def __init__( + self, + *, + action_id: Optional[str] = None, + options: Sequence[Option], + confirm: Optional[Union[dict, ConfirmObject]] = None, + **others: dict, + ): + """ + This is like a cross between a button and a select menu - when a user clicks + on this overflow button, they will be presented with a list of options to + choose from. Unlike the select menu, there is no typeahead field, and the + button always appears with an ellipsis ("…") rather than customisable text. + + As such, it is usually used if you want a more compact layout than a select + menu, or to supply a list of less visually important actions after a row of + buttons. You can also specify simple URL links as overflow menu options, + instead of actions. + + https://docs.slack.dev/reference/block-kit/block-elements/overflow-menu-element + + Args: + action_id (required): An identifier for the action triggered when a menu option is selected. + You can use this when you receive an interaction payload to identify the source of the action. + Should be unique among all other action_ids in the containing block. + Maximum length for this field is 255 characters. + options (required): An array of option objects to display in the menu. + Maximum number of options is 5, minimum is 1. + confirm: A confirm object that defines an optional confirmation dialog that appears + after a menu item is selected. + """ + super().__init__(action_id=action_id, type=self.type) + show_unknown_key_warning(self, others) + + self.options = options + self.confirm = ConfirmObject.parse(confirm) # type: ignore[arg-type] + + @JsonValidator(f"options attribute must have between {options_min_length} " f"and {options_max_length} items") + def _validate_options_length(self) -> bool: + return self.options_min_length <= len(self.options) <= self.options_max_length + + +# ------------------------------------------------- +# Workflow Button +# ------------------------------------------------- + + +class WorkflowButtonElement(InteractiveElement): + type = "workflow_button" + + @property + def attributes(self) -> Set[str]: # type: ignore[override] + return super().attributes.union({"text", "workflow", "style", "accessibility_label"}) + + def __init__( + self, + *, + text: Union[str, dict, TextObject], + action_id: Optional[str] = None, + workflow: Optional[Union[dict, Workflow]] = None, + style: Optional[str] = None, # primary, danger + accessibility_label: Optional[str] = None, + **others: dict, + ): + """Allows users to run a link trigger with customizable inputs + Interactive component - but interactions with workflow button elements will not send block_actions events, + since these are used to start new workflow runs. + https://docs.slack.dev/reference/block-kit/block-elements/workflow-button-element + + Args: + text (required): A text object that defines the button's text. + Can only be of type: plain_text. text may truncate with ~30 characters. + Maximum length for the text in this field is 75 characters. + action_id (required): An identifier for this action. + You can use this when you receive an interaction payload to identify the source of the action. + Should be unique among all other action_ids in the containing block. + Maximum length for this field is 255 characters. + workflow: A workflow object that contains details about the workflow + that will run when the button is clicked. + style: Decorates buttons with alternative visual color schemes. Use this option with restraint. + "primary" gives buttons a green outline and text, ideal for affirmation or confirmation actions. + "primary" should only be used for one button within a set. + "danger" gives buttons a red outline and text, and should be used when the action is destructive. + Use "danger" even more sparingly than "primary". + If you don't include this field, the default button style will be used. + accessibility_label: A label for longer descriptive text about a button element. + This label will be read out by screen readers instead of the button text object. + Maximum length for this field is 75 characters. + """ + super().__init__(action_id=action_id, type=self.type) + show_unknown_key_warning(self, others) + + # NOTE: default_type=PlainTextObject.type here is only for backward-compatibility with version 2.5.0 + self.text = TextObject.parse(text, default_type=PlainTextObject.type) + self.workflow = workflow + self.style = style + self.accessibility_label = accessibility_label + + +# ------------------------------------------------- +# Rich text elements +# ------------------------------------------------- + + +class RichTextElement(BlockElement): + pass + + +class RichTextListElement(RichTextElement): + type = "rich_text_list" + + @property + def attributes(self) -> Set[str]: # type: ignore[override] + return super().attributes.union({"elements", "style", "indent", "offset", "border"}) + + def __init__( + self, + *, + elements: Sequence[Union[dict, RichTextElement]], + style: Optional[str] = None, # bullet, ordered + indent: Optional[int] = None, + offset: Optional[int] = None, + border: Optional[int] = None, + **others: dict, + ): + super().__init__(type=self.type) + show_unknown_key_warning(self, others) + self.elements = BlockElement.parse_all(elements) + self.style = style + self.indent = indent + self.offset = offset + self.border = border + + +class RichTextPreformattedElement(RichTextElement): + type = "rich_text_preformatted" + + @property + def attributes(self) -> Set[str]: # type: ignore[override] + return super().attributes.union({"elements", "border"}) + + def __init__( + self, + *, + elements: Sequence[Union[dict, RichTextElement]], + border: Optional[int] = None, + **others: dict, + ): + super().__init__(type=self.type) + show_unknown_key_warning(self, others) + self.elements = BlockElement.parse_all(elements) + self.border = border + + +class RichTextQuoteElement(RichTextElement): + type = "rich_text_quote" + + @property + def attributes(self) -> Set[str]: # type: ignore[override] + return super().attributes.union({"elements"}) + + def __init__( + self, + *, + elements: Sequence[Union[dict, RichTextElement]], + **others: dict, + ): + super().__init__(type=self.type) + show_unknown_key_warning(self, others) + self.elements = BlockElement.parse_all(elements) + + +class RichTextSectionElement(RichTextElement): + type = "rich_text_section" + + @property + def attributes(self) -> Set[str]: # type: ignore[override] + return super().attributes.union({"elements"}) + + def __init__( + self, + *, + elements: Sequence[Union[dict, RichTextElement]], + **others: dict, + ): + super().__init__(type=self.type) + show_unknown_key_warning(self, others) + self.elements = BlockElement.parse_all(elements) + + +class RichTextElementParts: + class TextStyle: + def __init__( + self, + *, + bold: Optional[bool] = None, + italic: Optional[bool] = None, + strike: Optional[bool] = None, + code: Optional[bool] = None, + underline: Optional[bool] = None, + ): + self.bold = bold + self.italic = italic + self.strike = strike + self.code = code + self.underline = underline + + def to_dict(self, *args) -> dict: + result = { + "bold": self.bold, + "italic": self.italic, + "strike": self.strike, + "code": self.code, + "underline": self.underline, + } + return {k: v for k, v in result.items() if v is not None} + + class Text(RichTextElement): + type = "text" + + @property + def attributes(self) -> Set[str]: # type: ignore[override] + return super().attributes.union({"text", "style"}) + + def __init__( + self, + *, + text: str, + style: Optional[Union[dict, "RichTextElementParts.TextStyle"]] = None, + **others: dict, + ): + super().__init__(type=self.type) + show_unknown_key_warning(self, others) + self.text = text + self.style = style + + class Channel(RichTextElement): + type = "channel" + + @property + def attributes(self) -> Set[str]: # type: ignore[override] + return super().attributes.union({"channel_id", "style"}) + + def __init__( + self, + *, + channel_id: str, + style: Optional[Union[dict, "RichTextElementParts.TextStyle"]] = None, + **others: dict, + ): + super().__init__(type=self.type) + show_unknown_key_warning(self, others) + self.channel_id = channel_id + self.style = style + + class User(RichTextElement): + type = "user" + + @property + def attributes(self) -> Set[str]: # type: ignore[override] + return super().attributes.union({"user_id", "style"}) + + def __init__( + self, + *, + user_id: str, + style: Optional[Union[dict, "RichTextElementParts.TextStyle"]] = None, + **others: dict, + ): + super().__init__(type=self.type) + show_unknown_key_warning(self, others) + self.user_id = user_id + self.style = style + + class Emoji(RichTextElement): + type = "emoji" + + @property + def attributes(self) -> Set[str]: # type: ignore[override] + return super().attributes.union({"name", "skin_tone", "unicode", "style"}) + + def __init__( + self, + *, + name: str, + skin_tone: Optional[int] = None, + unicode: Optional[str] = None, + style: Optional[Union[dict, "RichTextElementParts.TextStyle"]] = None, + **others: dict, + ): + super().__init__(type=self.type) + show_unknown_key_warning(self, others) + self.name = name + self.skin_tone = skin_tone + self.unicode = unicode + self.style = style + + class Link(RichTextElement): + type = "link" + + @property + def attributes(self) -> Set[str]: # type: ignore[override] + return super().attributes.union({"url", "text", "style"}) + + def __init__( + self, + *, + url: str, + text: Optional[str] = None, + style: Optional[Union[dict, "RichTextElementParts.TextStyle"]] = None, + **others: dict, + ): + super().__init__(type=self.type) + show_unknown_key_warning(self, others) + self.url = url + self.text = text + self.style = style + + class Team(RichTextElement): + type = "team" + + @property + def attributes(self) -> Set[str]: # type: ignore[override] + return super().attributes.union({"team_id", "style"}) + + def __init__( + self, + *, + team_id: str, + style: Optional[Union[dict, "RichTextElementParts.TextStyle"]] = None, + **others: dict, + ): + super().__init__(type=self.type) + show_unknown_key_warning(self, others) + self.team_id = team_id + self.style = style + + class UserGroup(RichTextElement): + type = "usergroup" + + @property + def attributes(self) -> Set[str]: # type: ignore[override] + return super().attributes.union({"usergroup_id", "style"}) + + def __init__( + self, + *, + usergroup_id: str, + style: Optional[Union[dict, "RichTextElementParts.TextStyle"]] = None, + **others: dict, + ): + super().__init__(type=self.type) + show_unknown_key_warning(self, others) + self.usergroup_id = usergroup_id + self.style = style + + class Date(RichTextElement): + type = "date" + + @property + def attributes(self) -> Set[str]: # type: ignore[override] + return super().attributes.union({"timestamp", "format", "url", "fallback"}) + + def __init__( + self, + *, + timestamp: int, + format: str, + url: Optional[str] = None, + fallback: Optional[str] = None, + **others: dict, + ): + super().__init__(type=self.type) + show_unknown_key_warning(self, others) + self.timestamp = timestamp + self.format = format + self.url = url + self.fallback = fallback + + class Broadcast(RichTextElement): + type = "broadcast" + + @property + def attributes(self) -> Set[str]: # type: ignore[override] + return super().attributes.union({"range"}) + + def __init__( + self, + *, + range: str, # channel, here, .. + **others: dict, + ): + super().__init__(type=self.type) + show_unknown_key_warning(self, others) + self.range = range + + class Color(RichTextElement): + type = "color" + + @property + def attributes(self) -> Set[str]: # type: ignore[override] + return super().attributes.union({"value"}) + + def __init__( + self, + *, + value: str, + **others: dict, + ): + super().__init__(type=self.type) + show_unknown_key_warning(self, others) + self.value = value diff --git a/slack_sdk/models/blocks/blocks.py b/slack_sdk/models/blocks/blocks.py new file mode 100644 index 000000000..cac463c99 --- /dev/null +++ b/slack_sdk/models/blocks/blocks.py @@ -0,0 +1,779 @@ +import copy +import logging +import warnings +from typing import Any, Dict, List, Optional, Sequence, Set, Union + +from slack_sdk.models import show_unknown_key_warning +from slack_sdk.models.basic_objects import JsonObject, JsonValidator + +from ...errors import SlackObjectFormationError +from .basic_components import MarkdownTextObject, PlainTextObject, SlackFile, TextObject +from .block_elements import ( + BlockElement, + FeedbackButtonsElement, + IconButtonElement, + ImageElement, + InputInteractiveElement, + InteractiveElement, + RichTextElement, +) + +# ------------------------------------------------- +# Base Classes +# ------------------------------------------------- + + +class Block(JsonObject): + """Blocks are a series of components that can be combined + to create visually rich and compellingly interactive messages. + https://docs.slack.dev/reference/block-kit/blocks + """ + + attributes = {"block_id", "type"} + block_id_max_length = 255 + logger = logging.getLogger(__name__) + + def _subtype_warning(self): + warnings.warn( + "subtype is deprecated since slackclient 2.6.0, use type instead", + DeprecationWarning, + ) + + @property + def subtype(self) -> Optional[str]: + return self.type + + def __init__( + self, + *, + type: Optional[str] = None, + subtype: Optional[str] = None, # deprecated + block_id: Optional[str] = None, + ): + if subtype: + self._subtype_warning() + self.type = type if type else subtype + self.block_id = block_id + self.color = None + + @JsonValidator(f"block_id cannot exceed {block_id_max_length} characters") + def _validate_block_id_length(self): + return self.block_id is None or len(self.block_id) <= self.block_id_max_length + + @classmethod + def parse(cls, block: Union[dict, "Block"]) -> Optional["Block"]: + if block is None: + return None + elif isinstance(block, Block): + return block + else: + if "type" in block: + type = block["type"] + if type == SectionBlock.type: + return SectionBlock(**block) + elif type == DividerBlock.type: + return DividerBlock(**block) + elif type == ImageBlock.type: + return ImageBlock(**block) + elif type == ActionsBlock.type: + return ActionsBlock(**block) + elif type == ContextBlock.type: + return ContextBlock(**block) + elif type == ContextActionsBlock.type: + return ContextActionsBlock(**block) + elif type == InputBlock.type: + return InputBlock(**block) + elif type == FileBlock.type: + return FileBlock(**block) + elif type == CallBlock.type: + return CallBlock(**block) + elif type == HeaderBlock.type: + return HeaderBlock(**block) + elif type == MarkdownBlock.type: + return MarkdownBlock(**block) + elif type == VideoBlock.type: + return VideoBlock(**block) + elif type == RichTextBlock.type: + return RichTextBlock(**block) + elif type == TableBlock.type: + return TableBlock(**block) + else: + cls.logger.warning(f"Unknown block detected and skipped ({block})") + return None + else: + cls.logger.warning(f"Unknown block detected and skipped ({block})") + return None + + @classmethod + def parse_all(cls, blocks: Optional[Sequence[Union[dict, "Block"]]]) -> List["Block"]: + return [cls.parse(b) for b in blocks or []] # type: ignore[misc] + + +# ------------------------------------------------- +# Block Classes +# ------------------------------------------------- + + +class SectionBlock(Block): + type = "section" + fields_max_length = 10 + text_max_length = 3000 + + @property + def attributes(self) -> Set[str]: # type: ignore[override] + return super().attributes.union({"text", "fields", "accessory", "expand"}) + + def __init__( + self, + *, + block_id: Optional[str] = None, + text: Optional[Union[str, dict, TextObject]] = None, + fields: Optional[Sequence[Union[str, dict, TextObject]]] = None, + accessory: Optional[Union[dict, BlockElement]] = None, + expand: Optional[bool] = None, + **others: dict, + ): + """A section is one of the most flexible blocks available. + https://docs.slack.dev/reference/block-kit/blocks/section-block + + Args: + block_id (required): A string acting as a unique identifier for a block. + If not specified, one will be generated. + You can use this block_id when you receive an interaction payload to identify the source of the action. + Maximum length for this field is 255 characters. + block_id should be unique for each message and each iteration of a message. + If a message is updated, use a new block_id. + text (preferred): The text for the block, in the form of a text object. + Maximum length for the text in this field is 3000 characters. + This field is not required if a valid array of fields objects is provided instead. + fields (required if no text is provided): Required if no text is provided. + An array of text objects. Any text objects included with fields will be rendered + in a compact format that allows for 2 columns of side-by-side text. + Maximum number of items is 10. Maximum length for the text in each item is 2000 characters. + accessory: One of the available element objects. + expand: Whether or not this section block's text should always expand when rendered. + If false or not provided, it may be rendered with a 'see more' option to expand and show the full text. + For AI Assistant apps, this allows the app to post long messages without users needing + to click 'see more' to expand the message. + """ + super().__init__(type=self.type, block_id=block_id) + show_unknown_key_warning(self, others) + + self.text = TextObject.parse(text) # type: ignore[arg-type] + field_objects = [] + for f in fields or []: + if isinstance(f, str): + field_objects.append(MarkdownTextObject.from_str(f)) + elif isinstance(f, TextObject): + field_objects.append(f) # type: ignore[arg-type] + elif isinstance(f, dict) and "type" in f: + d = copy.copy(f) + t = d.pop("type") + if t == MarkdownTextObject.type: + field_objects.append(MarkdownTextObject(**d)) + else: + field_objects.append(PlainTextObject(**d)) # type: ignore[arg-type] + else: + self.logger.warning(f"Unsupported filed detected and skipped {f}") + self.fields = field_objects + self.accessory = BlockElement.parse(accessory) # type: ignore[arg-type] + self.expand = expand + + @JsonValidator("text or fields attribute must be specified") + def _validate_text_or_fields_populated(self): + return self.text is not None or self.fields + + @JsonValidator(f"fields attribute cannot exceed {fields_max_length} items") + def _validate_fields_length(self): + return self.fields is None or len(self.fields) <= self.fields_max_length + + @JsonValidator(f"text attribute cannot exceed {text_max_length} characters") + def _validate_alt_text_length(self): + return self.text is None or len(self.text.text) <= self.text_max_length + + +class DividerBlock(Block): + type = "divider" + + def __init__( + self, + *, + block_id: Optional[str] = None, + **others: dict, + ): + """A content divider, like an
, to split up different blocks inside of a message. + https://docs.slack.dev/reference/block-kit/blocks/divider-block + + Args: + block_id: A string acting as a unique identifier for a block. If not specified, one will be generated. + You can use this block_id when you receive an interaction payload to identify the source of the action. + Maximum length for this field is 255 characters. + block_id should be unique for each message and each iteration of a message. + If a message is updated, use a new block_id. + """ + super().__init__(type=self.type, block_id=block_id) + show_unknown_key_warning(self, others) + + +class ImageBlock(Block): + type = "image" + + @property + def attributes(self) -> Set[str]: # type: ignore[override] + return super().attributes.union({"alt_text", "image_url", "title", "slack_file"}) + + image_url_max_length = 3000 + alt_text_max_length = 2000 + title_max_length = 2000 + + def __init__( + self, + *, + alt_text: str, + image_url: Optional[str] = None, + slack_file: Optional[Union[Dict[str, Any], SlackFile]] = None, + title: Optional[Union[str, dict, PlainTextObject]] = None, + block_id: Optional[str] = None, + **others: dict, + ): + """A simple image block, designed to make those cat photos really pop. + https://docs.slack.dev/reference/block-kit/blocks/image-block + + Args: + alt_text (required): A plain-text summary of the image. This should not contain any markup. + Maximum length for this field is 2000 characters. + image_url: The URL of the image to be displayed. + Maximum length for this field is 3000 characters. + slack_file: A Slack image file object that defines the source of the image. + title: An optional title for the image in the form of a text object that can only be of type: plain_text. + Maximum length for the text in this field is 2000 characters. + block_id: A string acting as a unique identifier for a block. If not specified, one will be generated. + Maximum length for this field is 255 characters. + block_id should be unique for each message and each iteration of a message. + If a message is updated, use a new block_id. + """ + super().__init__(type=self.type, block_id=block_id) + show_unknown_key_warning(self, others) + + self.image_url = image_url + self.alt_text = alt_text + parsed_title = None + if title is not None: + if isinstance(title, str): + parsed_title = PlainTextObject(text=title) + elif isinstance(title, dict): + if title.get("type") != PlainTextObject.type: + raise SlackObjectFormationError(f"Unsupported type for title in an image block: {title.get('type')}") + parsed_title = PlainTextObject(text=title.get("text"), emoji=title.get("emoji")) # type: ignore[arg-type] + elif isinstance(title, PlainTextObject): + parsed_title = title + else: + raise SlackObjectFormationError(f"Unsupported type for title in an image block: {type(title)}") + if slack_file is not None: + self.slack_file = ( + slack_file if slack_file is None or isinstance(slack_file, SlackFile) else SlackFile(**slack_file) + ) + self.title = parsed_title + + @JsonValidator(f"image_url attribute cannot exceed {image_url_max_length} characters") + def _validate_image_url_length(self): + return self.image_url is None or len(self.image_url) <= self.image_url_max_length + + @JsonValidator(f"alt_text attribute cannot exceed {alt_text_max_length} characters") + def _validate_alt_text_length(self): + return len(self.alt_text) <= self.alt_text_max_length + + @JsonValidator(f"title attribute cannot exceed {title_max_length} characters") + def _validate_title_length(self): + return self.title is None or self.title.text is None or len(self.title.text) <= self.title_max_length + + +class ActionsBlock(Block): + type = "actions" + elements_max_length = 25 + + @property + def attributes(self) -> Set[str]: # type: ignore[override] + return super().attributes.union({"elements"}) + + def __init__( + self, + *, + elements: Sequence[Union[dict, InteractiveElement]], + block_id: Optional[str] = None, + **others: dict, + ): + """A block that is used to hold interactive elements. + https://docs.slack.dev/reference/block-kit/blocks/actions-block + + Args: + elements (required): An array of interactive element objects - buttons, select menus, overflow menus, + or date pickers. There is a maximum of 25 elements in each action block. + block_id: A string acting as a unique identifier for a block. + If not specified, a block_id will be generated. + You can use this block_id when you receive an interaction payload to identify the source of the action. + Maximum length for this field is 255 characters. + block_id should be unique for each message and each iteration of a message. + If a message is updated, use a new block_id. + """ + super().__init__(type=self.type, block_id=block_id) + show_unknown_key_warning(self, others) + + self.elements = BlockElement.parse_all(elements) + + @JsonValidator(f"elements attribute cannot exceed {elements_max_length} elements") + def _validate_elements_length(self): + return self.elements is None or len(self.elements) <= self.elements_max_length + + +class ContextBlock(Block): + type = "context" + elements_max_length = 10 + + @property + def attributes(self) -> Set[str]: # type: ignore[override] + return super().attributes.union({"elements"}) + + def __init__( + self, + *, + elements: Sequence[Union[dict, ImageElement, TextObject]], + block_id: Optional[str] = None, + **others: dict, + ): + """Displays message context, which can include both images and text. + https://docs.slack.dev/reference/block-kit/blocks/context-block + + Args: + elements (required): An array of image elements and text objects. Maximum number of items is 10. + block_id: A string acting as a unique identifier for a block. If not specified, one will be generated. + Maximum length for this field is 255 characters. + block_id should be unique for each message and each iteration of a message. + If a message is updated, use a new block_id. + """ + super().__init__(type=self.type, block_id=block_id) + show_unknown_key_warning(self, others) + + self.elements = BlockElement.parse_all(elements) + + @JsonValidator(f"elements attribute cannot exceed {elements_max_length} elements") + def _validate_elements_length(self): + return self.elements is None or len(self.elements) <= self.elements_max_length + + +class ContextActionsBlock(Block): + type = "context_actions" + elements_max_length = 5 + + @property + def attributes(self) -> Set[str]: # type: ignore[override] + return super().attributes.union({"elements"}) + + def __init__( + self, + *, + elements: Sequence[Union[dict, FeedbackButtonsElement, IconButtonElement]], + block_id: Optional[str] = None, + **others: dict, + ): + """Displays actions as contextual info, which can include both feedback buttons and icon buttons. + https://docs.slack.dev/reference/block-kit/blocks/context-actions-block + + Args: + elements (required): An array of feedback_buttons or icon_button block elements. Maximum number of items is 5. + block_id: A string acting as a unique identifier for a block. If not specified, one will be generated. + Maximum length for this field is 255 characters. + block_id should be unique for each message and each iteration of a message. + If a message is updated, use a new block_id. + """ + super().__init__(type=self.type, block_id=block_id) + show_unknown_key_warning(self, others) + + self.elements = BlockElement.parse_all(elements) + + @JsonValidator("elements attribute must be specified") + def _validate_elements(self): + return self.elements is None or len(self.elements) > 0 + + @JsonValidator(f"elements attribute cannot exceed {elements_max_length} elements") + def _validate_elements_length(self): + return self.elements is None or len(self.elements) <= self.elements_max_length + + +class InputBlock(Block): + type = "input" + label_max_length = 2000 + hint_max_length = 2000 + + @property + def attributes(self) -> Set[str]: # type: ignore[override] + return super().attributes.union({"label", "hint", "element", "optional", "dispatch_action"}) + + def __init__( + self, + *, + label: Union[str, dict, PlainTextObject], + element: Union[str, dict, InputInteractiveElement], + block_id: Optional[str] = None, + hint: Optional[Union[str, dict, PlainTextObject]] = None, + dispatch_action: Optional[bool] = None, + optional: Optional[bool] = None, + **others: dict, + ): + """A block that collects information from users - it can hold a plain-text input element, + a select menu element, a multi-select menu element, or a datepicker. + https://docs.slack.dev/reference/block-kit/blocks/input-block + + Args: + label (required): A label that appears above an input element in the form of a text object + that must have type of plain_text. Maximum length for the text in this field is 2000 characters. + element (required): An plain-text input element, a checkbox element, a radio button element, + a select menu element, a multi-select menu element, or a datepicker. + block_id: A string acting as a unique identifier for a block. If not specified, one will be generated. + Maximum length for this field is 255 characters. + block_id should be unique for each message or view and each iteration of a message or view. + If a message or view is updated, use a new block_id. + hint: An optional hint that appears below an input element in a lighter grey. + It must be a text object with a type of plain_text. + Maximum length for the text in this field is 2000 characters. + dispatch_action: A boolean that indicates whether or not the use of elements in this block + should dispatch a block_actions payload. Defaults to false. + optional: A boolean that indicates whether the input element may be empty when a user submits the modal. + Defaults to false. + """ + super().__init__(type=self.type, block_id=block_id) + show_unknown_key_warning(self, others) + + self.label = TextObject.parse(label, default_type=PlainTextObject.type) + self.element = BlockElement.parse(element) # type: ignore[arg-type] + self.hint = TextObject.parse(hint, default_type=PlainTextObject.type) # type: ignore[arg-type] + self.dispatch_action = dispatch_action + self.optional = optional + + @JsonValidator(f"label attribute cannot exceed {label_max_length} characters") + def _validate_label_length(self): + return self.label is None or self.label.text is None or len(self.label.text) <= self.label_max_length + + @JsonValidator(f"hint attribute cannot exceed {hint_max_length} characters") + def _validate_hint_length(self): + return self.hint is None or self.hint.text is None or len(self.hint.text) <= self.label_max_length + + @JsonValidator( + ( + "element attribute must be a string, select element, multi-select element, " + "or a datepicker. (Sub-classes of InputInteractiveElement)" + ) + ) + def _validate_element_type(self): + return self.element is None or isinstance(self.element, (str, InputInteractiveElement)) + + +class FileBlock(Block): + type = "file" + + @property + def attributes(self) -> Set[str]: # type: ignore[override] + return super().attributes.union({"external_id", "source"}) + + def __init__( + self, + *, + external_id: str, + source: str = "remote", + block_id: Optional[str] = None, + **others: dict, + ): + """Displays a remote file. + https://docs.slack.dev/reference/block-kit/blocks/file-block + + Args: + external_id (required): The external unique ID for this file. + source (required): At the moment, source will always be remote for a remote file. + block_id: A string acting as a unique identifier for a block. If not specified, one will be generated. + Maximum length for this field is 255 characters. + block_id should be unique for each message and each iteration of a message. + If a message is updated, use a new block_id. + """ + super().__init__(type=self.type, block_id=block_id) + show_unknown_key_warning(self, others) + + self.external_id = external_id + self.source = source + + +class CallBlock(Block): + type = "call" + + @property + def attributes(self) -> Set[str]: # type: ignore[override] + 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://docs.slack.dev/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 + + +class HeaderBlock(Block): + type = "header" + text_max_length = 150 + + @property + def attributes(self) -> Set[str]: # type: ignore[override] + return super().attributes.union({"text"}) + + def __init__( + self, + *, + block_id: Optional[str] = None, + text: Optional[Union[str, dict, TextObject]] = None, + **others: dict, + ): + """A header is a plain-text block that displays in a larger, bold font. + https://docs.slack.dev/reference/block-kit/blocks/header-block + + Args: + block_id: A string acting as a unique identifier for a block. If not specified, one will be generated. + Maximum length for this field is 255 characters. + block_id should be unique for each message and each iteration of a message. + If a message is updated, use a new block_id. + text (required): The text for the block, in the form of a plain_text text object. + Maximum length for the text in this field is 150 characters. + """ + super().__init__(type=self.type, block_id=block_id) + show_unknown_key_warning(self, others) + + self.text = TextObject.parse(text, default_type=PlainTextObject.type) # type: ignore[arg-type] + + @JsonValidator("text attribute must be specified") + def _validate_text(self): + return self.text is not None + + @JsonValidator(f"text attribute cannot exceed {text_max_length} characters") + def _validate_alt_text_length(self): + return self.text is None or len(self.text.text) <= self.text_max_length + + +class MarkdownBlock(Block): + type = "markdown" + text_max_length = 12000 + + @property + def attributes(self) -> Set[str]: # type: ignore[override] + return super().attributes.union({"text"}) + + def __init__( + self, + *, + text: str, + block_id: Optional[str] = None, + **others: dict, + ): + """Displays formatted markdown. + https://docs.slack.dev/reference/block-kit/blocks/markdown-block/ + + Args: + block_id: A string acting as a unique identifier for a block. If not specified, one will be generated. + Maximum length for this field is 255 characters. + block_id should be unique for each message and each iteration of a message. + If a message is updated, use a new block_id. + text (required): The standard markdown-formatted text. Limit 12,000 characters max. + """ + super().__init__(type=self.type, block_id=block_id) + show_unknown_key_warning(self, others) + + self.text = text + + @JsonValidator("text attribute must be specified") + def _validate_text(self): + return self.text != "" + + @JsonValidator(f"text attribute cannot exceed {text_max_length} characters") + def _validate_alt_text_length(self): + return len(self.text) <= self.text_max_length + + +class VideoBlock(Block): + type = "video" + title_max_length = 200 + author_name_max_length = 50 + + @property + def attributes(self) -> Set[str]: # type: ignore[override] + return super().attributes.union( + { + "alt_text", + "video_url", + "thumbnail_url", + "title", + "title_url", + "description", + "provider_icon_url", + "provider_name", + "author_name", + } + ) + + def __init__( + self, + *, + block_id: Optional[str] = None, + alt_text: Optional[str] = None, + video_url: Optional[str] = None, + thumbnail_url: Optional[str] = None, + title: Optional[Union[str, dict, PlainTextObject]] = None, + title_url: Optional[str] = None, + description: Optional[Union[str, dict, PlainTextObject]] = None, + provider_icon_url: Optional[str] = None, + provider_name: Optional[str] = None, + author_name: Optional[str] = None, + **others: dict, + ): + """A video block is designed to embed videos in all app surfaces + (e.g. link unfurls, messages, modals, App Home) — + anywhere you can put blocks! To use the video block within your app, + you must have the links.embed:write scope. + https://docs.slack.dev/reference/block-kit/blocks/video-block + + Args: + block_id: A string acting as a unique identifier for a block. If not specified, one will be generated. + Maximum length for this field is 255 characters. + block_id should be unique for each message and each iteration of a message. + If a message is updated, use a new block_id. + alt_text (required): A tooltip for the video. Required for accessibility + video_url (required): The URL to be embedded. Must match any existing unfurl domains within the app + and point to a HTTPS URL. + thumbnail_url (required): The thumbnail image URL + title (required): Video title in plain text format. Must be less than 200 characters. + title_url: Hyperlink for the title text. Must correspond to the non-embeddable URL for the video. + Must go to an HTTPS URL. + description: Description for video in plain text format. + provider_icon_url: Icon for the video provider - ex. Youtube icon + provider_name: The originating application or domain of the video ex. Youtube + author_name: Author name to be displayed. Must be less than 50 characters. + """ + super().__init__(type=self.type, block_id=block_id) + show_unknown_key_warning(self, others) + + self.alt_text = alt_text + self.video_url = video_url + self.thumbnail_url = thumbnail_url + self.title = TextObject.parse(title, default_type=PlainTextObject.type) # type: ignore[arg-type] + self.title_url = title_url + self.description = TextObject.parse(description, default_type=PlainTextObject.type) # type: ignore[arg-type] + self.provider_icon_url = provider_icon_url + self.provider_name = provider_name + self.author_name = author_name + + @JsonValidator("alt_text attribute must be specified") + def _validate_alt_text(self): + return self.alt_text is not None + + @JsonValidator("video_url attribute must be specified") + def _validate_video_url(self): + return self.video_url is not None + + @JsonValidator("thumbnail_url attribute must be specified") + def _validate_thumbnail_url(self): + return self.thumbnail_url is not None + + @JsonValidator("title attribute must be specified") + def _validate_title(self): + return self.title is not None + + @JsonValidator(f"title attribute cannot exceed {title_max_length} characters") + def _validate_title_length(self): + return self.title is None or len(self.title.text) < self.title_max_length + + @JsonValidator(f"author_name attribute cannot exceed {author_name_max_length} characters") + def _validate_author_name_length(self): + return self.author_name is None or len(self.author_name) < self.author_name_max_length + + +class RichTextBlock(Block): + type = "rich_text" + + @property + def attributes(self) -> Set[str]: # type: ignore[override] + return super().attributes.union({"elements"}) + + def __init__( + self, + *, + elements: Sequence[Union[dict, RichTextElement]], + block_id: Optional[str] = None, + **others: dict, + ): + """A block that is used to hold interactive elements. + https://docs.slack.dev/reference/block-kit/blocks/rich-text-block + + Args: + elements (required): An array of rich text objects - + rich_text_section, rich_text_list, rich_text_quote, rich_text_preformatted + block_id: A unique identifier for a block. If not specified, one will be generated. + Maximum length for this field is 255 characters. + block_id should be unique for each message or view and each iteration of a message or view. + If a message or view is updated, use a new block_id. + """ + super().__init__(type=self.type, block_id=block_id) + show_unknown_key_warning(self, others) + + self.elements = BlockElement.parse_all(elements) + + +class TableBlock(Block): + type = "table" + + @property + def attributes(self) -> Set[str]: # type: ignore[override] + return super().attributes.union({"rows", "column_settings"}) + + def __init__( + self, + *, + rows: Sequence[Sequence[Dict[str, Any]]], + column_settings: Optional[Sequence[Optional[Dict[str, Any]]]] = None, + block_id: Optional[str] = None, + **others: dict, + ): + """Displays structured information in a table. + https://docs.slack.dev/reference/block-kit/blocks/table-block + + Args: + rows (required): An array consisting of table rows. Maximum 100 rows. + Each row object is an array with a max of 20 table cells. + Table cells can have a type of raw_text or rich_text. + column_settings: An array describing column behavior. If there are fewer items in the column_settings array + than there are columns in the table, then the items in the the column_settings array will describe + the same number of columns in the table as there are in the array itself. + Any additional columns will have the default behavior. Maximum 20 items. + See below for column settings schema. + block_id: A unique identifier for a block. If not specified, a block_id will be generated. + You can use this block_id when you receive an interaction payload to identify the source of the action. + Maximum length for this field is 255 characters. + block_id should be unique for each message and each iteration of a message. + If a message is updated, use a new block_id. + """ + super().__init__(type=self.type, block_id=block_id) + show_unknown_key_warning(self, others) + + self.rows = rows + self.column_settings = column_settings + + @JsonValidator("rows attribute must be specified") + def _validate_rows(self): + return self.rows is not None and len(self.rows) > 0 diff --git a/slack_sdk/models/dialoags.py b/slack_sdk/models/dialoags.py new file mode 100644 index 000000000..1adf7b504 --- /dev/null +++ b/slack_sdk/models/dialoags.py @@ -0,0 +1,29 @@ +from slack_sdk.models.dialogs import AbstractDialogSelector +from slack_sdk.models.dialogs import DialogChannelSelector +from slack_sdk.models.dialogs import DialogConversationSelector +from slack_sdk.models.dialogs import DialogExternalSelector +from slack_sdk.models.dialogs import DialogStaticSelector +from slack_sdk.models.dialogs import DialogTextArea +from slack_sdk.models.dialogs import DialogTextComponent +from slack_sdk.models.dialogs import DialogTextField +from slack_sdk.models.dialogs import DialogUserSelector +from slack_sdk.models.dialogs import TextElementSubtypes +from slack_sdk.models.dialogs import DialogBuilder + +from slack import deprecation + +deprecation.show_message(__name__, "slack_sdk.models.dialogs") + +__all__ = [ + "AbstractDialogSelector", + "DialogChannelSelector", + "DialogConversationSelector", + "DialogExternalSelector", + "DialogStaticSelector", + "DialogTextArea", + "DialogTextComponent", + "DialogTextField", + "DialogUserSelector", + "TextElementSubtypes", + "DialogBuilder", +] diff --git a/slack_sdk/models/dialogs/__init__.py b/slack_sdk/models/dialogs/__init__.py new file mode 100644 index 000000000..cc67ed37e --- /dev/null +++ b/slack_sdk/models/dialogs/__init__.py @@ -0,0 +1,921 @@ +from abc import ABCMeta, abstractmethod +from json import dumps +from typing import List, Optional, Union, Set, Sequence + +from slack_sdk.models import extract_json +from slack_sdk.models.attachments import AbstractActionSelector +from slack_sdk.models.basic_objects import EnumValidator, JsonObject, JsonValidator +from slack_sdk.models.blocks import Option, OptionGroup, DynamicSelectElementTypes + +TextElementSubtypes = {"email", "number", "tel", "url"} + + +class DialogTextComponent(JsonObject, metaclass=ABCMeta): + attributes = { + "hint", + "label", + "max_length", + "min_length", + "name", + "optional", + "placeholder", + "subtype", + "type", + "value", + } + + name_max_length = 300 + label_max_length = 48 + placeholder_max_length = 150 + hint_max_length = 150 + + @property + @abstractmethod + def type(self): + pass + + @property + @abstractmethod + def max_value_length(self): + pass + + def __init__( + self, + *, + name: str, + label: str, + optional: bool = False, + placeholder: Optional[str] = None, + hint: Optional[str] = None, + value: Optional[str] = None, + min_length: int = 0, + max_length: Optional[int] = None, + subtype: Optional[str] = None, + ): + self.name = name + self.label = label + self.optional = optional + self.placeholder = placeholder + self.hint = hint + self.value = value + self.min_length = min_length + self.max_length = max_length or self.max_value_length + self.subtype = subtype + + @JsonValidator(f"name attribute cannot exceed {name_max_length} characters") + def name_length(self) -> bool: + return len(self.name) < self.name_max_length + + @JsonValidator(f"label attribute cannot exceed {label_max_length} characters") + def label_length(self) -> bool: + return len(self.label) < self.label_max_length + + @JsonValidator(f"placeholder attribute cannot exceed {placeholder_max_length} characters") + def placeholder_length(self) -> bool: + return self.placeholder is None or len(self.placeholder) < self.placeholder_max_length + + @JsonValidator(f"hint attribute cannot exceed {hint_max_length} characters") + def hint_length(self) -> bool: + return self.hint is None or len(self.hint) < self.hint_max_length + + @JsonValidator("value attribute exceeded bounds") + def value_length(self) -> bool: + return self.value is None or len(self.value) < self.max_value_length + + @JsonValidator("min_length attribute must be greater than or equal to 0") + def min_length_above_zero(self) -> bool: + return self.min_length is None or self.min_length >= 0 + + @JsonValidator("min_length attribute exceed bounds") + def min_length_length(self) -> bool: + return self.min_length is None or self.min_length <= self.max_value_length + + @JsonValidator("min_length attribute must be less than max value attribute") + def min_length_below_max_length(self) -> bool: + return self.min_length is None or self.min_length < self.max_length + + @JsonValidator("max_length attribute must be greater than or equal to 0") + def max_length_above_zero(self) -> bool: + return self.max_length is None or self.max_length > 0 + + @JsonValidator("max_length attribute exceeded bounds") + def max_length_length(self) -> bool: + return self.max_length is None or self.max_length <= self.max_value_length + + @EnumValidator("subtype", TextElementSubtypes) + def subtype_valid(self) -> bool: + return self.subtype is None or self.subtype in TextElementSubtypes + + +class DialogTextField(DialogTextComponent): + """ + Text elements are single-line plain text fields. + + https://docs.slack.dev/legacy/legacy-dialogs/#text_elements + """ + + type = "text" + max_value_length = 150 + + +class DialogTextArea(DialogTextComponent): + """ + A textarea is a multi-line plain text editing control. You've likely encountered + these on the world wide web. Use this element if you want a relatively long + answer from users. The element UI provides a remaining character count to the + max_length you have set or the default, 3000. + + https://docs.slack.dev/legacy/legacy-dialogs/#textarea_elements + """ + + type = "textarea" + max_value_length = 3000 + + +class AbstractDialogSelector(JsonObject, metaclass=ABCMeta): + DataSourceTypes = DynamicSelectElementTypes.union({"external", "static"}) + + attributes = {"data_source", "label", "name", "optional", "placeholder", "type"} + + name_max_length = 300 + label_max_length = 48 + placeholder_max_length = 150 + + @property + @abstractmethod + def data_source(self) -> str: + pass + + def __init__( + self, + *, + name: str, + label: str, + optional: bool = False, + value: Optional[Union[Option, str]] = None, + placeholder: Optional[str] = None, + ): + self.name = name + self.label = label + self.optional = optional + self.value = value + self.placeholder = placeholder + self.type = "select" + + @JsonValidator(f"name attribute cannot exceed {name_max_length} characters") + def name_length(self) -> bool: + return len(self.name) < self.name_max_length + + @JsonValidator(f"label attribute cannot exceed {label_max_length} characters") + def label_length(self) -> bool: + return len(self.label) < self.label_max_length + + @JsonValidator(f"placeholder attribute cannot exceed {placeholder_max_length} characters") + def placeholder_length(self) -> bool: + return self.placeholder is None or len(self.placeholder) < self.placeholder_max_length + + @EnumValidator("data_source", DataSourceTypes) + def data_source_valid(self) -> bool: + return self.data_source in self.DataSourceTypes + + def to_dict(self) -> dict: + json = super().to_dict() + if self.data_source == "external": + if isinstance(self.value, Option): + json["selected_options"] = extract_json([self.value], "dialog") + elif self.value is not None: + json["selected_options"] = Option.from_single_value(self.value) + else: + if isinstance(self.value, Option): + json["value"] = self.value.value + elif self.value is not None: + json["value"] = self.value + return json + + +class DialogStaticSelector(AbstractDialogSelector): + """ + Use the select element for multiple choice selections allowing users to pick a + single item from a list. True to web roots, this selection is displayed as a + dropdown menu. + + https://docs.slack.dev/legacy/legacy-dialogs/#select_elements + """ + + data_source = "static" + + options_max_length = 100 + + def __init__( + self, + *, + name: str, + label: str, + options: Union[Sequence[Option], Sequence[OptionGroup]], + optional: bool = False, + value: Optional[Union[Option, str]] = None, + placeholder: Optional[str] = None, + ): + """ + Use the select element for multiple choice selections allowing users to pick + a single item from a list. True to web roots, this selection is displayed as + a dropdown menu. + + A select element may contain up to 100 selections, provided as a list of + Option or OptionGroup objects + + https://docs.slack.dev/legacy/legacy-dialogs/#attributes_select_elements + + Args: + name: Name of form element. Required. No more than 300 characters. + label: Label displayed to user. Required. No more than 48 characters. + options: A list of up to 100 Option or OptionGroup objects. Object + types cannot be mixed. + optional: Provide true when the form element is not required. By + default, form elements are required. + value: Provide a default selected value. + placeholder: A string displayed as needed to help guide users in + completing the element. 150 character maximum. + """ + super().__init__( + name=name, + label=label, + optional=optional, + value=value, + placeholder=placeholder, + ) + self.options = options + + @JsonValidator(f"options attribute cannot exceed {options_max_length} items") + def options_length(self) -> bool: + return len(self.options) < self.options_max_length + + def to_dict(self) -> dict: + json = super().to_dict() + if isinstance(self.options[0], OptionGroup): + json["option_groups"] = extract_json(self.options, "dialog") + else: + json["options"] = extract_json(self.options, "dialog") + return json + + +class DialogUserSelector(AbstractDialogSelector): + data_source = "users" + + def __init__( + self, + *, + name: str, + label: str, + optional: bool = False, + value: Optional[str] = None, + placeholder: Optional[str] = None, + ): + """ + Now you can easily populate a select menu with a list of users. For example, + when you are creating a bug tracking app, you want to include a field for an + assignee. Slack pre-populates the user list in client-side, so your app + doesn't need access to a related OAuth scope. + + https://docs.slack.dev/legacy/legacy-dialogs/#dynamic_select_elements_users + + Args: + name: Name of form element. Required. No more than 300 characters. + label: Label displayed to user. Required. No more than 48 characters. + optional: Provide true when the form element is not required. By + default, form elements are required. + value: Provide a default selected value. + placeholder: A string displayed as needed to help guide users in + completing the element. 150 character maximum. + """ + super().__init__( + name=name, + label=label, + optional=optional, + value=value, + placeholder=placeholder, + ) + + +class DialogChannelSelector(AbstractDialogSelector): + data_source = "channels" + + def __init__( + self, + *, + name: str, + label: str, + optional: bool = False, + value: Optional[str] = None, + placeholder: Optional[str] = None, + ): + """ + You can also provide a select menu with a list of channels. Specify your + data_source as channels to limit only to public channels + + https://docs.slack.dev/legacy/legacy-dialogs/#dynamic_select_elements_channels_conversations + + Args: + name: Name of form element. Required. No more than 300 characters. + label: Label displayed to user. Required. No more than 48 characters. + optional: Provide true when the form element is not required. By + default, form elements are required. + value: Provide a default selected value. + placeholder: A string displayed as needed to help guide users in + completing the element. 150 character maximum. + """ + super().__init__( + name=name, + label=label, + optional=optional, + value=value, + placeholder=placeholder, + ) + + +class DialogConversationSelector(AbstractDialogSelector): + data_source = "conversations" + + def __init__( + self, + *, + name: str, + label: str, + optional: bool = False, + value: Optional[str] = None, + placeholder: Optional[str] = None, + ): + """ + You can also provide a select menu with a list of conversations - including + private channels, direct messages, MPIMs, and whatever else we consider a + conversation-like thing. + + https://docs.slack.dev/legacy/legacy-dialogs/#dynamic_select_elements_channels_conversations + + Args: + name: Name of form element. Required. No more than 300 characters. + label: Label displayed to user. Required. No more than 48 characters. + optional: Provide true when the form element is not required. By + default, form elements are required. + value: Provide a default selected value. + placeholder: A string displayed as needed to help guide users in + completing the element. 150 character maximum. + """ + super().__init__( + name=name, + label=label, + optional=optional, + value=value, + placeholder=placeholder, + ) + + +class DialogExternalSelector(AbstractDialogSelector): + data_source = "external" + + @property + def attributes(self) -> Set[str]: # type: ignore[override] + return super().attributes.union({"min_query_length"}) + + def __init__( + self, + *, + name: str, + label: str, + value: Optional[Option] = None, + min_query_length: Optional[int] = None, + optional: Optional[bool] = False, + placeholder: Optional[str] = None, + ): + """ + Use the select element for multiple choice selections allowing users to pick + a single item from a list. True to web roots, this selection is displayed as + a dropdown menu. + + A list of options can be loaded from an external URL and used in your dialog + menus. + + https://docs.slack.dev/legacy/legacy-dialogs/#dynamic_select_elements_external + + Args: + name: Name of form element. Required. No more than 300 characters. + label: Label displayed to user. Required. No more than 48 characters. + min_query_length: Specify the number of characters that must be typed + by a user into a dynamic select menu before dispatching to the app. + optional: Provide true when the form element is not required. By + default, form elements are required. + value: Provide a default selected value. This should be a single + Option or OptionGroup that exactly matches one that will be returned + from your external endpoint. + placeholder: A string displayed as needed to help guide users in + completing the element. 150 character maximum. + """ + super().__init__( + name=name, + label=label, + value=value, + optional=optional, # type: ignore[arg-type] + placeholder=placeholder, + ) + self.min_query_length = min_query_length + + +class DialogBuilder(JsonObject): + attributes: Set[str] = set() + + _callback_id: Optional[str] + _elements: List[Union[DialogTextComponent, AbstractDialogSelector]] + _submit_label: Optional[str] + _notify_on_cancel: bool + _state: Optional[str] + + title_max_length = 24 + submit_label_max_length = 24 + elements_max_length = 10 + state_max_length = 3000 + + def __init__(self): + """ + Create a DialogBuilder to more easily construct the JSON required to submit a + dialog to Slack + """ + self._title = None + self._callback_id = None + self._elements = [] + self._submit_label = None + self._notify_on_cancel = False + self._state = None + + def title(self, title: str) -> "DialogBuilder": + """ + Specify a title for this dialog + + Args: + title: must not exceed 24 characters + """ + self._title = title + return self + + def state(self, state: Union[dict, str]) -> "DialogBuilder": + """ + Pass state into this dialog - dictionaries will be automatically formatted to + JSON + + Args: + state: Extra state information that you need to pass from this dialog + back to your application on submission + """ + if isinstance(state, dict): + self._state = dumps(state) + else: + self._state = state + return self + + def callback_id(self, callback_id: str) -> "DialogBuilder": + """ + Specify a callback ID for this dialog, which your application will then + receive upon dialog submission + + Args: + callback_id: a string identifying this particular dialog + """ + self._callback_id = callback_id + return self + + def submit_label(self, label: str) -> "DialogBuilder": + """ + The label to use on the 'Submit' button on the dialog. Defaults to 'Submit' + if not specified. + + Args: + label: must not exceed 24 characters, and must be a single word (no + spaces) + """ + self._submit_label = label + return self + + def notify_on_cancel(self, notify: bool) -> "DialogBuilder": + """ + Whether this dialog should send a request to your application even if the + user cancels their interaction. Defaults to False. + + Args: + notify: Set to True to indicate that your application should receive a + request even if the user cancels interaction with the dialog. + """ + self._notify_on_cancel = notify + return self + + def text_field( + self, + *, + name: str, + label: str, + optional: bool = False, + placeholder: Optional[str] = None, + hint: Optional[str] = None, + value: Optional[str] = None, + min_length: int = 0, + max_length: int = 150, + subtype: Optional[str] = None, + ) -> "DialogBuilder": + """ + Text elements are single-line plain text fields. + + https://docs.slack.dev/legacy/legacy-dialogs/#attributes_text_elements + + Args: + name: Name of form element. Required. No more than 300 characters. + label: Label displayed to user. Required. 48 character maximum. + optional: Provide true when the form element is not required. By + default, form elements are required. + placeholder: A string displayed as needed to help guide users in + completing the element. 150 character maximum. + hint: Helpful text provided to assist users in answering a question. + Up to 150 characters. + value: A default value for this field. Up to 150 characters. + min_length: Minimum input length allowed for element. Up to 150 + characters. Defaults to 0. + max_length: Maximum input length allowed for element. Up to 150 + characters. Defaults to 150. + subtype: A subtype for this text input. Accepts email, number, tel, + or url. In some form factors, optimized input is provided for this + subtype. + """ + self._elements.append( + DialogTextField( + name=name, + label=label, + optional=optional, + placeholder=placeholder, + hint=hint, + value=value, + min_length=min_length, + max_length=max_length, + subtype=subtype, + ) + ) + return self + + def text_area( + self, + *, + name: str, + label: str, + optional: bool = False, + placeholder: Optional[str] = None, + hint: Optional[str] = None, + value: Optional[str] = None, + min_length: int = 0, + max_length: int = 3000, + subtype: Optional[str] = None, + ) -> "DialogBuilder": + """ + A textarea is a multi-line plain text editing control. You've likely + encountered these on the world wide web. Use this element if you want a + relatively long answer from users. The element UI provides a remaining + character count to the max_length you have set or the default, + 3000. + + https://docs.slack.dev/legacy/legacy-dialogs/#attributes_textarea_elements + + Args: + name: Name of form element. Required. No more than 300 characters. + label: Label displayed to user. Required. 48 character maximum. + optional: Provide true when the form element is not required. By + default, form elements are required. + placeholder: A string displayed as needed to help guide users in + completing the element. 150 character maximum. + hint: Helpful text provided to assist users in answering a question. + Up to 150 characters. + value: A default value for this field. Up to 3000 characters. + min_length: Minimum input length allowed for element. 1-3000 + characters. Defaults to 0. + max_length: Maximum input length allowed for element. 0-3000 + characters. Defaults to 3000. + subtype: A subtype for this text input. Accepts email, number, tel, + or url. In some form factors, optimized input is provided for this + subtype. + """ + self._elements.append( + DialogTextArea( + name=name, + label=label, + optional=optional, + placeholder=placeholder, + hint=hint, + value=value, + min_length=min_length, + max_length=max_length, + subtype=subtype, + ) + ) + return self + + def static_selector( + self, + *, + name: str, + label: str, + options: Union[Sequence[Option], Sequence[OptionGroup]], + optional: bool = False, + value: Optional[str] = None, + placeholder: Optional[str] = None, + ) -> "DialogBuilder": + """ + Use the select element for multiple choice selections allowing users to pick + a single item from a list. True to web roots, this selection is displayed as + a dropdown menu. + + A select element may contain up to 100 selections, provided as a list of + Option or OptionGroup objects + + https://docs.slack.dev/legacy/legacy-dialogs/#attributes_select_elements + + Args: + name: Name of form element. Required. No more than 300 characters. + label: Label displayed to user. Required. No more than 48 characters. + options: A list of up to 100 Option or OptionGroup objects. Object + types cannot be mixed. + optional: Provide true when the form element is not required. By + default, form elements are required. + value: Provide a default selected value. + placeholder: A string displayed as needed to help guide users in + completing the element. 150 character maximum. + """ + self._elements.append( + DialogStaticSelector( + name=name, + label=label, + options=options, + optional=optional, + value=value, + placeholder=placeholder, + ) + ) + return self + + def external_selector( + self, + *, + name: str, + label: str, + optional: bool = False, + value: Optional[Option] = None, + placeholder: Optional[str] = None, + min_query_length: Optional[int] = None, + ) -> "DialogBuilder": + """ + Use the select element for multiple choice selections allowing users to pick + a single item from a list. True to web roots, this selection is displayed as + a dropdown menu. + + A list of options can be loaded from an external URL and used in your dialog + menus. + + https://docs.slack.dev/legacy/legacy-dialogs/#dynamic_select_elements_external + + Args: + name: Name of form element. Required. No more than 300 characters. + label: Label displayed to user. Required. No more than 48 characters. + min_query_length: Specify the number of characters that must be + typed by a user into a dynamic select menu before dispatching to your + application. + optional: Provide true when the form element is not required. By + default, form elements are required. + value: Provide a default selected value. This should be a single + Option or OptionGroup that exactly matches one that will be returned + from your external endpoint. + placeholder: A string displayed as needed to help guide users in + completing the element. 150 character maximum. + """ + self._elements.append( + DialogExternalSelector( + name=name, + label=label, + optional=optional, + value=value, + placeholder=placeholder, + min_query_length=min_query_length, + ) + ) + return self + + def user_selector( + self, + *, + name: str, + label: str, + optional: bool = False, + value: Optional[str] = None, + placeholder: Optional[str] = None, + ) -> "DialogBuilder": + """ + Now you can easily populate a select menu with a list of users. For example, + when you are creating a bug tracking app, you want to include a field for an + assignee. Slack pre-populates the user list in client-side, so your app + doesn't need access to a related OAuth scope. + + https://docs.slack.dev/legacy/legacy-dialogs/#dynamic_select_elements_users + + Args: + name: Name of form element. Required. No more than 300 characters. + label: Label displayed to user. Required. No more than 48 characters. + optional: Provide true when the form element is not required. By + default, form elements are required. + value: Provide a default selected value. + placeholder: A string displayed as needed to help guide users in + completing the element. 150 character maximum. + """ + self._elements.append( + DialogUserSelector( + name=name, + label=label, + optional=optional, + value=value, + placeholder=placeholder, + ) + ) + return self + + def channel_selector( + self, + *, + name: str, + label: str, + optional: bool = False, + value: Optional[str] = None, + placeholder: Optional[str] = None, + ) -> "DialogBuilder": + """ + You can also provide a select menu with a list of channels. Specify your + data_source as channels to limit only to public channels + + https://docs.slack.dev/legacy/legacy-dialogs/#dynamic_select_elements_channels_conversations + + Args: + name: Name of form element. Required. No more than 300 characters. + label: Label displayed to user. Required. No more than 48 characters. + optional: Provide true when the form element is not required. By + default, form elements are required. + value: Provide a default selected value. + placeholder: A string displayed as needed to help guide users in + completing the element. 150 character maximum. + """ + self._elements.append( + DialogChannelSelector( + name=name, + label=label, + optional=optional, + value=value, + placeholder=placeholder, + ) + ) + return self + + def conversation_selector( + self, + *, + name: str, + label: str, + optional: bool = False, + value: Optional[str] = None, + placeholder: Optional[str] = None, + ) -> "DialogBuilder": + """ + You can also provide a select menu with a list of conversations - including + private channels, direct messages, MPIMs, and whatever else we consider a + conversation-like thing. + + https://docs.slack.dev/legacy/legacy-dialogs/#dynamic_select_elements_channels_conversations + + Args: + name: Name of form element. Required. No more than 300 characters. + label: Label displayed to user. Required. No more than 48 characters. + optional: Provide true when the form element is not required. By + default, form elements are required. + value: Provide a default selected value. + placeholder: A string displayed as needed to help guide users in + completing the element. 150 character maximum. + """ + self._elements.append( + DialogConversationSelector( + name=name, + label=label, + optional=optional, + value=value, + placeholder=placeholder, + ) + ) + return self + + @JsonValidator("title attribute is required") + def title_present(self) -> bool: + return self._title is not None + + @JsonValidator(f"title attribute cannot exceed {title_max_length} characters") + def title_length(self) -> bool: + return self._title is not None and len(self._title) <= self.title_max_length + + @JsonValidator("callback_id attribute is required") + def callback_id_present(self) -> bool: + return self._callback_id is not None + + @JsonValidator(f"dialogs must contain between 1 and {elements_max_length} elements") + def elements_length(self) -> bool: + return 0 < len(self._elements) <= self.elements_max_length + + @JsonValidator(f"submit_label cannot exceed {submit_label_max_length} characters") + def submit_label_length(self) -> bool: + return self._submit_label is None or len(self._submit_label) <= self.submit_label_max_length + + @JsonValidator("submit_label can only be one word") + def submit_label_valid(self) -> bool: + return self._submit_label is None or " " not in self._submit_label + + @JsonValidator(f"state cannot exceed {state_max_length} characters") + def state_length(self) -> bool: + return not self._state or len(self._state) <= self.state_max_length + + def to_dict(self) -> dict: + self.validate_json() + json = { + "title": self._title, + "callback_id": self._callback_id, + "elements": extract_json(self._elements), + "notify_on_cancel": self._notify_on_cancel, + } + if self._submit_label is not None: + json["submit_label"] = self._submit_label + if self._state is not None: + json["state"] = self._state + return json + + +class ActionStaticSelector(AbstractActionSelector): + """ + Use the select element for multiple choice selections allowing users to pick a + single item from a list. True to web roots, this selection is displayed as a + dropdown menu. + + https://docs.slack.dev/legacy/legacy-dialogs/#select_elements + """ + + data_source = "static" + + options_max_length = 100 + + def __init__( + self, + *, + name: str, + text: str, + options: Sequence[Union[Option, OptionGroup]], + selected_option: Optional[Option] = None, + ): + """ + Help users make clear, concise decisions by providing a menu of options + within messages. + + https://docs.slack.dev/legacy/legacy-messaging/legacy-adding-menus-to-messages/ + + Args: + name: Name this specific action. The name will be returned to your + Action URL along with the message's callback_id when this action is + invoked. Use it to identify this particular response path. + text: The user-facing label for the message button or menu + representing this action. Cannot contain markup. + options: A list of no mre than 100 Option or OptionGroup objects + selected_option: An Option object to pre-select as the default + value. + """ + super().__init__(name=name, text=text, selected_option=selected_option) + self.options = options + + @JsonValidator(f"options attribute cannot exceed {options_max_length} items") + def options_length(self) -> bool: + return len(self.options) < self.options_max_length + + def to_dict(self) -> dict: + json = super().to_dict() + if isinstance(self.options[0], OptionGroup): + json["option_groups"] = extract_json(self.options, "action") + else: + json["options"] = extract_json(self.options, "action") + return json + + +__all__ = [ + "TextElementSubtypes", + "AbstractDialogSelector", + "DialogChannelSelector", + "DialogConversationSelector", + "DialogExternalSelector", + "DialogStaticSelector", + "DialogTextArea", + "DialogTextComponent", + "DialogTextField", + "DialogUserSelector", + "TextElementSubtypes", + "DialogBuilder", +] diff --git a/slack_sdk/models/messages/__init__.py b/slack_sdk/models/messages/__init__.py new file mode 100644 index 000000000..47d42ddc6 --- /dev/null +++ b/slack_sdk/models/messages/__init__.py @@ -0,0 +1,85 @@ +from datetime import datetime +from typing import Optional, Union + +from slack_sdk.models.basic_objects import BaseObject + + +class Link(BaseObject): + def __init__(self, *, url: str, text: str): + """Base class used to generate links in Slack's not-quite Markdown, not quite HTML syntax + https://docs.slack.dev/messaging/formatting-message-text/#linking_to_urls + """ + self.url = url + self.text = text + + def __str__(self): + if self.text: + separator = "|" + else: + separator = "" + return f"<{self.url}{separator}{self.text}>" + + +class DateLink(Link): + def __init__( + self, + *, + date: Union[datetime, int], + date_format: str, + fallback: str, + link: Optional[str] = None, + ): + """Text containing a date or time should display that date in the local timezone of the person seeing the text. + https://docs.slack.dev/messaging/formatting-message-text/#date-formatting + """ + if isinstance(date, datetime): + epoch = int(date.timestamp()) + else: + epoch = date + if link is not None: + link = f"^{link}" + else: + link = "" + super().__init__(url=f"!date^{epoch}^{date_format}{link}", text=fallback) + + +class ObjectLink(Link): + prefix_mapping = { + "C": "#", # channel + "G": "#", # group message + "U": "@", # user + "W": "@", # workspace user (enterprise) + "B": "@", # bot user + "S": "!subteam^", # user groups, originally known as subteams + } + + def __init__(self, *, object_id: str, text: str = ""): + """Convenience class to create links to specific object types + https://docs.slack.dev/messaging/formatting-message-text/#linking-channels + """ + prefix = self.prefix_mapping.get(object_id[0].upper(), "@") + super().__init__(url=f"{prefix}{object_id}", text=text) + + +class ChannelLink(Link): + def __init__(self): + """Represents an @channel link, which notifies everyone present in this channel. + https://docs.slack.dev/messaging/formatting-message-text/ + """ + super().__init__(url="!channel", text="channel") + + +class HereLink(Link): + def __init__(self): + """Represents an @here link, which notifies all online users of this channel. + https://docs.slack.dev/messaging/formatting-message-text/ + """ + super().__init__(url="!here", text="here") + + +class EveryoneLink(Link): + def __init__(self): + """Represents an @everyone link, which notifies all users of this workspace. + https://docs.slack.dev/messaging/formatting-message-text/ + """ + super().__init__(url="!everyone", text="everyone") diff --git a/slack_sdk/models/messages/message.py b/slack_sdk/models/messages/message.py new file mode 100644 index 000000000..d4744aae7 --- /dev/null +++ b/slack_sdk/models/messages/message.py @@ -0,0 +1,77 @@ +import logging +import os +import warnings +from typing import Optional, Sequence + +from slack_sdk.models import extract_json +from slack_sdk.models.attachments import Attachment +from slack_sdk.models.basic_objects import ( + JsonObject, + JsonValidator, +) +from slack_sdk.models.blocks import Block + +LOGGER = logging.getLogger(__name__) + +skip_warn = os.environ.get("SLACKCLIENT_SKIP_DEPRECATION") # for unit tests etc. +if not skip_warn: + message = "This class is no longer actively maintained. " "Please use a dict object for building message data instead." + warnings.warn(message) + + +class Message(JsonObject): + attributes = {"text"} + + attachments_max_length = 100 + + def __init__( + self, + *, + text: str, + attachments: Optional[Sequence[Attachment]] = None, + blocks: Optional[Sequence[Block]] = None, + markdown: bool = True, + ): + """ + Create a message. + + https://docs.slack.dev/messaging/#message-structure + + Args: + text: Plain or Slack Markdown-like text to display in the message. + attachments: A list of Attachment objects to display after the rest of + the message's content. More than 20 is not recommended, but the actual + limit is 100 + blocks: A list of Block objects to attach to this message. If + specified, the 'text' property is ignored (more specifically, it's used + as a fallback on clients that can't render blocks) + markdown: Whether to parse markdown into formatting such as + bold/italics, or leave text completely unmodified. + """ + self.text = text + self.attachments = attachments or [] + self.blocks = blocks or [] + self.markdown = markdown + + @JsonValidator(f"attachments attribute cannot exceed {attachments_max_length} items") + def attachments_length(self): + return self.attachments is None or len(self.attachments) <= self.attachments_max_length + + def to_dict(self) -> dict: + json = super().to_dict() + if len(self.text) > 40000: + LOGGER.error("Messages over 40,000 characters are automatically truncated by Slack") + # The following limitation used to be true in the past. + # As of Feb 2021, having both is recommended + # ----------------- + # if self.text and self.blocks: + # # Slack doesn't render the text property if there are blocks, so: + # LOGGER.info(q + # "text attribute is treated as fallback text if blocks are attached to " + # "a message - insert text as a new SectionBlock if you want it to be " + # "displayed " + # ) + json["attachments"] = extract_json(self.attachments) + json["blocks"] = extract_json(self.blocks) + json["mrkdwn"] = self.markdown + return json diff --git a/slack_sdk/models/metadata/__init__.py b/slack_sdk/models/metadata/__init__.py new file mode 100644 index 000000000..7e4918401 --- /dev/null +++ b/slack_sdk/models/metadata/__init__.py @@ -0,0 +1,1255 @@ +from typing import Dict, Any, Union, Optional, List +from slack_sdk.models.basic_objects import JsonObject, EnumValidator + + +class Metadata(JsonObject): + """Message metadata + + https://docs.slack.dev/messaging/message-metadata/ + """ + + attributes = { + "event_type", + "event_payload", + } + + def __init__( + self, + event_type: str, + event_payload: Dict[str, Any], + **kwargs, + ): + self.event_type = event_type + self.event_payload = event_payload + self.additional_attributes = kwargs + + def __str__(self): + return str(self.get_non_null_attributes()) + + def __repr__(self): + return self.__str__() + + +# +# Work object entity metadata +# https://docs.slack.dev/messaging/work-objects/ +# + + +"""Entity types""" +EntityType = { + "slack#/entities/task", + "slack#/entities/file", + "slack#/entities/item", + "slack#/entities/incident", + "slack#/entities/content_item", +} + + +"""Custom field types""" +CustomFieldType = { + "integer", + "string", + "array", + "boolean", + "slack#/types/date", + "slack#/types/timestamp", + "slack#/types/image", + "slack#/types/channel_id", + "slack#/types/user", + "slack#/types/entity_ref", + "slack#/types/link", + "slack#/types/email", +} + + +class ExternalRef(JsonObject): + """Reference (and optional type) used to identify an entity within the developer's system""" + + attributes = { + "id", + "type", + } + + def __init__( + self, + id: str, + type: Optional[str] = None, + **kwargs, + ): + self.id = id + self.type = type + self.additional_attributes = kwargs + + def __str__(self): + return str(self.get_non_null_attributes()) + + def __repr__(self): + return self.__str__() + + +class FileEntitySlackFile(JsonObject): + """Slack file reference for file entities""" + + attributes = { + "id", + "type", + } + + def __init__( + self, + id: str, + type: Optional[str] = None, + **kwargs, + ): + self.id = id + self.type = type + self.additional_attributes = kwargs + + def __str__(self): + return str(self.get_non_null_attributes()) + + def __repr__(self): + return self.__str__() + + +class EntityIconSlackFile(JsonObject): + """Slack file reference for entity icon""" + + attributes = { + "id", + "url", + } + + def __init__( + self, + id: Optional[str] = None, + url: Optional[str] = None, + **kwargs, + ): + self.id = id + self.url = url + self.additional_attributes = kwargs + + def __str__(self): + return str(self.get_non_null_attributes()) + + def __repr__(self): + return self.__str__() + + +class EntityIconField(JsonObject): + """Icon field for entity attributes""" + + attributes = { + "alt_text", + "url", + "slack_file", + } + + def __init__( + self, + alt_text: str, + url: Optional[str] = None, + slack_file: Optional[Union[Dict[str, Any], EntityIconSlackFile]] = None, + **kwargs, + ): + self.alt_text = alt_text + self.url = url + self.slack_file = slack_file + self.additional_attributes = kwargs + + def __str__(self): + return str(self.get_non_null_attributes()) + + def __repr__(self): + return self.__str__() + + +class EntityEditSelectConfig(JsonObject): + """Select configuration for entity edit support""" + + attributes = { + "current_value", + "current_values", + "static_options", + "fetch_options_dynamically", + "min_query_length", + } + + def __init__( + self, + current_value: Optional[str] = None, + current_values: Optional[List[str]] = None, + static_options: Optional[List[Dict[str, Any]]] = None, # Option[] + fetch_options_dynamically: Optional[bool] = None, + min_query_length: Optional[int] = None, + **kwargs, + ): + self.current_value = current_value + self.current_values = current_values + self.static_options = static_options + self.fetch_options_dynamically = fetch_options_dynamically + self.min_query_length = min_query_length + self.additional_attributes = kwargs + + def __str__(self): + return str(self.get_non_null_attributes()) + + def __repr__(self): + return self.__str__() + + +class EntityEditNumberConfig(JsonObject): + """Number configuration for entity edit support""" + + attributes = { + "is_decimal_allowed", + "min_value", + "max_value", + } + + def __init__( + self, + is_decimal_allowed: Optional[bool] = None, + min_value: Optional[Union[int, float]] = None, + max_value: Optional[Union[int, float]] = None, + **kwargs, + ): + self.is_decimal_allowed = is_decimal_allowed + self.min_value = min_value + self.max_value = max_value + self.additional_attributes = kwargs + + def __str__(self): + return str(self.get_non_null_attributes()) + + def __repr__(self): + return self.__str__() + + +class EntityEditTextConfig(JsonObject): + """Text configuration for entity edit support""" + + attributes = { + "min_length", + "max_length", + } + + def __init__( + self, + min_length: Optional[int] = None, + max_length: Optional[int] = None, + **kwargs, + ): + self.min_length = min_length + self.max_length = max_length + self.additional_attributes = kwargs + + def __str__(self): + return str(self.get_non_null_attributes()) + + def __repr__(self): + return self.__str__() + + +class EntityEditSupport(JsonObject): + """Edit support configuration for entity fields""" + + attributes = { + "enabled", + "placeholder", + "hint", + "optional", + "select", + "number", + "text", + } + + def __init__( + self, + enabled: bool, + placeholder: Optional[Dict[str, Any]] = None, # PlainTextElement + hint: Optional[Dict[str, Any]] = None, # PlainTextElement + optional: Optional[bool] = None, + select: Optional[Union[Dict[str, Any], EntityEditSelectConfig]] = None, + number: Optional[Union[Dict[str, Any], EntityEditNumberConfig]] = None, + text: Optional[Union[Dict[str, Any], EntityEditTextConfig]] = None, + **kwargs, + ): + self.enabled = enabled + self.placeholder = placeholder + self.hint = hint + self.optional = optional + self.select = select + self.number = number + self.text = text + self.additional_attributes = kwargs + + def __str__(self): + return str(self.get_non_null_attributes()) + + def __repr__(self): + return self.__str__() + + +class EntityFullSizePreviewError(JsonObject): + """Error information for full-size preview""" + + attributes = { + "code", + "message", + } + + def __init__( + self, + code: str, + message: Optional[str] = None, + **kwargs, + ): + self.code = code + self.message = message + self.additional_attributes = kwargs + + def __str__(self): + return str(self.get_non_null_attributes()) + + def __repr__(self): + return self.__str__() + + +class EntityFullSizePreview(JsonObject): + """Full-size preview configuration for entity""" + + attributes = { + "is_supported", + "preview_url", + "mime_type", + "error", + } + + def __init__( + self, + is_supported: bool, + preview_url: Optional[str] = None, + mime_type: Optional[str] = None, + error: Optional[Union[Dict[str, Any], EntityFullSizePreviewError]] = None, + **kwargs, + ): + self.is_supported = is_supported + self.preview_url = preview_url + self.mime_type = mime_type + self.error = error + self.additional_attributes = kwargs + + def __str__(self): + return str(self.get_non_null_attributes()) + + def __repr__(self): + return self.__str__() + + +class EntityUserIDField(JsonObject): + """User ID field for entity""" + + attributes = { + "user_id", + } + + def __init__( + self, + user_id: str, + **kwargs, + ): + self.user_id = user_id + self.additional_attributes = kwargs + + def __str__(self): + return str(self.get_non_null_attributes()) + + def __repr__(self): + return self.__str__() + + +class EntityUserField(JsonObject): + """User field for entity""" + + attributes = { + "text", + "url", + "email", + "icon", + } + + def __init__( + self, + text: str, + url: Optional[str] = None, + email: Optional[str] = None, + icon: Optional[Union[Dict[str, Any], EntityIconField]] = None, + **kwargs, + ): + self.text = text + self.url = url + self.email = email + self.icon = icon + self.additional_attributes = kwargs + + def __str__(self): + return str(self.get_non_null_attributes()) + + def __repr__(self): + return self.__str__() + + +class EntityRefField(JsonObject): + """Entity reference field""" + + attributes = { + "entity_url", + "external_ref", + "title", + "display_type", + "icon", + } + + def __init__( + self, + entity_url: str, + external_ref: Union[Dict[str, Any], ExternalRef], + title: str, + display_type: Optional[str] = None, + icon: Optional[Union[Dict[str, Any], EntityIconField]] = None, + **kwargs, + ): + self.entity_url = entity_url + self.external_ref = external_ref + self.title = title + self.display_type = display_type + self.icon = icon + self.additional_attributes = kwargs + + def __str__(self): + return str(self.get_non_null_attributes()) + + def __repr__(self): + return self.__str__() + + +class EntityTypedField(JsonObject): + """Typed field for entity with various display options""" + + attributes = { + "type", + "label", + "value", + "link", + "icon", + "long", + "format", + "image_url", + "slack_file", + "alt_text", + "edit", + "tag_color", + "user", + "entity_ref", + } + + def __init__( + self, + type: str, + label: Optional[str] = None, + value: Optional[Union[str, int]] = None, + link: Optional[str] = None, + icon: Optional[Union[Dict[str, Any], EntityIconField]] = None, + long: Optional[bool] = None, + format: Optional[str] = None, + image_url: Optional[str] = None, + slack_file: Optional[Dict[str, Any]] = None, + alt_text: Optional[str] = None, + edit: Optional[Union[Dict[str, Any], EntityEditSupport]] = None, + tag_color: Optional[str] = None, + user: Optional[Union[Dict[str, Any], EntityUserIDField, EntityUserField]] = None, + entity_ref: Optional[Union[Dict[str, Any], EntityRefField]] = None, + **kwargs, + ): + self.type = type + self.label = label + self.value = value + self.link = link + self.icon = icon + self.long = long + self.format = format + self.image_url = image_url + self.slack_file = slack_file + self.alt_text = alt_text + self.edit = edit + self.tag_color = tag_color + self.user = user + self.entity_ref = entity_ref + self.additional_attributes = kwargs + + def __str__(self): + return str(self.get_non_null_attributes()) + + def __repr__(self): + return self.__str__() + + +class EntityStringField(JsonObject): + """String field for entity""" + + attributes = { + "value", + "label", + "format", + "link", + "icon", + "long", + "type", + "tag_color", + "edit", + } + + def __init__( + self, + value: str, + label: Optional[str] = None, + format: Optional[str] = None, + link: Optional[str] = None, + icon: Optional[Union[Dict[str, Any], EntityIconField]] = None, + long: Optional[bool] = None, + type: Optional[str] = None, + tag_color: Optional[str] = None, + edit: Optional[Union[Dict[str, Any], EntityEditSupport]] = None, + **kwargs, + ): + self.value = value + self.label = label + self.format = format + self.link = link + self.icon = icon + self.long = long + self.type = type + self.tag_color = tag_color + self.edit = edit + self.additional_attributes = kwargs + + def __str__(self): + return str(self.get_non_null_attributes()) + + def __repr__(self): + return self.__str__() + + +class EntityTimestampField(JsonObject): + """Timestamp field for entity""" + + attributes = { + "value", + "label", + "type", + "edit", + } + + def __init__( + self, + value: int, + label: Optional[str] = None, + type: Optional[str] = None, + edit: Optional[Union[Dict[str, Any], EntityEditSupport]] = None, + **kwargs, + ): + self.value = value + self.label = label + self.type = type + self.edit = edit + self.additional_attributes = kwargs + + def __str__(self): + return str(self.get_non_null_attributes()) + + def __repr__(self): + return self.__str__() + + +class EntityImageField(JsonObject): + """Image field for entity""" + + attributes = { + "alt_text", + "label", + "image_url", + "slack_file", + "title", + "type", + } + + def __init__( + self, + alt_text: str, + label: Optional[str] = None, + image_url: Optional[str] = None, + slack_file: Optional[Dict[str, Any]] = None, + title: Optional[str] = None, + type: Optional[str] = None, + **kwargs, + ): + self.alt_text = alt_text + self.label = label + self.image_url = image_url + self.slack_file = slack_file + self.title = title + self.type = type + self.additional_attributes = kwargs + + def __str__(self): + return str(self.get_non_null_attributes()) + + def __repr__(self): + return self.__str__() + + +class EntityBooleanCheckboxField(JsonObject): + """Boolean checkbox properties""" + + attributes = {"type", "text", "description"} + + def __init__( + self, + type: str, + text: str, + description: Optional[str], + **kwargs, + ): + self.type = type + self.text = text + self.description = description + self.additional_attributes = kwargs + + def __str__(self): + return str(self.get_non_null_attributes()) + + def __repr__(self): + return self.__str__() + + +class EntityBooleanTextField(JsonObject): + """Boolean text properties""" + + attributes = {"type", "true_text", "false_text", "true_description", "false_description"} + + def __init__( + self, + type: str, + true_text: str, + false_text: str, + true_description: Optional[str], + false_description: Optional[str], + **kwargs, + ): + self.type = type + self.true_text = (true_text,) + self.false_text = (false_text,) + self.true_description = (true_description,) + self.false_description = (false_description,) + self.additional_attributes = kwargs + + def __str__(self): + return str(self.get_non_null_attributes()) + + def __repr__(self): + return self.__str__() + + +class EntityArrayItemField(JsonObject): + """Array item field for entity (similar to EntityTypedField but with optional type)""" + + attributes = { + "type", + "label", + "value", + "link", + "icon", + "long", + "format", + "image_url", + "slack_file", + "alt_text", + "edit", + "tag_color", + "user", + "entity_ref", + } + + def __init__( + self, + type: Optional[str] = None, + label: Optional[str] = None, + value: Optional[Union[str, int]] = None, + link: Optional[str] = None, + icon: Optional[Union[Dict[str, Any], EntityIconField]] = None, + long: Optional[bool] = None, + format: Optional[str] = None, + image_url: Optional[str] = None, + slack_file: Optional[Dict[str, Any]] = None, + alt_text: Optional[str] = None, + edit: Optional[Union[Dict[str, Any], EntityEditSupport]] = None, + tag_color: Optional[str] = None, + user: Optional[Union[Dict[str, Any], EntityUserIDField, EntityUserField]] = None, + entity_ref: Optional[Union[Dict[str, Any], EntityRefField]] = None, + **kwargs, + ): + self.type = type + self.label = label + self.value = value + self.link = link + self.icon = icon + self.long = long + self.format = format + self.image_url = image_url + self.slack_file = slack_file + self.alt_text = alt_text + self.edit = edit + self.tag_color = tag_color + self.user = user + self.entity_ref = entity_ref + self.additional_attributes = kwargs + + def __str__(self): + return str(self.get_non_null_attributes()) + + def __repr__(self): + return self.__str__() + + +class EntityCustomField(JsonObject): + """Custom field for entity with flexible types""" + + attributes = { + "label", + "key", + "type", + "value", + "link", + "icon", + "long", + "format", + "image_url", + "slack_file", + "alt_text", + "tag_color", + "edit", + "item_type", + "user", + "entity_ref", + "boolean", + } + + def __init__( + self, + label: str, + key: str, + type: str, + value: Optional[Union[str, int, List[Union[Dict[str, Any], EntityArrayItemField]]]] = None, + link: Optional[str] = None, + icon: Optional[Union[Dict[str, Any], EntityIconField]] = None, + long: Optional[bool] = None, + format: Optional[str] = None, + image_url: Optional[str] = None, + slack_file: Optional[Dict[str, Any]] = None, + alt_text: Optional[str] = None, + tag_color: Optional[str] = None, + edit: Optional[Union[Dict[str, Any], EntityEditSupport]] = None, + item_type: Optional[str] = None, + user: Optional[Union[Dict[str, Any], EntityUserIDField, EntityUserField]] = None, + entity_ref: Optional[Union[Dict[str, Any], EntityRefField]] = None, + boolean: Optional[Union[Dict[str, Any], EntityBooleanCheckboxField, EntityBooleanTextField]] = None, + **kwargs, + ): + self.label = label + self.key = key + self.type = type + self.value = value + self.link = link + self.icon = icon + self.long = long + self.format = format + self.image_url = image_url + self.slack_file = slack_file + self.alt_text = alt_text + self.tag_color = tag_color + self.edit = edit + self.item_type = item_type + self.user = user + self.entity_ref = entity_ref + self.boolean = boolean + self.additional_attributes = kwargs + + def __str__(self): + return str(self.get_non_null_attributes()) + + def __repr__(self): + return self.__str__() + + @EnumValidator("type", CustomFieldType) + def type_valid(self): + return self.type is None or self.type in CustomFieldType + + +class FileEntityFields(JsonObject): + """Fields specific to file entities""" + + attributes = { + "preview", + "created_by", + "date_created", + "date_updated", + "last_modified_by", + "file_size", + "mime_type", + "full_size_preview", + } + + def __init__( + self, + preview: Optional[Union[Dict[str, Any], EntityImageField]] = None, + created_by: Optional[Union[Dict[str, Any], EntityTypedField]] = None, + date_created: Optional[Union[Dict[str, Any], EntityTimestampField]] = None, + date_updated: Optional[Union[Dict[str, Any], EntityTimestampField]] = None, + last_modified_by: Optional[Union[Dict[str, Any], EntityTypedField]] = None, + file_size: Optional[Union[Dict[str, Any], EntityStringField]] = None, + mime_type: Optional[Union[Dict[str, Any], EntityStringField]] = None, + full_size_preview: Optional[Union[Dict[str, Any], EntityFullSizePreview]] = None, + **kwargs, + ): + self.preview = preview + self.created_by = created_by + self.date_created = date_created + self.date_updated = date_updated + self.last_modified_by = last_modified_by + self.file_size = file_size + self.mime_type = mime_type + self.full_size_preview = full_size_preview + self.additional_attributes = kwargs + + def __str__(self): + return str(self.get_non_null_attributes()) + + def __repr__(self): + return self.__str__() + + +class TaskEntityFields(JsonObject): + """Fields specific to task entities""" + + attributes = { + "description", + "created_by", + "date_created", + "date_updated", + "assignee", + "status", + "due_date", + "priority", + } + + def __init__( + self, + description: Optional[Union[Dict[str, Any], EntityStringField]] = None, + created_by: Optional[Union[Dict[str, Any], EntityTypedField]] = None, + date_created: Optional[Union[Dict[str, Any], EntityTimestampField]] = None, + date_updated: Optional[Union[Dict[str, Any], EntityTimestampField]] = None, + assignee: Optional[Union[Dict[str, Any], EntityTypedField]] = None, + status: Optional[Union[Dict[str, Any], EntityStringField]] = None, + due_date: Optional[Union[Dict[str, Any], EntityTypedField]] = None, + priority: Optional[Union[Dict[str, Any], EntityStringField]] = None, + **kwargs, + ): + self.description = description + self.created_by = created_by + self.date_created = date_created + self.date_updated = date_updated + self.assignee = assignee + self.status = status + self.due_date = due_date + self.priority = priority + self.additional_attributes = kwargs + + def __str__(self): + return str(self.get_non_null_attributes()) + + def __repr__(self): + return self.__str__() + + +class IncidentEntityFields(JsonObject): + """Fields specific to incident entities""" + + attributes = { + "status", + "priority", + "urgency", + "created_by", + "assigned_to", + "date_created", + "date_updated", + "description", + "service", + } + + def __init__( + self, + status: Optional[Union[Dict[str, Any], EntityStringField]] = None, + priority: Optional[Union[Dict[str, Any], EntityStringField]] = None, + urgency: Optional[Union[Dict[str, Any], EntityStringField]] = None, + created_by: Optional[Union[Dict[str, Any], EntityTypedField]] = None, + assigned_to: Optional[Union[Dict[str, Any], EntityTypedField]] = None, + date_created: Optional[Union[Dict[str, Any], EntityTimestampField]] = None, + date_updated: Optional[Union[Dict[str, Any], EntityTimestampField]] = None, + description: Optional[Union[Dict[str, Any], EntityStringField]] = None, + service: Optional[Union[Dict[str, Any], EntityStringField]] = None, + **kwargs, + ): + self.status = status + self.priority = priority + self.urgency = urgency + self.created_by = created_by + self.assigned_to = assigned_to + self.date_created = date_created + self.date_updated = date_updated + self.description = description + self.service = service + self.additional_attributes = kwargs + + def __str__(self): + return str(self.get_non_null_attributes()) + + def __repr__(self): + return self.__str__() + + +class ContentItemEntityFields(JsonObject): + """Fields specific to content item entities""" + + attributes = { + "preview", + "description", + "created_by", + "date_created", + "date_updated", + "last_modified_by", + } + + def __init__( + self, + preview: Optional[Union[Dict[str, Any], EntityImageField]] = None, + description: Optional[Union[Dict[str, Any], EntityStringField]] = None, + created_by: Optional[Union[Dict[str, Any], EntityTypedField]] = None, + date_created: Optional[Union[Dict[str, Any], EntityTimestampField]] = None, + date_updated: Optional[Union[Dict[str, Any], EntityTimestampField]] = None, + last_modified_by: Optional[Union[Dict[str, Any], EntityTypedField]] = None, + **kwargs, + ): + self.preview = preview + self.description = description + self.created_by = created_by + self.date_created = date_created + self.date_updated = date_updated + self.last_modified_by = last_modified_by + self.additional_attributes = kwargs + + def __str__(self): + return str(self.get_non_null_attributes()) + + def __repr__(self): + return self.__str__() + + +class EntityActionProcessingState(JsonObject): + """Processing state configuration for entity action button""" + + attributes = { + "enabled", + "interstitial_text", + } + + def __init__( + self, + enabled: bool, + interstitial_text: Optional[str] = None, + **kwargs, + ): + self.enabled = enabled + self.interstitial_text = interstitial_text + self.additional_attributes = kwargs + + def __str__(self): + return str(self.get_non_null_attributes()) + + def __repr__(self): + return self.__str__() + + +class EntityActionButton(JsonObject): + """Action button for entity""" + + attributes = { + "text", + "action_id", + "value", + "style", + "url", + "accessibility_label", + "processing_state", + } + + def __init__( + self, + text: str, + action_id: str, + value: Optional[str] = None, + style: Optional[str] = None, + url: Optional[str] = None, + accessibility_label: Optional[str] = None, + processing_state: Optional[Union[Dict[str, Any], EntityActionProcessingState]] = None, + **kwargs, + ): + self.text = text + self.action_id = action_id + self.value = value + self.style = style + self.url = url + self.accessibility_label = accessibility_label + self.processing_state = processing_state + self.additional_attributes = kwargs + + def __str__(self): + return str(self.get_non_null_attributes()) + + def __repr__(self): + return self.__str__() + + +class EntityTitle(JsonObject): + """Title for entity attributes""" + + attributes = { + "text", + "edit", + } + + def __init__( + self, + text: str, + edit: Optional[Union[Dict[str, Any], EntityEditSupport]] = None, + **kwargs, + ): + self.text = text + self.edit = edit + self.additional_attributes = kwargs + + def __str__(self): + return str(self.get_non_null_attributes()) + + def __repr__(self): + return self.__str__() + + +class EntityAttributes(JsonObject): + """Attributes for an entity""" + + attributes = { + "title", + "display_type", + "display_id", + "product_icon", + "product_name", + "locale", + "full_size_preview", + "metadata_last_modified", + } + + def __init__( + self, + title: Union[Dict[str, Any], EntityTitle], + display_type: Optional[str] = None, + display_id: Optional[str] = None, + product_icon: Optional[Union[Dict[str, Any], EntityIconField]] = None, + product_name: Optional[str] = None, + locale: Optional[str] = None, + full_size_preview: Optional[Union[Dict[str, Any], EntityFullSizePreview]] = None, + metadata_last_modified: Optional[int] = None, + **kwargs, + ): + self.title = title + self.display_type = display_type + self.display_id = display_id + self.product_icon = product_icon + self.product_name = product_name + self.locale = locale + self.full_size_preview = full_size_preview + self.metadata_last_modified = metadata_last_modified + self.additional_attributes = kwargs + + def __str__(self): + return str(self.get_non_null_attributes()) + + def __repr__(self): + return self.__str__() + + +class EntityActions(JsonObject): + """Actions configuration for entity""" + + attributes = { + "primary_actions", + "overflow_actions", + } + + def __init__( + self, + primary_actions: Optional[List[Union[Dict[str, Any], EntityActionButton]]] = None, + overflow_actions: Optional[List[Union[Dict[str, Any], EntityActionButton]]] = None, + **kwargs, + ): + self.primary_actions = primary_actions + self.overflow_actions = overflow_actions + self.additional_attributes = kwargs + + def __str__(self): + return str(self.get_non_null_attributes()) + + def __repr__(self): + return self.__str__() + + +class EntityPayload(JsonObject): + """Payload schema for an entity""" + + attributes = { + "attributes", + "fields", + "custom_fields", + "slack_file", + "display_order", + "actions", + } + + def __init__( + self, + attributes: Union[Dict[str, Any], EntityAttributes], + fields: Optional[ + Union[Dict[str, Any], ContentItemEntityFields, FileEntityFields, IncidentEntityFields, TaskEntityFields] + ] = None, + custom_fields: Optional[List[Union[Dict[str, Any], EntityCustomField]]] = None, + slack_file: Optional[Union[Dict[str, Any], FileEntitySlackFile]] = None, + display_order: Optional[List[str]] = None, + actions: Optional[Union[Dict[str, Any], EntityActions]] = None, + **kwargs, + ): + # Store entity attributes data with a different internal name to avoid + # shadowing the class-level 'attributes' set used for JSON serialization + self._entity_attributes = attributes + self.fields = fields + self.custom_fields = custom_fields + self.slack_file = slack_file + self.display_order = display_order + self.actions = actions + self.additional_attributes = kwargs + + @property + def entity_attributes(self) -> Union[Dict[str, Any], EntityAttributes]: + """Get the entity attributes data. + + Note: Use this property to access the attributes data. The class-level + 'attributes' is reserved for the JSON serialization schema. + """ + return self._entity_attributes + + @entity_attributes.setter + def entity_attributes(self, value: Union[Dict[str, Any], EntityAttributes]): + """Set the entity attributes data.""" + self._entity_attributes = value + + def get_object_attribute(self, key: str): + if key == "attributes": + return self._entity_attributes + else: + return getattr(self, key, None) + + def __str__(self): + return str(self.get_non_null_attributes()) + + def __repr__(self): + return self.__str__() + + +class EntityMetadata(JsonObject): + """Work object entity metadata + + https://docs.slack.dev/messaging/work-objects/ + """ + + attributes = { + "entity_type", + "entity_payload", + "external_ref", + "url", + "app_unfurl_url", + } + + def __init__( + self, + entity_type: str, + entity_payload: Union[Dict[str, Any], EntityPayload], + external_ref: Union[Dict[str, Any], ExternalRef], + url: str, + app_unfurl_url: Optional[str] = None, + **kwargs, + ): + self.entity_type = entity_type + self.entity_payload = entity_payload + self.external_ref = external_ref + self.url = url + self.app_unfurl_url = app_unfurl_url + self.additional_attributes = kwargs + + def __str__(self): + return str(self.get_non_null_attributes()) + + def __repr__(self): + return self.__str__() + + @EnumValidator("entity_type", EntityType) + def entity_type_valid(self): + return self.entity_type is None or self.entity_type in EntityType + + +class EventAndEntityMetadata(JsonObject): + """Message metadata with entities + + https://docs.slack.dev/messaging/message-metadata/ + https://docs.slack.dev/messaging/work-objects/ + """ + + attributes = {"event_type", "event_payload", "entities"} + + def __init__( + self, + event_type: Optional[str] = None, + event_payload: Optional[Dict[str, Any]] = None, + entities: Optional[List[Union[Dict[str, Any], EntityMetadata]]] = None, + **kwargs, + ): + self.event_type = event_type + self.event_payload = event_payload + self.entities = entities + self.additional_attributes = kwargs + + def __str__(self): + return str(self.get_non_null_attributes()) + + def __repr__(self): + return self.__str__() diff --git a/slack_sdk/models/views/__init__.py b/slack_sdk/models/views/__init__.py new file mode 100644 index 000000000..915f6eade --- /dev/null +++ b/slack_sdk/models/views/__init__.py @@ -0,0 +1,232 @@ +import copy +import logging +from typing import Optional, Union, Dict, Sequence + +from slack_sdk.models.basic_objects import JsonObject, JsonValidator +from slack_sdk.models.blocks import Block, TextObject, PlainTextObject, Option + + +class View(JsonObject): + """View object for modals and Home tabs. + + https://docs.slack.dev/reference/views/ + """ + + types = ["modal", "home", "workflow_step"] + + attributes = { + "type", + "id", + "callback_id", + "external_id", + "team_id", + "bot_id", + "app_id", + "root_view_id", + "previous_view_id", + "title", + "submit", + "close", + "blocks", + "private_metadata", + "state", + "hash", + "clear_on_close", + "notify_on_close", + } + + def __init__( + self, + # "modal", "home", and "workflow_step" + type: str, + id: Optional[str] = None, + callback_id: Optional[str] = None, + external_id: Optional[str] = None, + team_id: Optional[str] = None, + bot_id: Optional[str] = None, + app_id: Optional[str] = None, + root_view_id: Optional[str] = None, + previous_view_id: Optional[str] = None, + title: Optional[Union[str, dict, PlainTextObject]] = None, + submit: Optional[Union[str, dict, PlainTextObject]] = None, + close: Optional[Union[str, dict, PlainTextObject]] = None, + blocks: Optional[Sequence[Union[dict, Block]]] = None, + private_metadata: Optional[str] = None, + state: Optional[Union[dict, "ViewState"]] = None, + hash: Optional[str] = None, + clear_on_close: Optional[bool] = None, + notify_on_close: Optional[bool] = None, + **kwargs, + ): + self.type = type + self.id = id + self.callback_id = callback_id + self.external_id = external_id + self.team_id = team_id + self.bot_id = bot_id + self.app_id = app_id + self.root_view_id = root_view_id + self.previous_view_id = previous_view_id + self.title = TextObject.parse(title, default_type=PlainTextObject.type) # type: ignore[arg-type] + self.submit = TextObject.parse(submit, default_type=PlainTextObject.type) # type: ignore[arg-type] + self.close = TextObject.parse(close, default_type=PlainTextObject.type) # type: ignore[arg-type] + self.blocks = Block.parse_all(blocks) + self.private_metadata = private_metadata + self.state = state + if self.state is not None and isinstance(self.state, dict): + self.state = ViewState(**self.state) + self.hash = hash + self.clear_on_close = clear_on_close + self.notify_on_close = notify_on_close + self.additional_attributes = kwargs + + title_max_length = 24 + blocks_max_length = 100 + close_max_length = 24 + submit_max_length = 24 + private_metadata_max_length = 3000 + callback_id_max_length: int = 255 + + @JsonValidator('type must be either "modal", "home" or "workflow_step"') + def _validate_type(self): + return self.type is not None and self.type in self.types + + @JsonValidator(f"title must be between 1 and {title_max_length} characters") + def _validate_title_length(self): + return self.title is None or 1 <= len(self.title.text) <= self.title_max_length + + @JsonValidator(f"views must contain between 1 and {blocks_max_length} blocks") + def _validate_blocks_length(self): + return self.blocks is None or 0 < len(self.blocks) <= self.blocks_max_length + + @JsonValidator("home view cannot have submit and close") + def _validate_home_tab_structure(self): + return self.type != "home" or (self.type == "home" and self.close is None and self.submit is None) + + @JsonValidator(f"close cannot exceed {close_max_length} characters") + def _validate_close_length(self): + return self.close is None or len(self.close.text) <= self.close_max_length + + @JsonValidator(f"submit cannot exceed {submit_max_length} characters") + def _validate_submit_length(self): + return self.submit is None or len(self.submit.text) <= int(self.submit_max_length) + + @JsonValidator(f"private_metadata cannot exceed {private_metadata_max_length} characters") + def _validate_private_metadata_max_length(self): + return self.private_metadata is None or len(self.private_metadata) <= self.private_metadata_max_length + + @JsonValidator(f"callback_id cannot exceed {callback_id_max_length} characters") + def _validate_callback_id_max_length(self): + return self.callback_id is None or len(self.callback_id) <= self.callback_id_max_length + + def __str__(self): + return str(self.get_non_null_attributes()) + + def __repr__(self): + return self.__str__() + + +class ViewState(JsonObject): + attributes = {"values"} + logger = logging.getLogger(__name__) + + @classmethod + def _show_warning_about_unknown(cls, value): + c = value.__class__ + name = ".".join([c.__module__, c.__name__]) + cls.logger.warning(f"Unknown type for view.state.values detected ({name}) and ViewState skipped to add it") + + def __init__( + self, + *, + values: Dict[str, Dict[str, Union[dict, "ViewStateValue"]]], + ): + value_objects: Dict[str, Dict[str, ViewStateValue]] = {} + new_state_values = copy.copy(values) + if isinstance(new_state_values, dict): # just in case + for block_id, actions in new_state_values.items(): + if actions is None: + continue + elif isinstance(actions, dict): + new_actions: Dict[str, Union[ViewStateValue, dict]] = copy.copy(actions) + for action_id, v in actions.items(): + if isinstance(v, dict): + d = copy.copy(v) + value_object = ViewStateValue(**d) + elif isinstance(v, ViewStateValue): + value_object = v + else: + self._show_warning_about_unknown(v) + continue + new_actions[action_id] = value_object + value_objects[block_id] = new_actions # type: ignore[assignment] + else: + self._show_warning_about_unknown(v) + self.values = value_objects + + def to_dict(self, *args) -> Dict[str, Dict[str, Dict[str, dict]]]: + self.validate_json() + if self.values is not None: + dict_values: Dict[str, Dict[str, dict]] = {} + for block_id, actions in self.values.items(): + if actions: + dict_value: Dict[str, dict] = {action_id: value.to_dict() for action_id, value in actions.items()} + dict_values[block_id] = dict_value + return {"values": dict_values} + else: + return {} + + +class ViewStateValue(JsonObject): + attributes = { + "type", + "value", + "selected_date", + "selected_time", + "selected_conversation", + "selected_channel", + "selected_user", + "selected_option", + "selected_conversations", + "selected_channels", + "selected_users", + "selected_options", + } + + def __init__( + self, + *, + type: Optional[str] = None, + value: Optional[str] = None, + selected_date: Optional[str] = None, + selected_time: Optional[str] = None, + selected_conversation: Optional[str] = None, + selected_channel: Optional[str] = None, + selected_user: Optional[str] = None, + selected_option: Optional[Union[dict, Option]] = None, + selected_conversations: Optional[Sequence[str]] = None, + selected_channels: Optional[Sequence[str]] = None, + selected_users: Optional[Sequence[str]] = None, + selected_options: Optional[Sequence[Union[dict, Option]]] = None, + ): + self.type = type + self.value = value + self.selected_date = selected_date + self.selected_time = selected_time + self.selected_conversation = selected_conversation + self.selected_channel = selected_channel + self.selected_user = selected_user + self.selected_option = selected_option + self.selected_conversations = selected_conversations + self.selected_channels = selected_channels + self.selected_users = selected_users + + if isinstance(selected_options, list): + self.selected_options = [] + for option in selected_options: + if isinstance(option, Option): + self.selected_options.append(option) + elif isinstance(option, dict): + self.selected_options.append(Option(**option)) + else: + self.selected_options = selected_options # type: ignore[assignment] diff --git a/slack_sdk/oauth/__init__.py b/slack_sdk/oauth/__init__.py new file mode 100644 index 000000000..a27b606b0 --- /dev/null +++ b/slack_sdk/oauth/__init__.py @@ -0,0 +1,20 @@ +"""Modules for implementing the Slack OAuth flow + +https://docs.slack.dev/tools/python-slack-sdk/oauth +""" + +from .authorize_url_generator import AuthorizeUrlGenerator +from .authorize_url_generator import OpenIDConnectAuthorizeUrlGenerator +from .installation_store import InstallationStore +from .redirect_uri_page_renderer import RedirectUriPageRenderer +from .state_store import OAuthStateStore +from .state_utils import OAuthStateUtils + +__all__ = [ + "AuthorizeUrlGenerator", + "OpenIDConnectAuthorizeUrlGenerator", + "InstallationStore", + "RedirectUriPageRenderer", + "OAuthStateStore", + "OAuthStateUtils", +] diff --git a/slack_sdk/oauth/authorize_url_generator/__init__.py b/slack_sdk/oauth/authorize_url_generator/__init__.py new file mode 100644 index 000000000..f2617bec6 --- /dev/null +++ b/slack_sdk/oauth/authorize_url_generator/__init__.py @@ -0,0 +1,67 @@ +from typing import Optional, Sequence + + +class AuthorizeUrlGenerator: + def __init__( + self, + *, + client_id: str, + redirect_uri: Optional[str] = None, + scopes: Optional[Sequence[str]] = None, + user_scopes: Optional[Sequence[str]] = None, + authorization_url: str = "https://slack.com/oauth/v2/authorize", + ): + self.client_id = client_id + self.redirect_uri = redirect_uri + self.scopes = scopes + self.user_scopes = user_scopes + self.authorization_url = authorization_url + + def generate(self, state: str, team: Optional[str] = None) -> str: + scopes = ",".join(self.scopes) if self.scopes else "" + user_scopes = ",".join(self.user_scopes) if self.user_scopes else "" + url = ( + f"{self.authorization_url}?" + f"state={state}&" + f"client_id={self.client_id}&" + f"scope={scopes}&" + f"user_scope={user_scopes}" + ) + if self.redirect_uri is not None: + url += f"&redirect_uri={self.redirect_uri}" + if team is not None: + url += f"&team={team}" + return url + + +class OpenIDConnectAuthorizeUrlGenerator: + """Refer to https://openid.net/specs/openid-connect-core-1_0.html""" + + def __init__( + self, + *, + client_id: str, + redirect_uri: str, + scopes: Optional[Sequence[str]] = None, + authorization_url: str = "https://slack.com/openid/connect/authorize", + ): + self.client_id = client_id + self.redirect_uri = redirect_uri + self.scopes = scopes + self.authorization_url = authorization_url + + def generate(self, state: str, nonce: Optional[str] = None, team: Optional[str] = None) -> str: + scopes = ",".join(self.scopes) if self.scopes else "" + url = ( + f"{self.authorization_url}?" + "response_type=code&" + f"state={state}&" + f"client_id={self.client_id}&" + f"scope={scopes}&" + f"redirect_uri={self.redirect_uri}" + ) + if team is not None: + url += f"&team={team}" + if nonce is not None: + url += f"&nonce={nonce}" + return url diff --git a/slack_sdk/oauth/installation_store/__init__.py b/slack_sdk/oauth/installation_store/__init__.py new file mode 100644 index 000000000..b93ebca5e --- /dev/null +++ b/slack_sdk/oauth/installation_store/__init__.py @@ -0,0 +1,10 @@ +from .file import FileInstallationStore +from .installation_store import InstallationStore +from .models import Bot, Installation + +__all__ = [ + "FileInstallationStore", + "InstallationStore", + "Bot", + "Installation", +] diff --git a/slack_sdk/oauth/installation_store/amazon_s3/__init__.py b/slack_sdk/oauth/installation_store/amazon_s3/__init__.py new file mode 100644 index 000000000..c5c420f12 --- /dev/null +++ b/slack_sdk/oauth/installation_store/amazon_s3/__init__.py @@ -0,0 +1,351 @@ +import json +import logging +from logging import Logger +from typing import Optional + +from botocore.client import BaseClient # type: ignore[import-untyped] + +from slack_sdk.errors import SlackClientConfigurationError +from slack_sdk.oauth.installation_store.async_installation_store import ( + AsyncInstallationStore, +) +from slack_sdk.oauth.installation_store.installation_store import InstallationStore +from slack_sdk.oauth.installation_store.models.bot import Bot +from slack_sdk.oauth.installation_store.models.installation import Installation + + +class AmazonS3InstallationStore(InstallationStore, AsyncInstallationStore): + def __init__( + self, + *, + s3_client: BaseClient, + bucket_name: str, + client_id: str, + historical_data_enabled: bool = True, + logger: Logger = logging.getLogger(__name__), + ): + self.s3_client = s3_client + self.bucket_name = bucket_name + self.historical_data_enabled = historical_data_enabled + self.client_id = client_id + self._logger = logger + + @property + def logger(self) -> Logger: + if self._logger is None: + self._logger = logging.getLogger(__name__) + return self._logger + + async def async_save(self, installation: Installation): + return self.save(installation) + + async def async_save_bot(self, bot: Bot): + return self.save_bot(bot) + + def save(self, installation: Installation): + none = "none" + e_id = installation.enterprise_id or none + t_id = installation.team_id or none + workspace_path = f"{self.client_id}/{e_id}-{t_id}" + + self.save_bot(installation.to_bot()) + + if self.historical_data_enabled: + history_version: str = str(installation.installed_at) + + # per workspace + entity: str = json.dumps(installation.__dict__) + response = self.s3_client.put_object( + Bucket=self.bucket_name, + Body=entity, + Key=f"{workspace_path}/installer-latest", + ) + self.logger.debug(f"S3 put_object response: {response}") + response = self.s3_client.put_object( + Bucket=self.bucket_name, + Body=entity, + Key=f"{workspace_path}/installer-{history_version}", + ) + self.logger.debug(f"S3 put_object response: {response}") + + # per workspace per user + u_id = installation.user_id or none + entity = json.dumps(installation.__dict__) + response = self.s3_client.put_object( + Bucket=self.bucket_name, + Body=entity, + Key=f"{workspace_path}/installer-{u_id}-latest", + ) + self.logger.debug(f"S3 put_object response: {response}") + response = self.s3_client.put_object( + Bucket=self.bucket_name, + Body=entity, + Key=f"{workspace_path}/installer-{u_id}-{history_version}", + ) + self.logger.debug(f"S3 put_object response: {response}") + + else: + # per workspace + entity = json.dumps(installation.__dict__) + response = self.s3_client.put_object( + Bucket=self.bucket_name, + Body=entity, + Key=f"{workspace_path}/installer-latest", + ) + self.logger.debug(f"S3 put_object response: {response}") + + # per workspace per user + u_id = installation.user_id or none + entity = json.dumps(installation.__dict__) + response = self.s3_client.put_object( + Bucket=self.bucket_name, + Body=entity, + Key=f"{workspace_path}/installer-{u_id}-latest", + ) + self.logger.debug(f"S3 put_object response: {response}") + + def save_bot(self, bot: Bot): + if bot.bot_token is None: + self.logger.debug("Skipped saving a new row because of the absense of bot token in it") + return + + none = "none" + e_id = bot.enterprise_id or none + t_id = bot.team_id or none + workspace_path = f"{self.client_id}/{e_id}-{t_id}" + + if self.historical_data_enabled: + history_version: str = str(bot.installed_at) + entity: str = json.dumps(bot.__dict__) + response = self.s3_client.put_object( + Bucket=self.bucket_name, + Body=entity, + Key=f"{workspace_path}/bot-latest", + ) + self.logger.debug(f"S3 put_object response: {response}") + response = self.s3_client.put_object( + Bucket=self.bucket_name, + Body=entity, + Key=f"{workspace_path}/bot-{history_version}", + ) + self.logger.debug(f"S3 put_object response: {response}") + + else: + entity = json.dumps(bot.__dict__) + response = self.s3_client.put_object( + Bucket=self.bucket_name, + Body=entity, + Key=f"{workspace_path}/bot-latest", + ) + self.logger.debug(f"S3 put_object response: {response}") + + async def async_find_bot( + self, + *, + enterprise_id: Optional[str], + team_id: Optional[str], + is_enterprise_install: Optional[bool] = False, + ) -> Optional[Bot]: + return self.find_bot( + enterprise_id=enterprise_id, + team_id=team_id, + is_enterprise_install=is_enterprise_install, + ) + + def find_bot( + self, + *, + enterprise_id: Optional[str], + team_id: Optional[str], + is_enterprise_install: Optional[bool] = False, + ) -> Optional[Bot]: + none = "none" + e_id = enterprise_id or none + t_id = team_id or none + if is_enterprise_install: + t_id = none + workspace_path = f"{self.client_id}/{e_id}-{t_id}" + try: + fetch_response = self.s3_client.get_object( + Bucket=self.bucket_name, + Key=f"{workspace_path}/bot-latest", + ) + self.logger.debug(f"S3 get_object response: {fetch_response}") + body = fetch_response["Body"].read().decode("utf-8") + data = json.loads(body) + return Bot(**data) + except Exception as e: + message = f"Failed to find bot installation data for enterprise: {e_id}, team: {t_id}: {e}" + self.logger.warning(message) + return None + + async def async_find_installation( + self, + *, + enterprise_id: Optional[str], + team_id: Optional[str], + user_id: Optional[str] = None, + is_enterprise_install: Optional[bool] = False, + ) -> Optional[Installation]: + return self.find_installation( + enterprise_id=enterprise_id, + team_id=team_id, + user_id=user_id, + is_enterprise_install=is_enterprise_install, + ) + + def find_installation( + self, + *, + enterprise_id: Optional[str], + team_id: Optional[str], + user_id: Optional[str] = None, + is_enterprise_install: Optional[bool] = False, + ) -> Optional[Installation]: + none = "none" + e_id = enterprise_id or none + t_id = team_id or none + if is_enterprise_install: + t_id = none + workspace_path = f"{self.client_id}/{e_id}-{t_id}" + try: + key = f"{workspace_path}/installer-{user_id}-latest" if user_id else f"{workspace_path}/installer-latest" + fetch_response = self.s3_client.get_object( + Bucket=self.bucket_name, + Key=key, + ) + self.logger.debug(f"S3 get_object response: {fetch_response}") + body = fetch_response["Body"].read().decode("utf-8") + data = json.loads(body) + installation = Installation(**data) + + has_user_installation = user_id is not None and installation is not None + no_bot_token_installation = installation is not None and installation.bot_token is None + should_find_bot_installation = has_user_installation or no_bot_token_installation + if should_find_bot_installation: + # Retrieve the latest bot token, just in case + # See also: https://github.com/slackapi/bolt-python/issues/664 + latest_bot_installation = self.find_bot( + enterprise_id=enterprise_id, + team_id=team_id, + is_enterprise_install=is_enterprise_install, + ) + if latest_bot_installation is not None and installation.bot_token != latest_bot_installation.bot_token: + # NOTE: this logic is based on the assumption that every single installation has bot scopes + # If you need to installation patterns without bot scopes in the same S3 bucket, + # please fork this code and implement your own logic. + installation.bot_id = latest_bot_installation.bot_id + installation.bot_user_id = latest_bot_installation.bot_user_id + installation.bot_token = latest_bot_installation.bot_token + installation.bot_scopes = latest_bot_installation.bot_scopes + installation.bot_refresh_token = latest_bot_installation.bot_refresh_token + installation.bot_token_expires_at = latest_bot_installation.bot_token_expires_at + + return installation + + except Exception as e: + message = f"Failed to find an installation data for enterprise: {e_id}, team: {t_id}: {e}" + self.logger.warning(message) + return None + + async def async_delete_bot(self, *, enterprise_id: Optional[str], team_id: Optional[str]) -> None: + return self.delete_bot( + enterprise_id=enterprise_id, + team_id=team_id, + ) + + def delete_bot(self, *, enterprise_id: Optional[str], team_id: Optional[str]) -> None: + none = "none" + e_id = enterprise_id or none + t_id = team_id or none + workspace_path = f"{self.client_id}/{e_id}-{t_id}" + objects = self.s3_client.list_objects( + Bucket=self.bucket_name, + Prefix=f"{workspace_path}/bot-", + ) + for content in objects.get("Contents", []): + key = content.get("Key") + if key is not None: + self.logger.info(f"Going to delete bot installation ({key})") + try: + self.s3_client.delete_object( + Bucket=self.bucket_name, + Key=content.get("Key"), + ) + except Exception as e: + message = f"Failed to find bot installation data for enterprise: {e_id}, team: {t_id}: {e}" + raise SlackClientConfigurationError(message) + + async def async_delete_installation( + self, + *, + enterprise_id: Optional[str], + team_id: Optional[str], + user_id: Optional[str] = None, + ) -> None: + return self.delete_installation( + enterprise_id=enterprise_id, + team_id=team_id, + user_id=user_id, + ) + + def delete_installation( + self, + *, + enterprise_id: Optional[str], + team_id: Optional[str], + user_id: Optional[str] = None, + ) -> None: + none = "none" + e_id = enterprise_id or none + t_id = team_id or none + workspace_path = f"{self.client_id}/{e_id}-{t_id}" + objects = self.s3_client.list_objects( + Bucket=self.bucket_name, + Prefix=f"{workspace_path}/installer-{user_id or ''}", + ) + deleted_keys = [] + for content in objects.get("Contents", []): + key = content.get("Key") + if key is not None: + self.logger.info(f"Going to delete installation ({key})") + try: + self.s3_client.delete_object( + Bucket=self.bucket_name, + Key=key, + ) + deleted_keys.append(key) + except Exception as e: + message = f"Failed to find bot installation data for enterprise: {e_id}, team: {t_id}: {e}" + raise SlackClientConfigurationError(message) + + try: + no_user_id_key = key.replace(f"-{user_id}", "") + if not no_user_id_key.endswith("installer-latest"): + self.s3_client.delete_object( + Bucket=self.bucket_name, + Key=no_user_id_key, + ) + deleted_keys.append(no_user_id_key) + except Exception as e: + message = f"Failed to find bot installation data for enterprise: {e_id}, team: {t_id}: {e}" + raise SlackClientConfigurationError(message) + + # Check the remaining installation data + objects = self.s3_client.list_objects( + Bucket=self.bucket_name, + Prefix=f"{workspace_path}/installer-", + MaxKeys=10, # the small number would be enough for this purpose + ) + keys = [c.get("Key") for c in objects.get("Contents", []) if c.get("Key") not in deleted_keys] + # If only installer-latest remains, we should delete the one as well + if len(keys) == 1 and keys[0].endswith("installer-latest"): + content = objects.get("Contents", [])[0] + try: + self.s3_client.delete_object( + Bucket=self.bucket_name, + Key=content.get("Key"), + ) + except Exception as e: + message = f"Failed to find bot installation data for enterprise: {e_id}, team: {t_id}: {e}" + raise SlackClientConfigurationError(message) diff --git a/slack_sdk/oauth/installation_store/async_cacheable_installation_store.py b/slack_sdk/oauth/installation_store/async_cacheable_installation_store.py new file mode 100644 index 000000000..935d24fe4 --- /dev/null +++ b/slack_sdk/oauth/installation_store/async_cacheable_installation_store.py @@ -0,0 +1,136 @@ +from logging import Logger +from typing import Optional, Dict + +from slack_sdk.oauth.installation_store import Bot, Installation +from slack_sdk.oauth.installation_store.async_installation_store import ( + AsyncInstallationStore, +) + + +class AsyncCacheableInstallationStore(AsyncInstallationStore): + underlying: AsyncInstallationStore + cached_bots: Dict[str, Bot] + cached_installations: Dict[str, Installation] + + def __init__(self, installation_store: AsyncInstallationStore): + """A simple memory cache wrapper for any installation stores. + + Args: + installation_store: The installation store to wrap + """ + self.underlying = installation_store + self.cached_bots = {} + self.cached_installations = {} + + @property + def logger(self) -> Logger: + return self.underlying.logger + + async def async_save(self, installation: Installation): + # Invalidate cache data for update operations + key = f"{installation.enterprise_id or ''}-{installation.team_id or ''}" + if key in self.cached_bots: + self.cached_bots.pop(key) + key = f"{installation.enterprise_id or ''}-{installation.team_id or ''}-{installation.user_id or ''}" + if key in self.cached_installations: + self.cached_installations.pop(key) + return await self.underlying.async_save(installation) + + async def async_save_bot(self, bot: Bot): + # Invalidate cache data for update operations + key = f"{bot.enterprise_id or ''}-{bot.team_id or ''}" + if key in self.cached_bots: + self.cached_bots.pop(key) + return await self.underlying.async_save_bot(bot) + + async def async_find_bot( + self, + *, + enterprise_id: Optional[str], + team_id: Optional[str], + is_enterprise_install: Optional[bool] = False, + ) -> Optional[Bot]: + if is_enterprise_install or team_id is None: + team_id = "" + key = f"{enterprise_id or ''}-{team_id or ''}" + if key in self.cached_bots: + return self.cached_bots[key] + bot = await self.underlying.async_find_bot( + enterprise_id=enterprise_id, + team_id=team_id, + is_enterprise_install=is_enterprise_install, + ) + if bot: + self.cached_bots[key] = bot + return bot + + async def async_find_installation( + self, + *, + enterprise_id: Optional[str], + team_id: Optional[str], + user_id: Optional[str] = None, + is_enterprise_install: Optional[bool] = False, + ) -> Optional[Installation]: + if is_enterprise_install or team_id is None: + team_id = "" + key = f"{enterprise_id or ''}-{team_id or ''}-{user_id or ''}" + if key in self.cached_installations: + return self.cached_installations[key] + installation = await self.underlying.async_find_installation( + enterprise_id=enterprise_id, + team_id=team_id, + user_id=user_id, + is_enterprise_install=is_enterprise_install, + ) + if installation: + self.cached_installations[key] = installation + return installation + + async def async_delete_bot( + self, + *, + enterprise_id: Optional[str], + team_id: Optional[str], + ) -> None: + await self.underlying.async_delete_bot( + enterprise_id=enterprise_id, + team_id=team_id, + ) + key = f"{enterprise_id or ''}-{team_id or ''}" + self.cached_bots.pop(key) + + async def async_delete_installation( + self, + *, + enterprise_id: Optional[str], + team_id: Optional[str], + user_id: Optional[str] = None, + ) -> None: + await self.underlying.async_delete_installation( + enterprise_id=enterprise_id, + team_id=team_id, + user_id=user_id, + ) + key_prefix = f"{enterprise_id or ''}-{team_id or ''}" + for key in list(self.cached_installations.keys()): + if key.startswith(key_prefix): + self.cached_installations.pop(key) + + async def async_delete_all( + self, + *, + enterprise_id: Optional[str], + team_id: Optional[str], + ): + await self.underlying.async_delete_all( + enterprise_id=enterprise_id, + team_id=team_id, + ) + key_prefix = f"{enterprise_id or ''}-{team_id or ''}" + for key in list(self.cached_bots.keys()): + if key.startswith(key_prefix): + self.cached_bots.pop(key) + for key in list(self.cached_installations.keys()): + if key.startswith(key_prefix): + self.cached_installations.pop(key) diff --git a/slack_sdk/oauth/installation_store/async_installation_store.py b/slack_sdk/oauth/installation_store/async_installation_store.py new file mode 100644 index 000000000..f8b76b860 --- /dev/null +++ b/slack_sdk/oauth/installation_store/async_installation_store.py @@ -0,0 +1,92 @@ +from logging import Logger +from typing import Optional + +from .models.bot import Bot +from .models.installation import Installation + + +class AsyncInstallationStore: + """The installation store interface for asyncio-based apps. + + The minimum required methods are: + + * async_save(installation) + * async_find_installation(enterprise_id, team_id, user_id, is_enterprise_install) + + If you would like to properly handle app uninstallations and token revocations, + the following methods should be implemented. + + * async_delete_installation(enterprise_id, team_id, user_id) + * async_delete_all(enterprise_id, team_id) + + If your app needs only bot scope installations, the simpler way to implement would be: + + * async_save(installation) + * async_find_bot(enterprise_id, team_id, is_enterprise_install) + * async_delete_bot(enterprise_id, team_id) + * async_delete_all(enterprise_id, team_id) + """ + + @property + def logger(self) -> Logger: + raise NotImplementedError() + + async def async_save(self, installation: Installation): + """Saves an installation data""" + raise NotImplementedError() + + async def async_save_bot(self, bot: Bot): + """Saves a bot installation data""" + raise NotImplementedError() + + async def async_find_bot( + self, + *, + enterprise_id: Optional[str], + team_id: Optional[str], + is_enterprise_install: Optional[bool] = False, + ) -> Optional[Bot]: + """Finds a bot scope installation per workspace / org""" + raise NotImplementedError() + + async def async_find_installation( + self, + *, + enterprise_id: Optional[str], + team_id: Optional[str], + user_id: Optional[str] = None, + is_enterprise_install: Optional[bool] = False, + ) -> Optional[Installation]: + """Finds a relevant installation for the given IDs. + If the user_id is absent, this method may return the latest installation in the workspace / org. + """ + raise NotImplementedError() + + async def async_delete_bot( + self, + *, + enterprise_id: Optional[str], + team_id: Optional[str], + ) -> None: + """Deletes a bot scope installation per workspace / org""" + raise NotImplementedError() + + async def async_delete_installation( + self, + *, + enterprise_id: Optional[str], + team_id: Optional[str], + user_id: Optional[str] = None, + ) -> None: + """Deletes an installation that matches the given IDs""" + raise NotImplementedError() + + async def async_delete_all( + self, + *, + enterprise_id: Optional[str], + team_id: Optional[str], + ): + """Deletes all installation data for the given workspace / org""" + await self.async_delete_bot(enterprise_id=enterprise_id, team_id=team_id) + await self.async_delete_installation(enterprise_id=enterprise_id, team_id=team_id) diff --git a/slack_sdk/oauth/installation_store/cacheable_installation_store.py b/slack_sdk/oauth/installation_store/cacheable_installation_store.py new file mode 100644 index 000000000..2455779dd --- /dev/null +++ b/slack_sdk/oauth/installation_store/cacheable_installation_store.py @@ -0,0 +1,136 @@ +from logging import Logger +from typing import Optional, Dict + +from slack_sdk.oauth import InstallationStore +from slack_sdk.oauth.installation_store import Bot, Installation + + +class CacheableInstallationStore(InstallationStore): + underlying: InstallationStore + cached_bots: Dict[str, Bot] + cached_installations: Dict[str, Installation] + + def __init__(self, installation_store: InstallationStore): + """A simple memory cache wrapper for any installation stores. + + Args: + installation_store: The installation store to wrap + """ + self.underlying = installation_store + self.cached_bots = {} + self.cached_installations = {} + + @property + def logger(self) -> Logger: + return self.underlying.logger + + def save(self, installation: Installation): + # Invalidate cache data for update operations + key = f"{installation.enterprise_id or ''}-{installation.team_id or ''}" + if key in self.cached_bots: + self.cached_bots.pop(key) + key = f"{installation.enterprise_id or ''}-{installation.team_id or ''}-{installation.user_id or ''}" + if key in self.cached_installations: + self.cached_installations.pop(key) + + return self.underlying.save(installation) + + def save_bot(self, bot: Bot): + # Invalidate cache data for update operations + key = f"{bot.enterprise_id or ''}-{bot.team_id or ''}" + if key in self.cached_bots: + self.cached_bots.pop(key) + return self.underlying.save_bot(bot) + + def find_bot( + self, + *, + enterprise_id: Optional[str], + team_id: Optional[str], + is_enterprise_install: Optional[bool] = False, + ) -> Optional[Bot]: + if is_enterprise_install or team_id is None: + team_id = "" + key = f"{enterprise_id or ''}-{team_id or ''}" + if key in self.cached_bots: + return self.cached_bots[key] + bot = self.underlying.find_bot( + enterprise_id=enterprise_id, + team_id=team_id, + is_enterprise_install=is_enterprise_install, + ) + if bot: + self.cached_bots[key] = bot + return bot + + def find_installation( + self, + *, + enterprise_id: Optional[str], + team_id: Optional[str], + user_id: Optional[str] = None, + is_enterprise_install: Optional[bool] = False, + ) -> Optional[Installation]: + if is_enterprise_install or team_id is None: + team_id = "" + key = f"{enterprise_id or ''}-{team_id or ''}-{user_id or ''}" + if key in self.cached_installations: + return self.cached_installations[key] + installation = self.underlying.find_installation( + enterprise_id=enterprise_id, + team_id=team_id, + user_id=user_id, + is_enterprise_install=is_enterprise_install, + ) + if installation: + self.cached_installations[key] = installation + return installation + + def delete_bot( + self, + *, + enterprise_id: Optional[str], + team_id: Optional[str], + ) -> None: + self.underlying.delete_bot( + enterprise_id=enterprise_id, + team_id=team_id, + ) + key = f"{enterprise_id or ''}-{team_id or ''}" + if key in self.cached_bots: + self.cached_bots.pop(key) + + def delete_installation( + self, + *, + enterprise_id: Optional[str], + team_id: Optional[str], + user_id: Optional[str] = None, + ) -> None: + self.underlying.delete_installation( + enterprise_id=enterprise_id, + team_id=team_id, + user_id=user_id, + ) + key_prefix = f"{enterprise_id or ''}-{team_id or ''}" + for key in list(self.cached_installations.keys()): + if key.startswith(key_prefix): + self.cached_installations.pop(key) + + def delete_all( + self, + *, + enterprise_id: Optional[str], + team_id: Optional[str], + ): + self.underlying.delete_all( + enterprise_id=enterprise_id, + team_id=team_id, + ) + key_prefix = f"{enterprise_id or ''}-{team_id or ''}" + for key in list(self.cached_bots.keys()): + if key.startswith(key_prefix): + self.cached_bots.pop(key) + for key in list(self.cached_installations.keys()): + if key.startswith(key_prefix): + self.cached_installations.pop(key) diff --git a/slack_sdk/oauth/installation_store/file/__init__.py b/slack_sdk/oauth/installation_store/file/__init__.py new file mode 100644 index 000000000..e178048b4 --- /dev/null +++ b/slack_sdk/oauth/installation_store/file/__init__.py @@ -0,0 +1,252 @@ +import glob +import json +import logging +import os +from logging import Logger +from pathlib import Path +from typing import Optional, Union + +from slack_sdk.oauth.installation_store.async_installation_store import ( + AsyncInstallationStore, +) +from slack_sdk.oauth.installation_store.installation_store import InstallationStore +from slack_sdk.oauth.installation_store.models.bot import Bot +from slack_sdk.oauth.installation_store.models.installation import Installation + + +class FileInstallationStore(InstallationStore, AsyncInstallationStore): + def __init__( + self, + *, + base_dir: str = str(Path.home()) + "/.bolt-app-installation", + historical_data_enabled: bool = True, + client_id: Optional[str] = None, + logger: Logger = logging.getLogger(__name__), + ): + self.base_dir = base_dir + self.historical_data_enabled = historical_data_enabled + self.client_id = client_id + if self.client_id is not None: + self.base_dir = f"{self.base_dir}/{self.client_id}" + self._logger = logger + + @property + def logger(self) -> Logger: + if self._logger is None: + self._logger = logging.getLogger(__name__) + return self._logger + + async def async_save(self, installation: Installation): + return self.save(installation) + + async def async_save_bot(self, bot: Bot): + return self.save_bot(bot) + + def save(self, installation: Installation): + none = "none" + e_id = installation.enterprise_id or none + t_id = installation.team_id or none + team_installation_dir = f"{self.base_dir}/{e_id}-{t_id}" + self._mkdir(team_installation_dir) + + self.save_bot(installation.to_bot()) + + if self.historical_data_enabled: + history_version: str = str(installation.installed_at) + + # per workspace + entity: str = json.dumps(installation.__dict__) + with open(f"{team_installation_dir}/installer-latest", "w") as f: + f.write(entity) + with open(f"{team_installation_dir}/installer-{history_version}", "w") as f: + f.write(entity) + + # per workspace per user + u_id = installation.user_id or none + entity = json.dumps(installation.__dict__) + with open(f"{team_installation_dir}/installer-{u_id}-latest", "w") as f: + f.write(entity) + with open(f"{team_installation_dir}/installer-{u_id}-{history_version}", "w") as f: + f.write(entity) + + else: + u_id = installation.user_id or none + installer_filepath = f"{team_installation_dir}/installer-{u_id}-latest" + with open(installer_filepath, "w") as f: + entity = json.dumps(installation.__dict__) + f.write(entity) + + def save_bot(self, bot: Bot): + if bot.bot_token is None: + self.logger.debug("Skipped saving a new row because of the absense of bot token in it") + return + + none = "none" + e_id = bot.enterprise_id or none + t_id = bot.team_id or none + team_installation_dir = f"{self.base_dir}/{e_id}-{t_id}" + self._mkdir(team_installation_dir) + + if self.historical_data_enabled: + history_version: str = str(bot.installed_at) + + entity: str = json.dumps(bot.__dict__) + with open(f"{team_installation_dir}/bot-latest", "w") as f: + f.write(entity) + with open(f"{team_installation_dir}/bot-{history_version}", "w") as f: + f.write(entity) + else: + with open(f"{team_installation_dir}/bot-latest", "w") as f: + entity = json.dumps(bot.__dict__) + f.write(entity) + + async def async_find_bot( + self, + *, + enterprise_id: Optional[str], + team_id: Optional[str], + is_enterprise_install: Optional[bool] = False, + ) -> Optional[Bot]: + return self.find_bot( + enterprise_id=enterprise_id, + team_id=team_id, + is_enterprise_install=is_enterprise_install, + ) + + def find_bot( + self, + *, + enterprise_id: Optional[str], + team_id: Optional[str], + is_enterprise_install: Optional[bool] = False, + ) -> Optional[Bot]: + none = "none" + e_id = enterprise_id or none + t_id = team_id or none + if is_enterprise_install: + t_id = none + bot_filepath = f"{self.base_dir}/{e_id}-{t_id}/bot-latest" + try: + with open(bot_filepath) as f: + data = json.loads(f.read()) + return Bot(**data) + except FileNotFoundError as e: + message = f"Installation data missing for enterprise: {e_id}, team: {t_id}: {e}" + self.logger.debug(message) + return None + + async def async_find_installation( + self, + *, + enterprise_id: Optional[str], + team_id: Optional[str], + user_id: Optional[str] = None, + is_enterprise_install: Optional[bool] = False, + ) -> Optional[Installation]: + return self.find_installation( + enterprise_id=enterprise_id, + team_id=team_id, + user_id=user_id, + is_enterprise_install=is_enterprise_install, + ) + + def find_installation( + self, + *, + enterprise_id: Optional[str], + team_id: Optional[str], + user_id: Optional[str] = None, + is_enterprise_install: Optional[bool] = False, + ) -> Optional[Installation]: + none = "none" + e_id = enterprise_id or none + t_id = team_id or none + if is_enterprise_install: + t_id = none + installation_filepath = f"{self.base_dir}/{e_id}-{t_id}/installer-latest" + if user_id is not None: + installation_filepath = f"{self.base_dir}/{e_id}-{t_id}/installer-{user_id}-latest" + + try: + installation: Optional[Installation] = None + with open(installation_filepath) as f: + data = json.loads(f.read()) + installation = Installation(**data) + + has_user_installation = user_id is not None and installation is not None + no_bot_token_installation = installation is not None and installation.bot_token is None + should_find_bot_installation = has_user_installation or no_bot_token_installation + if should_find_bot_installation: + # Retrieve the latest bot token, just in case + # See also: https://github.com/slackapi/bolt-python/issues/664 + latest_bot_installation = self.find_bot( + enterprise_id=enterprise_id, + team_id=team_id, + is_enterprise_install=is_enterprise_install, + ) + if latest_bot_installation is not None and installation.bot_token != latest_bot_installation.bot_token: + # NOTE: this logic is based on the assumption that every single installation has bot scopes + # If you need to installation patterns without bot scopes in the same S3 bucket, + # please fork this code and implement your own logic. + installation.bot_id = latest_bot_installation.bot_id + installation.bot_user_id = latest_bot_installation.bot_user_id + installation.bot_token = latest_bot_installation.bot_token + installation.bot_scopes = latest_bot_installation.bot_scopes + installation.bot_refresh_token = latest_bot_installation.bot_refresh_token + installation.bot_token_expires_at = latest_bot_installation.bot_token_expires_at + + return installation + + except FileNotFoundError as e: + message = f"Installation data missing for enterprise: {e_id}, team: {t_id}: {e}" + self.logger.debug(message) + return None + + async def async_delete_bot(self, *, enterprise_id: Optional[str], team_id: Optional[str]) -> None: + return self.delete_bot(enterprise_id=enterprise_id, team_id=team_id) + + def delete_bot(self, *, enterprise_id: Optional[str], team_id: Optional[str]) -> None: + none = "none" + e_id = enterprise_id or none + t_id = team_id or none + filepath_glob = f"{self.base_dir}/{e_id}-{t_id}/bot-*" + self._delete_by_glob(e_id, t_id, filepath_glob) + + async def async_delete_installation( + self, + *, + enterprise_id: Optional[str], + team_id: Optional[str], + user_id: Optional[str] = None, + ) -> None: + return self.delete_installation(enterprise_id=enterprise_id, team_id=team_id, user_id=user_id) + + def delete_installation( + self, + *, + enterprise_id: Optional[str], + team_id: Optional[str], + user_id: Optional[str] = None, + ) -> None: + none = "none" + e_id = enterprise_id or none + t_id = team_id or none + if user_id is not None: + filepath_glob = f"{self.base_dir}/{e_id}-{t_id}/installer-{user_id}-*" + else: + filepath_glob = f"{self.base_dir}/{e_id}-{t_id}/installer-*" + self._delete_by_glob(e_id, t_id, filepath_glob) + + def _delete_by_glob(self, e_id: str, t_id: str, filepath_glob: str): + for filepath in glob.glob(filepath_glob): + try: + os.remove(filepath) + except FileNotFoundError as e: + message = f"Failed to delete installation data for enterprise: {e_id}, team: {t_id}: {e}" + self.logger.warning(message) + + @staticmethod + def _mkdir(path: Union[str, Path]): + if isinstance(path, str): + path = Path(path) + path.mkdir(parents=True, exist_ok=True) diff --git a/slack_sdk/oauth/installation_store/installation_store.py b/slack_sdk/oauth/installation_store/installation_store.py new file mode 100644 index 000000000..8143d2fb7 --- /dev/null +++ b/slack_sdk/oauth/installation_store/installation_store.py @@ -0,0 +1,97 @@ +"""Slack installation data store + +Refer to https://docs.slack.dev/tools/python-slack-sdk/oauth for details. +""" + +from logging import Logger +from typing import Optional + +from .models.bot import Bot +from .models.installation import Installation + + +class InstallationStore: + """The installation store interface. + + The minimum required methods are: + + * save(installation) + * find_installation(enterprise_id, team_id, user_id, is_enterprise_install) + + If you would like to properly handle app uninstallations and token revocations, + the following methods should be implemented. + + * delete_installation(enterprise_id, team_id, user_id) + * delete_all(enterprise_id, team_id) + + If your app needs only bot scope installations, the simpler way to implement would be: + + * save(installation) + * find_bot(enterprise_id, team_id, is_enterprise_install) + * delete_bot(enterprise_id, team_id) + * delete_all(enterprise_id, team_id) + """ + + @property + def logger(self) -> Logger: + raise NotImplementedError() + + def save(self, installation: Installation): + """Saves an installation data""" + raise NotImplementedError() + + def save_bot(self, bot: Bot): + """Saves a bot installation data""" + raise NotImplementedError() + + def find_bot( + self, + *, + enterprise_id: Optional[str], + team_id: Optional[str], + is_enterprise_install: Optional[bool] = False, + ) -> Optional[Bot]: + """Finds a bot scope installation per workspace / org""" + raise NotImplementedError() + + def find_installation( + self, + *, + enterprise_id: Optional[str], + team_id: Optional[str], + user_id: Optional[str] = None, + is_enterprise_install: Optional[bool] = False, + ) -> Optional[Installation]: + """Finds a relevant installation for the given IDs. + If the user_id is absent, this method may return the latest installation in the workspace / org. + """ + raise NotImplementedError() + + def delete_bot( + self, + *, + enterprise_id: Optional[str], + team_id: Optional[str], + ) -> None: + """Deletes a bot scope installation per workspace / org""" + raise NotImplementedError() + + def delete_installation( + self, + *, + enterprise_id: Optional[str], + team_id: Optional[str], + user_id: Optional[str] = None, + ) -> None: + """Deletes an installation that matches the given IDs""" + raise NotImplementedError() + + def delete_all( + self, + *, + enterprise_id: Optional[str], + team_id: Optional[str], + ): + """Deletes all installation data for the given workspace / org""" + self.delete_bot(enterprise_id=enterprise_id, team_id=team_id) + self.delete_installation(enterprise_id=enterprise_id, team_id=team_id) diff --git a/slack_sdk/oauth/installation_store/internals.py b/slack_sdk/oauth/installation_store/internals.py new file mode 100644 index 000000000..52eeeb98b --- /dev/null +++ b/slack_sdk/oauth/installation_store/internals.py @@ -0,0 +1,40 @@ +from datetime import datetime +from typing import Type, TypeVar, Union + + +def _from_iso_format_to_datetime(iso_datetime_str: str) -> datetime: + if "+" not in iso_datetime_str: + iso_datetime_str += "+00:00" + return datetime.fromisoformat(iso_datetime_str) + + +def _from_iso_format_to_unix_timestamp(iso_datetime_str: str) -> float: + return _from_iso_format_to_datetime(iso_datetime_str).timestamp() + + +TimestampType = TypeVar("TimestampType", float, int) + + +def _timestamp_to_type(ts: Union[TimestampType, datetime, str], target_type: Type[TimestampType]) -> TimestampType: + result: TimestampType + + if isinstance(ts, target_type): + # unnecessary type casting makes pytype happy + result = target_type(ts) + + # although a type of the timestamp is just checked, + # pytype doesn't consider the following line valid: + # result = ts + # see https://github.com/google/pytype/issues/1012 + + elif isinstance(ts, datetime): + result = target_type(ts.timestamp()) + elif isinstance(ts, str): + try: + result = target_type(ts) + except ValueError: + result = target_type(_from_iso_format_to_unix_timestamp(ts)) + else: + raise ValueError(f"Unsupported data format for timestamp {ts}") + + return result diff --git a/slack_sdk/oauth/installation_store/models/__init__.py b/slack_sdk/oauth/installation_store/models/__init__.py new file mode 100644 index 000000000..d65b1474c --- /dev/null +++ b/slack_sdk/oauth/installation_store/models/__init__.py @@ -0,0 +1,7 @@ +from .bot import Bot +from .installation import Installation + +__all__ = [ + "Bot", + "Installation", +] diff --git a/slack_sdk/oauth/installation_store/models/bot.py b/slack_sdk/oauth/installation_store/models/bot.py new file mode 100644 index 000000000..3f2f6de81 --- /dev/null +++ b/slack_sdk/oauth/installation_store/models/bot.py @@ -0,0 +1,117 @@ +from datetime import datetime, timezone +from time import time +from typing import Optional, Union, Dict, Any, Sequence + +from slack_sdk.oauth.installation_store.internals import _timestamp_to_type + + +class Bot: + app_id: Optional[str] + enterprise_id: Optional[str] + enterprise_name: Optional[str] + team_id: Optional[str] + team_name: Optional[str] + bot_token: str + bot_id: str + bot_user_id: str + bot_scopes: Sequence[str] + # only when token rotation is enabled + bot_refresh_token: Optional[str] + # only when token rotation is enabled + bot_token_expires_at: Optional[int] + is_enterprise_install: bool + installed_at: float + + custom_values: Dict[str, Any] + + def __init__( + self, + *, + app_id: Optional[str] = None, + # org / workspace + enterprise_id: Optional[str] = None, + enterprise_name: Optional[str] = None, + team_id: Optional[str] = None, + team_name: Optional[str] = None, + # bot + bot_token: str, + bot_id: str, + bot_user_id: str, + bot_scopes: Union[str, Sequence[str]] = "", + # only when token rotation is enabled + bot_refresh_token: Optional[str] = None, + # only when token rotation is enabled + bot_token_expires_in: Optional[int] = None, + # only for duplicating this object + # only when token rotation is enabled + bot_token_expires_at: Optional[Union[int, datetime, str]] = None, + is_enterprise_install: Optional[bool] = False, + # timestamps + # The expected value type is float but the internals handle other types too + # for str values, we support only ISO datetime format. + installed_at: Union[float, datetime, str], + # custom values + custom_values: Optional[Dict[str, Any]] = None, + ): + self.app_id = app_id + self.enterprise_id = enterprise_id + self.enterprise_name = enterprise_name + self.team_id = team_id + self.team_name = team_name + + self.bot_token = bot_token + self.bot_id = bot_id + self.bot_user_id = bot_user_id + if isinstance(bot_scopes, str): + self.bot_scopes = bot_scopes.split(",") if len(bot_scopes) > 0 else [] + else: + self.bot_scopes = bot_scopes + self.bot_refresh_token = bot_refresh_token + + if bot_token_expires_at is not None: + self.bot_token_expires_at = _timestamp_to_type(bot_token_expires_at, int) + elif bot_token_expires_in is not None: + self.bot_token_expires_at = int(time()) + bot_token_expires_in + else: + self.bot_token_expires_at = None + + self.is_enterprise_install = is_enterprise_install or False + + self.installed_at = _timestamp_to_type(installed_at, float) + + self.custom_values = custom_values if custom_values is not None else {} + + def set_custom_value(self, name: str, value: Any): + self.custom_values[name] = value + + def get_custom_value(self, name: str) -> Optional[Any]: + return self.custom_values.get(name) + + def _to_standard_value_dict(self) -> Dict[str, Any]: + return { + "app_id": self.app_id, + "enterprise_id": self.enterprise_id, + "enterprise_name": self.enterprise_name, + "team_id": self.team_id, + "team_name": self.team_name, + "bot_token": self.bot_token, + "bot_id": self.bot_id, + "bot_user_id": self.bot_user_id, + "bot_scopes": ",".join(self.bot_scopes) if self.bot_scopes else None, + "bot_refresh_token": self.bot_refresh_token, + "bot_token_expires_at": ( + datetime.fromtimestamp(self.bot_token_expires_at, tz=timezone.utc) + if self.bot_token_expires_at is not None + else None + ), + "is_enterprise_install": self.is_enterprise_install, + "installed_at": datetime.fromtimestamp(self.installed_at, tz=timezone.utc), + } + + def to_dict_for_copying(self) -> Dict[str, Any]: + return {"custom_values": self.custom_values, **self._to_standard_value_dict()} + + def to_dict(self) -> Dict[str, Any]: + # prioritize standard_values over custom_values + # when the same keys exist in both + return {**self.custom_values, **self._to_standard_value_dict()} diff --git a/slack_sdk/oauth/installation_store/models/installation.py b/slack_sdk/oauth/installation_store/models/installation.py new file mode 100644 index 000000000..18ca8e0b1 --- /dev/null +++ b/slack_sdk/oauth/installation_store/models/installation.py @@ -0,0 +1,204 @@ +from datetime import datetime, timezone +from time import time +from typing import Optional, Union, Dict, Any, Sequence + +from slack_sdk.oauth.installation_store.internals import _timestamp_to_type +from slack_sdk.oauth.installation_store.models.bot import Bot + + +class Installation: + app_id: Optional[str] + enterprise_id: Optional[str] + enterprise_name: Optional[str] + enterprise_url: Optional[str] + team_id: Optional[str] + team_name: Optional[str] + bot_token: Optional[str] + bot_id: Optional[str] + bot_user_id: Optional[str] + bot_scopes: Optional[Sequence[str]] + bot_refresh_token: Optional[str] # only when token rotation is enabled + # only when token rotation is enabled + # Unix time (seconds): only when token rotation is enabled + bot_token_expires_at: Optional[int] + user_id: str + user_token: Optional[str] + user_scopes: Optional[Sequence[str]] + user_refresh_token: Optional[str] # only when token rotation is enabled + # Unix time (seconds): only when token rotation is enabled + user_token_expires_at: Optional[int] + incoming_webhook_url: Optional[str] + incoming_webhook_channel: Optional[str] + incoming_webhook_channel_id: Optional[str] + incoming_webhook_configuration_url: Optional[str] + is_enterprise_install: bool + token_type: Optional[str] + installed_at: float + + custom_values: Dict[str, Any] + + def __init__( + self, + *, + app_id: Optional[str] = None, + # org / workspace + enterprise_id: Optional[str] = None, + enterprise_name: Optional[str] = None, + enterprise_url: Optional[str] = None, + team_id: Optional[str] = None, + team_name: Optional[str] = None, + # bot + bot_token: Optional[str] = None, + bot_id: Optional[str] = None, + bot_user_id: Optional[str] = None, + bot_scopes: Union[str, Sequence[str]] = "", + bot_refresh_token: Optional[str] = None, # only when token rotation is enabled + # only when token rotation is enabled + bot_token_expires_in: Optional[int] = None, + # only for duplicating this object + # only when token rotation is enabled + bot_token_expires_at: Optional[Union[int, datetime, str]] = None, + # installer + user_id: str, + user_token: Optional[str] = None, + user_scopes: Union[str, Sequence[str]] = "", + user_refresh_token: Optional[str] = None, # only when token rotation is enabled + # only when token rotation is enabled + user_token_expires_in: Optional[int] = None, + # only for duplicating this object + # only when token rotation is enabled + user_token_expires_at: Optional[Union[int, datetime, str]] = None, + # incoming webhook + incoming_webhook_url: Optional[str] = None, + incoming_webhook_channel: Optional[str] = None, + incoming_webhook_channel_id: Optional[str] = None, + incoming_webhook_configuration_url: Optional[str] = None, + # org app + is_enterprise_install: Optional[bool] = False, + token_type: Optional[str] = None, + # timestamps + # The expected value type is float but the internals handle other types too + # for str values, we supports only ISO datetime format. + installed_at: Optional[Union[float, datetime, str]] = None, + # custom values + custom_values: Optional[Dict[str, Any]] = None, + ): + self.app_id = app_id + self.enterprise_id = enterprise_id + self.enterprise_name = enterprise_name + self.enterprise_url = enterprise_url + self.team_id = team_id + self.team_name = team_name + self.bot_token = bot_token + self.bot_id = bot_id + self.bot_user_id = bot_user_id + if isinstance(bot_scopes, str): + self.bot_scopes = bot_scopes.split(",") if len(bot_scopes) > 0 else [] + else: + self.bot_scopes = bot_scopes + self.bot_refresh_token = bot_refresh_token + + if bot_token_expires_at is not None: + self.bot_token_expires_at = _timestamp_to_type(bot_token_expires_at, int) + elif bot_token_expires_in is not None: + self.bot_token_expires_at = int(time()) + bot_token_expires_in + else: + self.bot_token_expires_at = None + + self.user_id = user_id + self.user_token = user_token + if isinstance(user_scopes, str): + self.user_scopes = user_scopes.split(",") if len(user_scopes) > 0 else [] + else: + self.user_scopes = user_scopes + self.user_refresh_token = user_refresh_token + + if user_token_expires_at is not None: + self.user_token_expires_at = _timestamp_to_type(user_token_expires_at, int) + elif user_token_expires_in is not None: + self.user_token_expires_at = int(time()) + user_token_expires_in + else: + self.user_token_expires_at = None + + self.incoming_webhook_url = incoming_webhook_url + self.incoming_webhook_channel = incoming_webhook_channel + self.incoming_webhook_channel_id = incoming_webhook_channel_id + self.incoming_webhook_configuration_url = incoming_webhook_configuration_url + + self.is_enterprise_install = is_enterprise_install or False + self.token_type = token_type + + if installed_at is None: + self.installed_at = datetime.now().timestamp() + else: + self.installed_at = _timestamp_to_type(installed_at, float) + + self.custom_values = custom_values if custom_values is not None else {} + + def to_bot(self) -> Bot: + return Bot( + app_id=self.app_id, + enterprise_id=self.enterprise_id, + enterprise_name=self.enterprise_name, + team_id=self.team_id, + team_name=self.team_name, + bot_token=self.bot_token, # type: ignore[arg-type] + bot_id=self.bot_id, # type: ignore[arg-type] + bot_user_id=self.bot_user_id, # type: ignore[arg-type] + bot_scopes=self.bot_scopes, # type: ignore[arg-type] + bot_refresh_token=self.bot_refresh_token, + bot_token_expires_at=self.bot_token_expires_at, + is_enterprise_install=self.is_enterprise_install, + installed_at=self.installed_at, + custom_values=self.custom_values, + ) + + def set_custom_value(self, name: str, value: Any): + self.custom_values[name] = value + + def get_custom_value(self, name: str) -> Optional[Any]: + return self.custom_values.get(name) + + def _to_standard_value_dict(self) -> Dict[str, Any]: + return { + "app_id": self.app_id, + "enterprise_id": self.enterprise_id, + "enterprise_name": self.enterprise_name, + "enterprise_url": self.enterprise_url, + "team_id": self.team_id, + "team_name": self.team_name, + "bot_token": self.bot_token, + "bot_id": self.bot_id, + "bot_user_id": self.bot_user_id, + "bot_scopes": ",".join(self.bot_scopes) if self.bot_scopes else None, + "bot_refresh_token": self.bot_refresh_token, + "bot_token_expires_at": ( + datetime.fromtimestamp(self.bot_token_expires_at, tz=timezone.utc) + if self.bot_token_expires_at is not None + else None + ), + "user_id": self.user_id, + "user_token": self.user_token, + "user_scopes": ",".join(self.user_scopes) if self.user_scopes else None, + "user_refresh_token": self.user_refresh_token, + "user_token_expires_at": ( + datetime.fromtimestamp(self.user_token_expires_at, tz=timezone.utc) + if self.user_token_expires_at is not None + else None + ), + "incoming_webhook_url": self.incoming_webhook_url, + "incoming_webhook_channel": self.incoming_webhook_channel, + "incoming_webhook_channel_id": self.incoming_webhook_channel_id, + "incoming_webhook_configuration_url": self.incoming_webhook_configuration_url, + "is_enterprise_install": self.is_enterprise_install, + "token_type": self.token_type, + "installed_at": datetime.fromtimestamp(self.installed_at, tz=timezone.utc), + } + + def to_dict_for_copying(self) -> Dict[str, Any]: + return {"custom_values": self.custom_values, **self._to_standard_value_dict()} + + def to_dict(self) -> Dict[str, Any]: + # prioritize standard_values over custom_values + # when the same keys exist in both + return {**self.custom_values, **self._to_standard_value_dict()} diff --git a/slack_sdk/oauth/installation_store/sqlalchemy/__init__.py b/slack_sdk/oauth/installation_store/sqlalchemy/__init__.py new file mode 100644 index 000000000..f629deead --- /dev/null +++ b/slack_sdk/oauth/installation_store/sqlalchemy/__init__.py @@ -0,0 +1,611 @@ +import logging +from logging import Logger +from typing import Optional + +import sqlalchemy +from sqlalchemy import ( + Table, + Column, + Integer, + String, + DateTime, + Index, + and_, + desc, + MetaData, +) +from sqlalchemy.engine import Engine +from sqlalchemy.sql.sqltypes import Boolean +from sqlalchemy.ext.asyncio import AsyncEngine +from slack_sdk.oauth.installation_store.installation_store import InstallationStore +from slack_sdk.oauth.installation_store.models.bot import Bot +from slack_sdk.oauth.installation_store.models.installation import Installation +from slack_sdk.oauth.installation_store.async_installation_store import ( + AsyncInstallationStore, +) + + +class SQLAlchemyInstallationStore(InstallationStore): + default_bots_table_name: str = "slack_bots" + default_installations_table_name: str = "slack_installations" + + client_id: str + engine: Engine + metadata: MetaData + installations: Table + + @classmethod + def build_installations_table(cls, metadata: MetaData, table_name: str) -> Table: + return sqlalchemy.Table( + table_name, + metadata, + Column("id", Integer, primary_key=True, autoincrement=True), + Column("client_id", String(32), nullable=False), + Column("app_id", String(32), nullable=False), + Column("enterprise_id", String(32)), + Column("enterprise_name", String(200)), + Column("enterprise_url", String(200)), + Column("team_id", String(32)), + Column("team_name", String(200)), + Column("bot_token", String(200)), + Column("bot_id", String(32)), + Column("bot_user_id", String(32)), + Column("bot_scopes", String(1000)), + Column("bot_refresh_token", String(200)), # added in v3.8.0 + Column("bot_token_expires_at", DateTime), # added in v3.8.0 + Column("user_id", String(32), nullable=False), + Column("user_token", String(200)), + Column("user_scopes", String(1000)), + Column("user_refresh_token", String(200)), # added in v3.8.0 + Column("user_token_expires_at", DateTime), # added in v3.8.0 + Column("incoming_webhook_url", String(200)), + Column("incoming_webhook_channel", String(200)), + Column("incoming_webhook_channel_id", String(200)), + Column("incoming_webhook_configuration_url", String(200)), + Column("is_enterprise_install", Boolean, default=False, nullable=False), + Column("token_type", String(32)), + Column( + "installed_at", + DateTime, + nullable=False, + default=sqlalchemy.sql.func.now(), + ), + Index( + f"{table_name}_idx", + "client_id", + "enterprise_id", + "team_id", + "user_id", + "installed_at", + ), + ) + + @classmethod + def build_bots_table(cls, metadata: MetaData, table_name: str) -> Table: + return Table( + table_name, + metadata, + Column("id", Integer, primary_key=True, autoincrement=True), + Column("client_id", String(32), nullable=False), + Column("app_id", String(32), nullable=False), + Column("enterprise_id", String(32)), + Column("enterprise_name", String(200)), + Column("team_id", String(32)), + Column("team_name", String(200)), + Column("bot_token", String(200)), + Column("bot_id", String(32)), + Column("bot_user_id", String(32)), + Column("bot_scopes", String(1000)), + Column("bot_refresh_token", String(200)), # added in v3.8.0 + Column("bot_token_expires_at", DateTime), # added in v3.8.0 + Column("is_enterprise_install", Boolean, default=False, nullable=False), + Column( + "installed_at", + DateTime, + nullable=False, + default=sqlalchemy.sql.func.now(), + ), + Index( + f"{table_name}_idx", + "client_id", + "enterprise_id", + "team_id", + "installed_at", + ), + ) + + def __init__( + self, + client_id: str, + engine: Engine, + bots_table_name: str = default_bots_table_name, + installations_table_name: str = default_installations_table_name, + logger: Logger = logging.getLogger(__name__), + ): + self.metadata = sqlalchemy.MetaData() + self.bots = self.build_bots_table(metadata=self.metadata, table_name=bots_table_name) + self.installations = self.build_installations_table(metadata=self.metadata, table_name=installations_table_name) + self.client_id = client_id + self._logger = logger + self.engine = engine + + def create_tables(self): + self.metadata.create_all(self.engine) + + @property + def logger(self) -> Logger: + return self._logger + + def save(self, installation: Installation): + with self.engine.begin() as conn: + i = installation.to_dict() + i["client_id"] = self.client_id + + i_column = self.installations.c + installations_rows = conn.execute( + sqlalchemy.select(i_column.id) + .where( + and_( + i_column.client_id == self.client_id, + i_column.enterprise_id == installation.enterprise_id, + i_column.team_id == installation.team_id, + i_column.installed_at == i.get("installed_at"), + ) + ) + .limit(1) + ) + installations_row_id: Optional[str] = None + for row in installations_rows.mappings(): + installations_row_id = row["id"] + if installations_row_id is None: + conn.execute(self.installations.insert(), i) + else: + update_statement = self.installations.update().where(i_column.id == installations_row_id).values(**i) + conn.execute(update_statement, i) + + # bots + self.save_bot(installation.to_bot()) + + def save_bot(self, bot: Bot): + with self.engine.begin() as conn: + # bots + b = bot.to_dict() + b["client_id"] = self.client_id + + b_column = self.bots.c + bots_rows = conn.execute( + sqlalchemy.select(b_column.id) + .where( + and_( + b_column.client_id == self.client_id, + b_column.enterprise_id == bot.enterprise_id, + b_column.team_id == bot.team_id, + b_column.installed_at == b.get("installed_at"), + ) + ) + .limit(1) + ) + bots_row_id: Optional[str] = None + for row in bots_rows.mappings(): + bots_row_id = row["id"] + if bots_row_id is None: + conn.execute(self.bots.insert(), b) + else: + update_statement = self.bots.update().where(b_column.id == bots_row_id).values(**b) + conn.execute(update_statement, b) + + def find_bot( + self, + *, + enterprise_id: Optional[str], + team_id: Optional[str], + is_enterprise_install: Optional[bool] = False, + ) -> Optional[Bot]: + if is_enterprise_install or team_id is None: + team_id = None + + c = self.bots.c + query = ( + self.bots.select() + .where( + and_( + c.client_id == self.client_id, + c.enterprise_id == enterprise_id, + c.team_id == team_id, + c.bot_token.is_not(None), # the latest one that has a bot token + ) + ) + .order_by(desc(c.installed_at)) + .limit(1) + ) + + with self.engine.connect() as conn: + result: object = conn.execute(query) + for row in result.mappings(): # type: ignore[attr-defined] + return self.build_bot_entity(row) + return None + + def find_installation( + self, + *, + enterprise_id: Optional[str], + team_id: Optional[str], + user_id: Optional[str] = None, + is_enterprise_install: Optional[bool] = False, + ) -> Optional[Installation]: + if is_enterprise_install or team_id is None: + team_id = None + + c = self.installations.c + where_clause = and_( + c.client_id == self.client_id, + c.enterprise_id == enterprise_id, + c.team_id == team_id, + ) + if user_id is not None: + where_clause = and_( + c.client_id == self.client_id, + c.enterprise_id == enterprise_id, + c.team_id == team_id, + c.user_id == user_id, + ) + + query = self.installations.select().where(where_clause).order_by(desc(c.installed_at)).limit(1) + + installation: Optional[Installation] = None + with self.engine.connect() as conn: + result: object = conn.execute(query) + for row in result.mappings(): # type: ignore[attr-defined] + installation = self.build_installation_entity(row) + + has_user_installation = user_id is not None and installation is not None + no_bot_token_installation = installation is not None and installation.bot_token is None + should_find_bot_installation = has_user_installation or no_bot_token_installation + if should_find_bot_installation: + # Retrieve the latest bot token, just in case + # See also: https://github.com/slackapi/bolt-python/issues/664 + latest_bot_installation = self.find_bot( + enterprise_id=enterprise_id, + team_id=team_id, + is_enterprise_install=is_enterprise_install, + ) + if ( + latest_bot_installation is not None + and installation is not None + and installation.bot_token != latest_bot_installation.bot_token + ): + installation.bot_id = latest_bot_installation.bot_id + installation.bot_user_id = latest_bot_installation.bot_user_id + installation.bot_token = latest_bot_installation.bot_token + installation.bot_scopes = latest_bot_installation.bot_scopes + installation.bot_refresh_token = latest_bot_installation.bot_refresh_token + installation.bot_token_expires_at = latest_bot_installation.bot_token_expires_at + + return installation + + def delete_bot(self, *, enterprise_id: Optional[str], team_id: Optional[str]) -> None: + table = self.bots + c = table.c + with self.engine.begin() as conn: + deletion = table.delete().where( + and_( + c.client_id == self.client_id, + c.enterprise_id == enterprise_id, + c.team_id == team_id, + ) + ) + conn.execute(deletion) + + def delete_installation( + self, + *, + enterprise_id: Optional[str], + team_id: Optional[str], + user_id: Optional[str] = None, + ) -> None: + table = self.installations + c = table.c + with self.engine.begin() as conn: + if user_id is not None: + deletion = table.delete().where( + and_( + c.client_id == self.client_id, + c.enterprise_id == enterprise_id, + c.team_id == team_id, + c.user_id == user_id, + ) + ) + conn.execute(deletion) + else: + deletion = table.delete().where( + and_( + c.client_id == self.client_id, + c.enterprise_id == enterprise_id, + c.team_id == team_id, + ) + ) + conn.execute(deletion) + + @classmethod + def build_installation_entity(cls, row) -> Installation: + return Installation( + app_id=row["app_id"], + enterprise_id=row["enterprise_id"], + enterprise_name=row["enterprise_name"], + enterprise_url=row["enterprise_url"], + team_id=row["team_id"], + team_name=row["team_name"], + bot_token=row["bot_token"], + bot_id=row["bot_id"], + bot_user_id=row["bot_user_id"], + bot_scopes=row["bot_scopes"], + bot_refresh_token=row["bot_refresh_token"], + bot_token_expires_at=row["bot_token_expires_at"], + user_id=row["user_id"], + user_token=row["user_token"], + user_scopes=row["user_scopes"], + user_refresh_token=row["user_refresh_token"], + user_token_expires_at=row["user_token_expires_at"], + # Only the incoming webhook issued in the latest installation is set in this logic + incoming_webhook_url=row["incoming_webhook_url"], + incoming_webhook_channel=row["incoming_webhook_channel"], + incoming_webhook_channel_id=row["incoming_webhook_channel_id"], + incoming_webhook_configuration_url=row["incoming_webhook_configuration_url"], + is_enterprise_install=row["is_enterprise_install"], + token_type=row["token_type"], + installed_at=row["installed_at"], + ) + + @classmethod + def build_bot_entity(cls, row) -> Bot: + return Bot( + app_id=row["app_id"], + enterprise_id=row["enterprise_id"], + enterprise_name=row["enterprise_name"], + team_id=row["team_id"], + team_name=row["team_name"], + bot_token=row["bot_token"], + bot_id=row["bot_id"], + bot_user_id=row["bot_user_id"], + bot_scopes=row["bot_scopes"], + bot_refresh_token=row["bot_refresh_token"], + bot_token_expires_at=row["bot_token_expires_at"], + is_enterprise_install=row["is_enterprise_install"], + installed_at=row["installed_at"], + ) + + +class AsyncSQLAlchemyInstallationStore(AsyncInstallationStore): + default_bots_table_name: str = "slack_bots" + default_installations_table_name: str = "slack_installations" + + client_id: str + engine: AsyncEngine + metadata: MetaData + installations: Table + + def __init__( + self, + client_id: str, + engine: AsyncEngine, + bots_table_name: str = default_bots_table_name, + installations_table_name: str = default_installations_table_name, + logger: Logger = logging.getLogger(__name__), + ): + self.metadata = sqlalchemy.MetaData() + self.bots = self.build_bots_table(metadata=self.metadata, table_name=bots_table_name) + self.installations = self.build_installations_table(metadata=self.metadata, table_name=installations_table_name) + self.client_id = client_id + self._logger = logger + self.engine = engine + + @classmethod + def build_installations_table(cls, metadata: MetaData, table_name: str) -> Table: + return SQLAlchemyInstallationStore.build_installations_table(metadata, table_name) + + @classmethod + def build_bots_table(cls, metadata: MetaData, table_name: str) -> Table: + return SQLAlchemyInstallationStore.build_bots_table(metadata, table_name) + + async def create_tables(self): + async with self.engine.begin() as conn: + await conn.run_sync(self.metadata.create_all) + + @property + def logger(self) -> Logger: + return self._logger + + async def async_save(self, installation: Installation): + async with self.engine.begin() as conn: + i = installation.to_dict() + i["client_id"] = self.client_id + + i_column = self.installations.c + installations_rows = await conn.execute( + sqlalchemy.select(i_column.id) + .where( + and_( + i_column.client_id == self.client_id, + i_column.enterprise_id == installation.enterprise_id, + i_column.team_id == installation.team_id, + i_column.installed_at == i.get("installed_at"), + ) + ) + .limit(1) + ) + installations_row_id: Optional[str] = None + for row in installations_rows.mappings(): + installations_row_id = row["id"] + if installations_row_id is None: + await conn.execute(self.installations.insert(), i) + else: + update_statement = self.installations.update().where(i_column.id == installations_row_id).values(**i) + await conn.execute(update_statement, i) + + # bots + await self.async_save_bot(installation.to_bot()) + + async def async_save_bot(self, bot: Bot): + async with self.engine.begin() as conn: + # bots + b = bot.to_dict() + b["client_id"] = self.client_id + + b_column = self.bots.c + bots_rows = await conn.execute( + sqlalchemy.select(b_column.id) + .where( + and_( + b_column.client_id == self.client_id, + b_column.enterprise_id == bot.enterprise_id, + b_column.team_id == bot.team_id, + b_column.installed_at == b.get("installed_at"), + ) + ) + .limit(1) + ) + bots_row_id: Optional[str] = None + for row in bots_rows.mappings(): + bots_row_id = row["id"] + if bots_row_id is None: + await conn.execute(self.bots.insert(), b) + else: + update_statement = self.bots.update().where(b_column.id == bots_row_id).values(**b) + await conn.execute(update_statement, b) + + async def async_find_bot( + self, + *, + enterprise_id: Optional[str], + team_id: Optional[str], + is_enterprise_install: Optional[bool] = False, + ) -> Optional[Bot]: + if is_enterprise_install or team_id is None: + team_id = None + + c = self.bots.c + query = ( + self.bots.select() + .where( + and_( + c.client_id == self.client_id, + c.enterprise_id == enterprise_id, + c.team_id == team_id, + c.bot_token.is_not(None), # the latest one that has a bot token + ) + ) + .order_by(desc(c.installed_at)) + .limit(1) + ) + + async with self.engine.connect() as conn: + result: object = await conn.execute(query) + for row in result.mappings(): # type: ignore[attr-defined] + return SQLAlchemyInstallationStore.build_bot_entity(row) + return None + + async def async_find_installation( + self, + *, + enterprise_id: Optional[str], + team_id: Optional[str], + user_id: Optional[str] = None, + is_enterprise_install: Optional[bool] = False, + ) -> Optional[Installation]: + if is_enterprise_install or team_id is None: + team_id = None + + c = self.installations.c + where_clause = and_( + c.client_id == self.client_id, + c.enterprise_id == enterprise_id, + c.team_id == team_id, + ) + if user_id is not None: + where_clause = and_( + c.client_id == self.client_id, + c.enterprise_id == enterprise_id, + c.team_id == team_id, + c.user_id == user_id, + ) + + query = self.installations.select().where(where_clause).order_by(desc(c.installed_at)).limit(1) + + installation: Optional[Installation] = None + async with self.engine.connect() as conn: + result: object = await conn.execute(query) + for row in result.mappings(): # type: ignore[attr-defined] + installation = SQLAlchemyInstallationStore.build_installation_entity(row) + + has_user_installation = user_id is not None and installation is not None + no_bot_token_installation = installation is not None and installation.bot_token is None + should_find_bot_installation = has_user_installation or no_bot_token_installation + if should_find_bot_installation: + # Retrieve the latest bot token, just in case + # See also: https://github.com/slackapi/bolt-python/issues/664 + latest_bot_installation = await self.async_find_bot( + enterprise_id=enterprise_id, + team_id=team_id, + is_enterprise_install=is_enterprise_install, + ) + if ( + latest_bot_installation is not None + and installation is not None + and installation.bot_token != latest_bot_installation.bot_token + ): + installation.bot_id = latest_bot_installation.bot_id + installation.bot_user_id = latest_bot_installation.bot_user_id + installation.bot_token = latest_bot_installation.bot_token + installation.bot_scopes = latest_bot_installation.bot_scopes + installation.bot_refresh_token = latest_bot_installation.bot_refresh_token + installation.bot_token_expires_at = latest_bot_installation.bot_token_expires_at + + return installation + + async def async_delete_bot( + self, + *, + enterprise_id: Optional[str], + team_id: Optional[str], + ) -> None: + table = self.bots + c = table.c + async with self.engine.begin() as conn: + deletion = table.delete().where( + and_( + c.client_id == self.client_id, + c.enterprise_id == enterprise_id, + c.team_id == team_id, + ) + ) + await conn.execute(deletion) + + async def async_delete_installation( + self, + *, + enterprise_id: Optional[str], + team_id: Optional[str], + user_id: Optional[str] = None, + ) -> None: + table = self.installations + c = table.c + async with self.engine.begin() as conn: + if user_id is not None: + deletion = table.delete().where( + and_( + c.client_id == self.client_id, + c.enterprise_id == enterprise_id, + c.team_id == team_id, + c.user_id == user_id, + ) + ) + await conn.execute(deletion) + else: + deletion = table.delete().where( + and_( + c.client_id == self.client_id, + c.enterprise_id == enterprise_id, + c.team_id == team_id, + ) + ) + await conn.execute(deletion) diff --git a/slack_sdk/oauth/installation_store/sqlite3/__init__.py b/slack_sdk/oauth/installation_store/sqlite3/__init__.py new file mode 100644 index 000000000..12acd05c7 --- /dev/null +++ b/slack_sdk/oauth/installation_store/sqlite3/__init__.py @@ -0,0 +1,615 @@ +import logging +import sqlite3 +from logging import Logger +from sqlite3 import Connection +from typing import Optional + +from slack_sdk.oauth.installation_store.async_installation_store import ( + AsyncInstallationStore, +) +from slack_sdk.oauth.installation_store.installation_store import InstallationStore +from slack_sdk.oauth.installation_store.models.bot import Bot +from slack_sdk.oauth.installation_store.models.installation import Installation + + +class SQLite3InstallationStore(InstallationStore, AsyncInstallationStore): + def __init__( + self, + *, + database: str, + client_id: str, + logger: Logger = logging.getLogger(__name__), + ): + self.database = database + self.client_id = client_id + self.init_called = False + self._logger = logger + + @property + def logger(self) -> Logger: + if self._logger is None: + self._logger = logging.getLogger(__name__) + return self._logger + + def init(self): + try: + with sqlite3.connect(database=self.database) as conn: + cur = conn.execute("select count(1) from slack_installations;") + row_num = cur.fetchone()[0] + self.logger.debug(f"{row_num} installations are stored in {self.database}") + except Exception: + self.create_tables() + self.init_called = True + + def connect(self) -> Connection: + if not self.init_called: + self.init() + return sqlite3.connect(database=self.database) + + def create_tables(self): + with sqlite3.connect(database=self.database) as conn: + conn.execute( + """ + create table slack_installations ( + id integer primary key autoincrement, + client_id text not null, + app_id text not null, + enterprise_id text not null default '', + enterprise_name text, + enterprise_url text, + team_id text not null default '', + team_name text, + bot_token text, + bot_id text, + bot_user_id text, + bot_scopes text, + bot_refresh_token text, -- since v3.8 + bot_token_expires_at datetime, -- since v3.8 + user_id text not null, + user_token text, + user_scopes text, + user_refresh_token text, -- since v3.8 + user_token_expires_at datetime, -- since v3.8 + incoming_webhook_url text, + incoming_webhook_channel text, + incoming_webhook_channel_id text, + incoming_webhook_configuration_url text, + is_enterprise_install boolean not null default 0, + token_type text, + installed_at datetime not null default current_timestamp + ); + """ + ) + conn.execute( + """ + create index slack_installations_idx on slack_installations ( + client_id, + enterprise_id, + team_id, + user_id, + installed_at + ); + """ + ) + conn.execute( + """ + create table slack_bots ( + id integer primary key autoincrement, + client_id text not null, + app_id text not null, + enterprise_id text not null default '', + enterprise_name text, + team_id text not null default '', + team_name text, + bot_token text not null, + bot_id text not null, + bot_user_id text not null, + bot_scopes text, + bot_refresh_token text, -- since v3.8 + bot_token_expires_at datetime, -- since v3.8 + is_enterprise_install boolean not null default 0, + installed_at datetime not null default current_timestamp + ); + """ + ) + conn.execute( + """ + create index slack_bots_idx on slack_bots ( + client_id, + enterprise_id, + team_id, + installed_at + ); + """ + ) + self.logger.debug(f"Tables have been created (database: {self.database})") + conn.commit() + + async def async_save(self, installation: Installation): + return self.save(installation) + + async def async_save_bot(self, bot: Bot): + return self.save_bot(bot) + + def save(self, installation: Installation): + with self.connect() as conn: + conn.execute( + """ + insert into slack_installations ( + client_id, + app_id, + enterprise_id, + enterprise_name, + enterprise_url, + team_id, + team_name, + bot_token, + bot_id, + bot_user_id, + bot_scopes, + bot_refresh_token, -- since v3.8 + bot_token_expires_at, -- since v3.8 + user_id, + user_token, + user_scopes, + user_refresh_token, -- since v3.8 + user_token_expires_at, -- since v3.8 + incoming_webhook_url, + incoming_webhook_channel, + incoming_webhook_channel_id, + incoming_webhook_configuration_url, + is_enterprise_install, + token_type + ) + values + ( + ?, + ?, + ?, + ?, + ?, + ?, + ?, + ?, + ?, + ?, + ?, + ?, + ?, + ?, + ?, + ?, + ?, + ?, + ?, + ?, + ?, + ?, + ?, + ? + ); + """, + [ + self.client_id, + installation.app_id, + installation.enterprise_id or "", + installation.enterprise_name, + installation.enterprise_url, + installation.team_id or "", + installation.team_name, + installation.bot_token, + installation.bot_id, + installation.bot_user_id, + ",".join(installation.bot_scopes), # type: ignore[arg-type] + installation.bot_refresh_token, + installation.bot_token_expires_at, + installation.user_id, + installation.user_token, + ",".join(installation.user_scopes) if installation.user_scopes else None, + installation.user_refresh_token, + installation.user_token_expires_at, + installation.incoming_webhook_url, + installation.incoming_webhook_channel, + installation.incoming_webhook_channel_id, + installation.incoming_webhook_configuration_url, + 1 if installation.is_enterprise_install else 0, + installation.token_type, + ], + ) + self.logger.debug( + f"New rows in slack_bots and slack_installations have been created (database: {self.database})" + ) + conn.commit() + + self.save_bot(installation.to_bot()) + + def save_bot(self, bot: Bot): + if bot.bot_token is None: + self.logger.debug("Skipped saving a new row because of the absense of bot token in it") + return + + with self.connect() as conn: + conn.execute( + """ + insert into slack_bots ( + client_id, + app_id, + enterprise_id, + enterprise_name, + team_id, + team_name, + bot_token, + bot_id, + bot_user_id, + bot_scopes, + bot_refresh_token, -- since v3.8 + bot_token_expires_at, -- since v3.8 + is_enterprise_install + ) + values + ( + ?, + ?, + ?, + ?, + ?, + ?, + ?, + ?, + ?, + ?, + ?, + ?, + ? + ); + """, + [ + self.client_id, + bot.app_id, + bot.enterprise_id or "", + bot.enterprise_name, + bot.team_id or "", + bot.team_name, + bot.bot_token, + bot.bot_id, + bot.bot_user_id, + ",".join(bot.bot_scopes), + bot.bot_refresh_token, + bot.bot_token_expires_at, + bot.is_enterprise_install, + ], + ) + conn.commit() + + async def async_find_bot( + self, + *, + enterprise_id: Optional[str], + team_id: Optional[str], + is_enterprise_install: Optional[bool] = False, + ) -> Optional[Bot]: + return self.find_bot( + enterprise_id=enterprise_id, + team_id=team_id, + is_enterprise_install=is_enterprise_install, + ) + + def find_bot( + self, + *, + enterprise_id: Optional[str], + team_id: Optional[str], + is_enterprise_install: Optional[bool] = False, + ) -> Optional[Bot]: + if is_enterprise_install or team_id is None: + team_id = "" + + try: + with self.connect() as conn: + cur = conn.execute( + """ + select + app_id, + enterprise_id, + enterprise_name, + team_id, + team_name, + bot_token, + bot_id, + bot_user_id, + bot_scopes, + bot_refresh_token, -- since v3.8 + bot_token_expires_at, -- since v3.8 + is_enterprise_install, + installed_at + from + slack_bots + where + client_id = ? + and + enterprise_id = ? + and + team_id = ? + order by installed_at desc + limit 1 + """, + [self.client_id, enterprise_id or "", team_id or ""], + ) + row = cur.fetchone() + result = "found" if row and len(row) > 0 else "not found" + self.logger.debug(f"find_bot's query result: {result} (database: {self.database})") + if row and len(row) > 0: + bot = Bot( + app_id=row[0], + enterprise_id=row[1], + enterprise_name=row[2], + team_id=row[3], + team_name=row[4], + bot_token=row[5], + bot_id=row[6], + bot_user_id=row[7], + bot_scopes=row[8], + bot_refresh_token=row[9], + bot_token_expires_at=row[10], + is_enterprise_install=row[11], + installed_at=row[12], + ) + return bot + return None + + except Exception as e: + message = f"Failed to find bot installation data for enterprise: {enterprise_id}, team: {team_id}: {e}" + if self.logger.level <= logging.DEBUG: + self.logger.exception(message) + else: + self.logger.warning(message) + return None + + async def async_find_installation( + self, + *, + enterprise_id: Optional[str], + team_id: Optional[str], + user_id: Optional[str] = None, + is_enterprise_install: Optional[bool] = False, + ) -> Optional[Installation]: + return self.find_installation( + enterprise_id=enterprise_id, + team_id=team_id, + user_id=user_id, + is_enterprise_install=is_enterprise_install, + ) + + def find_installation( + self, + *, + enterprise_id: Optional[str], + team_id: Optional[str], + user_id: Optional[str] = None, + is_enterprise_install: Optional[bool] = False, + ) -> Optional[Installation]: + if is_enterprise_install or team_id is None: + team_id = "" + + try: + with self.connect() as conn: + row = None + columns = """ + app_id, + enterprise_id, + enterprise_name, + enterprise_url, + team_id, + team_name, + bot_token, + bot_id, + bot_user_id, + bot_scopes, + bot_refresh_token, -- since v3.8 + bot_token_expires_at, -- since v3.8 + user_id, + user_token, + user_scopes, + user_refresh_token, -- since v3.8 + user_token_expires_at, -- since v3.8 + incoming_webhook_url, + incoming_webhook_channel, + incoming_webhook_channel_id, + incoming_webhook_configuration_url, + is_enterprise_install, + token_type, + installed_at + """ + if user_id is None: + cur = conn.execute( + f""" + select + {columns} + from + slack_installations + where + client_id = ? + and + enterprise_id = ? + and + team_id = ? + order by installed_at desc + limit 1 + """, + [self.client_id, enterprise_id or "", team_id], + ) + row = cur.fetchone() + else: + cur = conn.execute( + f""" + select + {columns} + from + slack_installations + where + client_id = ? + and + enterprise_id = ? + and + team_id = ? + and + user_id = ? + order by installed_at desc + limit 1 + """, + [self.client_id, enterprise_id or "", team_id, user_id], + ) + row = cur.fetchone() + + if row is None: + return None + + result = "found" if row and len(row) > 0 else "not found" + self.logger.debug(f"find_installation's query result: {result} (database: {self.database})") + if row and len(row) > 0: + installation = Installation( + app_id=row[0], + enterprise_id=row[1], + enterprise_name=row[2], + enterprise_url=row[3], + team_id=row[4], + team_name=row[5], + bot_token=row[6], + bot_id=row[7], + bot_user_id=row[8], + bot_scopes=row[9], + bot_refresh_token=row[10], + bot_token_expires_at=row[11], + user_id=row[12], + user_token=row[13], + user_scopes=row[14], + user_refresh_token=row[15], + user_token_expires_at=row[16], + incoming_webhook_url=row[17], + incoming_webhook_channel=row[18], + incoming_webhook_channel_id=row[19], + incoming_webhook_configuration_url=row[20], + is_enterprise_install=row[21], + token_type=row[22], + installed_at=row[23], + ) + + if user_id is not None: + # Retrieve the latest bot token, just in case + # See also: https://github.com/slackapi/bolt-python/issues/664 + cur = conn.execute( + """ + select + bot_token, + bot_id, + bot_user_id, + bot_scopes, + bot_refresh_token, + bot_token_expires_at + from + slack_installations + where + client_id = ? + and + enterprise_id = ? + and + team_id = ? + and + bot_token is not null + order by installed_at desc + limit 1 + """, + [self.client_id, enterprise_id or "", team_id], + ) + row = cur.fetchone() + installation.bot_token = row[0] + installation.bot_id = row[1] + installation.bot_user_id = row[2] + installation.bot_scopes = row[3] + installation.bot_refresh_token = row[4] + installation.bot_token_expires_at = row[5] + + return installation + return None + + except Exception as e: + message = f"Failed to find an installation data for enterprise: {enterprise_id}, team: {team_id}: {e}" + if self.logger.level <= logging.DEBUG: + self.logger.exception(message) + else: + self.logger.warning(message) + return None + + def delete_bot(self, *, enterprise_id: Optional[str], team_id: Optional[str]) -> None: + try: + with self.connect() as conn: + conn.execute( + """ + delete + from + slack_bots + where + client_id = ? + and + enterprise_id = ? + and + team_id = ? + """, + [self.client_id, enterprise_id or "", team_id or ""], + ) + conn.commit() + except Exception as e: + message = f"Failed to delete bot installation data for enterprise: {enterprise_id}, team: {team_id}: {e}" + if self.logger.level <= logging.DEBUG: + self.logger.exception(message) + else: + self.logger.warning(message) + + def delete_installation( + self, + *, + enterprise_id: Optional[str], + team_id: Optional[str], + user_id: Optional[str] = None, + ) -> None: + try: + with self.connect() as conn: + if user_id is None: + conn.execute( + """ + delete + from + slack_installations + where + client_id = ? + and + enterprise_id = ? + and + team_id = ? + """, + [self.client_id, enterprise_id or "", team_id], + ) + else: + conn.execute( + """ + delete + from + slack_installations + where + client_id = ? + and + enterprise_id = ? + and + team_id = ? + and + user_id = ? + """, + [self.client_id, enterprise_id or "", team_id, user_id], + ) + conn.commit() + except Exception as e: + message = f"Failed to delete installation data for enterprise: {enterprise_id}, team: {team_id}: {e}" + if self.logger.level <= logging.DEBUG: + self.logger.exception(message) + else: + self.logger.warning(message) diff --git a/slack_sdk/oauth/redirect_uri_page_renderer/__init__.py b/slack_sdk/oauth/redirect_uri_page_renderer/__init__.py new file mode 100644 index 000000000..bc630953f --- /dev/null +++ b/slack_sdk/oauth/redirect_uri_page_renderer/__init__.py @@ -0,0 +1,72 @@ +import html +from typing import Optional + + +class RedirectUriPageRenderer: + def __init__( + self, + *, + install_path: str, + redirect_uri_path: str, + success_url: Optional[str] = None, + failure_url: Optional[str] = None, + ): + self.install_path = install_path + self.redirect_uri_path = redirect_uri_path + self.success_url = success_url + self.failure_url = failure_url + + def render_success_page( + self, + app_id: str, + team_id: Optional[str], + is_enterprise_install: Optional[bool] = None, + enterprise_url: Optional[str] = None, + ) -> str: + url = self.success_url + if url is None: + if is_enterprise_install is True and enterprise_url is not None and app_id is not None: + url = f"{enterprise_url}manage/organization/apps/profile/{app_id}/workspaces/add" + elif team_id is None or app_id is None: + url = "slack://open" + else: + url = f"slack://app?team={team_id}&id={app_id}" + browser_url = f"https://app.slack.com/client/{team_id}" + + return f""" + + + + + + +

Thank you!

+

Redirecting to the Slack App... click here. If you use the browser version of Slack, click this link instead.

+ + +""" # noqa: E501 + + def render_failure_page(self, reason: str) -> str: + return f""" + + + + + +

Oops, Something Went Wrong!

+

Please try again from here or contact the app owner (reason: {html.escape(reason)})

+ + +""" # noqa: E501 diff --git a/slack_sdk/oauth/state_store/__init__.py b/slack_sdk/oauth/state_store/__init__.py new file mode 100644 index 000000000..15491fd1d --- /dev/null +++ b/slack_sdk/oauth/state_store/__init__.py @@ -0,0 +1,13 @@ +"""OAuth state parameter data store + +Refer to https://docs.slack.dev/tools/python-slack-sdk/oauth for details. +""" + +# from .amazon_s3_state_store import AmazonS3OAuthStateStore +from .file import FileOAuthStateStore +from .state_store import OAuthStateStore + +__all__ = [ + "FileOAuthStateStore", + "OAuthStateStore", +] diff --git a/slack_sdk/oauth/state_store/amazon_s3/__init__.py b/slack_sdk/oauth/state_store/amazon_s3/__init__.py new file mode 100644 index 000000000..1ad590838 --- /dev/null +++ b/slack_sdk/oauth/state_store/amazon_s3/__init__.py @@ -0,0 +1,69 @@ +import logging +import time +from logging import Logger +from uuid import uuid4 + +from botocore.client import BaseClient # type: ignore[import-untyped] + +from ..async_state_store import AsyncOAuthStateStore +from ..state_store import OAuthStateStore + + +class AmazonS3OAuthStateStore(OAuthStateStore, AsyncOAuthStateStore): + def __init__( + self, + *, + s3_client: BaseClient, + bucket_name: str, + expiration_seconds: int, + logger: Logger = logging.getLogger(__name__), + ): + self.s3_client = s3_client + self.bucket_name = bucket_name + self.expiration_seconds = expiration_seconds + self._logger = logger + + @property + def logger(self) -> Logger: + if self._logger is None: + self._logger = logging.getLogger(__name__) + return self._logger + + async def async_issue(self, *args, **kwargs) -> str: + return self.issue(*args, **kwargs) + + async def async_consume(self, state: str) -> bool: + return self.consume(state) + + def issue(self, *args, **kwargs) -> str: + state = str(uuid4()) + response = self.s3_client.put_object( + Bucket=self.bucket_name, + Body=str(time.time()), + Key=state, + ) + self.logger.debug(f"S3 put_object response: {response}") + return state + + def consume(self, state: str) -> bool: + try: + fetch_response = self.s3_client.get_object( + Bucket=self.bucket_name, + Key=state, + ) + self.logger.debug(f"S3 get_object response: {fetch_response}") + body = fetch_response["Body"].read().decode("utf-8") + created = float(body) + expiration = created + self.expiration_seconds + still_valid: bool = time.time() < expiration + + deletion_response = self.s3_client.delete_object( + Bucket=self.bucket_name, + Key=state, + ) + self.logger.debug(f"S3 delete_object response: {deletion_response}") + return still_valid + except Exception as e: + message = f"Failed to find any persistent data for state: {state} - {e}" + self.logger.warning(message) + return False diff --git a/slack_sdk/oauth/state_store/async_state_store.py b/slack_sdk/oauth/state_store/async_state_store.py new file mode 100644 index 000000000..22e883bbe --- /dev/null +++ b/slack_sdk/oauth/state_store/async_state_store.py @@ -0,0 +1,13 @@ +from logging import Logger + + +class AsyncOAuthStateStore: + @property + def logger(self) -> Logger: + raise NotImplementedError() + + async def async_issue(self, *args, **kwargs) -> str: + raise NotImplementedError() + + async def async_consume(self, state: str) -> bool: + raise NotImplementedError() diff --git a/slack_sdk/oauth/state_store/file/__init__.py b/slack_sdk/oauth/state_store/file/__init__.py new file mode 100644 index 000000000..ffd619cc6 --- /dev/null +++ b/slack_sdk/oauth/state_store/file/__init__.py @@ -0,0 +1,71 @@ +import logging +import os +import time +from logging import Logger +from pathlib import Path +from typing import Union, Optional +from uuid import uuid4 + +from ..async_state_store import AsyncOAuthStateStore +from ..state_store import OAuthStateStore + + +class FileOAuthStateStore(OAuthStateStore, AsyncOAuthStateStore): + def __init__( + self, + *, + expiration_seconds: int, + base_dir: str = str(Path.home()) + "/.bolt-app-oauth-state", + client_id: Optional[str] = None, + logger: Logger = logging.getLogger(__name__), + ): + self.expiration_seconds = expiration_seconds + + self.base_dir = base_dir + self.client_id = client_id + if self.client_id is not None: + self.base_dir = f"{self.base_dir}/{self.client_id}" + self._logger = logger + + @property + def logger(self) -> Logger: + if self._logger is None: + self._logger = logging.getLogger(__name__) + return self._logger + + async def async_issue(self, *args, **kwargs) -> str: + return self.issue(*args, **kwargs) + + async def async_consume(self, state: str) -> bool: + return self.consume(state) + + def issue(self, *args, **kwargs) -> str: + state = str(uuid4()) + self._mkdir(self.base_dir) + filepath = f"{self.base_dir}/{state}" + with open(filepath, "w") as f: + content = str(time.time()) + f.write(content) + return state + + def consume(self, state: str) -> bool: + filepath = f"{self.base_dir}/{state}" + try: + with open(filepath) as f: + created = float(f.read()) + expiration = created + self.expiration_seconds + still_valid: bool = time.time() < expiration + + os.remove(filepath) # consume the file by deleting it + return still_valid + + except FileNotFoundError as e: + message = f"Failed to find any persistent data for state: {state} - {e}" + self.logger.warning(message) + return False + + @staticmethod + def _mkdir(path: Union[str, Path]): + if isinstance(path, str): + path = Path(path) + path.mkdir(parents=True, exist_ok=True) diff --git a/slack_sdk/oauth/state_store/sqlalchemy/__init__.py b/slack_sdk/oauth/state_store/sqlalchemy/__init__.py new file mode 100644 index 000000000..8bb3ec1ff --- /dev/null +++ b/slack_sdk/oauth/state_store/sqlalchemy/__init__.py @@ -0,0 +1,149 @@ +import logging +import time +from datetime import datetime, timezone +from logging import Logger +from uuid import uuid4 + +from ..state_store import OAuthStateStore +from ..async_state_store import AsyncOAuthStateStore +import sqlalchemy +from sqlalchemy import Table, Column, Integer, String, DateTime, and_, MetaData +from sqlalchemy.engine import Engine +from sqlalchemy.ext.asyncio import AsyncEngine + + +class SQLAlchemyOAuthStateStore(OAuthStateStore): + default_table_name: str = "slack_oauth_states" + + expiration_seconds: int + engine: Engine + metadata: MetaData + oauth_states: Table + + @classmethod + def build_oauth_states_table(cls, metadata: MetaData, table_name: str) -> Table: + return sqlalchemy.Table( + table_name, + metadata, + metadata, + Column("id", Integer, primary_key=True, autoincrement=True), + Column("state", String(200), nullable=False), + Column("expire_at", DateTime, nullable=False), + ) + + def __init__( + self, + expiration_seconds: int, + engine: Engine, + logger: Logger = logging.getLogger(__name__), + table_name: str = default_table_name, + ): + self.expiration_seconds = expiration_seconds + self._logger = logger + self.engine = engine + self.metadata = MetaData() + self.oauth_states = self.build_oauth_states_table(self.metadata, table_name) + + def create_tables(self): + self.metadata.create_all(self.engine) + + @property + def logger(self) -> Logger: + if self._logger is None: + self._logger = logging.getLogger(__name__) + return self._logger + + def issue(self, *args, **kwargs) -> str: + state: str = str(uuid4()) + now = datetime.fromtimestamp(time.time() + self.expiration_seconds, tz=timezone.utc) + with self.engine.begin() as conn: + conn.execute( + self.oauth_states.insert(), + {"state": state, "expire_at": now}, + ) + return state + + def consume(self, state: str) -> bool: + try: + with self.engine.begin() as conn: + c = self.oauth_states.c + query = self.oauth_states.select().where(and_(c.state == state, c.expire_at > datetime.now(tz=timezone.utc))) + result = conn.execute(query) + for row in result.mappings(): + self.logger.debug(f"consume's query result: {row}") + conn.execute(self.oauth_states.delete().where(c.id == row["id"])) + return True + return False + except Exception as e: + message = f"Failed to find any persistent data for state: {state} - {e}" + self.logger.warning(message) + return False + + +class AsyncSQLAlchemyOAuthStateStore(AsyncOAuthStateStore): + default_table_name: str = "slack_oauth_states" + + expiration_seconds: int + engine: AsyncEngine + metadata: MetaData + oauth_states: Table + + @classmethod + def build_oauth_states_table(cls, metadata: MetaData, table_name: str) -> Table: + return sqlalchemy.Table( + table_name, + metadata, + metadata, + Column("id", Integer, primary_key=True, autoincrement=True), + Column("state", String(200), nullable=False), + Column("expire_at", DateTime, nullable=False), + ) + + def __init__( + self, + expiration_seconds: int, + engine: AsyncEngine, + logger: Logger = logging.getLogger(__name__), + table_name: str = default_table_name, + ): + self.expiration_seconds = expiration_seconds + self._logger = logger + self.engine = engine + self.metadata = MetaData() + self.oauth_states = self.build_oauth_states_table(self.metadata, table_name) + + async def create_tables(self): + async with self.engine.begin() as conn: + await conn.run_sync(self.metadata.create_all) + + @property + def logger(self) -> Logger: + if self._logger is None: + self._logger = logging.getLogger(__name__) + return self._logger + + async def async_issue(self, *args, **kwargs) -> str: + state: str = str(uuid4()) + now = datetime.fromtimestamp(time.time() + self.expiration_seconds, tz=timezone.utc) + async with self.engine.begin() as conn: + await conn.execute( + self.oauth_states.insert(), + {"state": state, "expire_at": now}, + ) + return state + + async def async_consume(self, state: str) -> bool: + try: + async with self.engine.begin() as conn: + c = self.oauth_states.c + query = self.oauth_states.select().where(and_(c.state == state, c.expire_at > datetime.now(tz=timezone.utc))) + result = await conn.execute(query) + for row in result.mappings(): + self.logger.debug(f"consume's query result: {row}") + await conn.execute(self.oauth_states.delete().where(c.id == row["id"])) + return True + return False + except Exception as e: + message = f"Failed to find any persistent data for state: {state} - {e}" + self.logger.warning(message) + return False diff --git a/slack_sdk/oauth/state_store/sqlite3/__init__.py b/slack_sdk/oauth/state_store/sqlite3/__init__.py new file mode 100644 index 000000000..8a82e333f --- /dev/null +++ b/slack_sdk/oauth/state_store/sqlite3/__init__.py @@ -0,0 +1,96 @@ +import logging +import sqlite3 +import time +from logging import Logger +from sqlite3 import Connection +from uuid import uuid4 + +from ..async_state_store import AsyncOAuthStateStore +from ..state_store import OAuthStateStore + + +class SQLite3OAuthStateStore(OAuthStateStore, AsyncOAuthStateStore): + def __init__( + self, + *, + database: str, + expiration_seconds: int, + logger: Logger = logging.getLogger(__name__), + ): + self.database = database + self.expiration_seconds = expiration_seconds + self.init_called = False + self._logger = logger + + @property + def logger(self) -> Logger: + if self._logger is None: + self._logger = logging.getLogger(__name__) + return self._logger + + def init(self): + try: + with sqlite3.connect(database=self.database) as conn: + cur = conn.execute("select count(1) from oauth_states;") + row_num = cur.fetchone()[0] + self.logger.debug(f"{row_num} oauth states are stored in {self.database}") + except Exception: + self.create_tables() + self.init_called = True + + def connect(self) -> Connection: + if not self.init_called: + self.init() + return sqlite3.connect(database=self.database) + + def create_tables(self): + with sqlite3.connect(database=self.database) as conn: + conn.execute( + """ + create table oauth_states ( + id integer primary key autoincrement, + state text not null, + expire_at datetime not null + ); + """ + ) + self.logger.debug(f"Tables have been created (database: {self.database})") + conn.commit() + + async def async_issue(self, *args, **kwargs) -> str: + return self.issue(*args, **kwargs) + + async def async_consume(self, state: str) -> bool: + return self.consume(state) + + def issue(self, *args, **kwargs) -> str: + state: str = str(uuid4()) + with self.connect() as conn: + parameters = [ + state, + time.time() + self.expiration_seconds, + ] + conn.execute("insert into oauth_states (state, expire_at) values (?, ?);", parameters) + self.logger.debug(f"issue's insertion result: {parameters} (database: {self.database})") + conn.commit() + return state + + def consume(self, state: str) -> bool: + try: + with self.connect() as conn: + cur = conn.execute( + "select id, state from oauth_states where state = ? and expire_at > ?;", + [state, time.time()], + ) + row = cur.fetchone() + self.logger.debug(f"consume's query result: {row} (database: {self.database})") + if row and len(row) > 0: + id = row[0] + conn.execute("delete from oauth_states where id = ?;", [id]) + conn.commit() + return True + return False + except Exception as e: + message = f"Failed to find any persistent data for state: {state} - {e}" + self.logger.warning(message) + return False diff --git a/slack_sdk/oauth/state_store/state_store.py b/slack_sdk/oauth/state_store/state_store.py new file mode 100644 index 000000000..78fba7bb2 --- /dev/null +++ b/slack_sdk/oauth/state_store/state_store.py @@ -0,0 +1,13 @@ +from logging import Logger + + +class OAuthStateStore: + @property + def logger(self) -> Logger: + raise NotImplementedError() + + def issue(self, *args, **kwargs) -> str: + raise NotImplementedError() + + def consume(self, state: str) -> bool: + raise NotImplementedError() diff --git a/slack_sdk/oauth/state_utils/__init__.py b/slack_sdk/oauth/state_utils/__init__.py new file mode 100644 index 000000000..cfdb66bcb --- /dev/null +++ b/slack_sdk/oauth/state_utils/__init__.py @@ -0,0 +1,42 @@ +from typing import Optional, Dict, Sequence, Union + + +class OAuthStateUtils: + cookie_name: str + expiration_seconds: int + + default_cookie_name: str = "slack-app-oauth-state" + default_expiration_seconds: int = 60 * 10 # 10 minutes + + def __init__( + self, + *, + cookie_name: str = default_cookie_name, + expiration_seconds: int = default_expiration_seconds, + ): + self.cookie_name = cookie_name + self.expiration_seconds = expiration_seconds + + def build_set_cookie_for_new_state(self, state: str) -> str: + return f"{self.cookie_name}={state}; " "Secure; " "HttpOnly; " "Path=/; " f"Max-Age={self.expiration_seconds}" + + def build_set_cookie_for_deletion(self) -> str: + return f"{self.cookie_name}=deleted; " "Secure; " "HttpOnly; " "Path=/; " "Expires=Thu, 01 Jan 1970 00:00:00 GMT" + + def is_valid_browser( + self, + state: Optional[str], + request_headers: Dict[str, Union[str, Sequence[str]]], + ) -> bool: + if state is None or request_headers is None or request_headers.get("cookie", None) is None: + return False + cookies = request_headers["cookie"] + if isinstance(cookies, str): + cookies = [cookies] + for cookie in cookies: + values = cookie.split(";") + for value in values: + # handle quoted cookie values (e.g. due to base64 encoding) + if value.strip().replace('"', "").replace("'", "") == f"{self.cookie_name}={state}": + return True + return False diff --git a/slack_sdk/oauth/token_rotation/__init__.py b/slack_sdk/oauth/token_rotation/__init__.py new file mode 100644 index 000000000..5915afef7 --- /dev/null +++ b/slack_sdk/oauth/token_rotation/__init__.py @@ -0,0 +1,5 @@ +from .rotator import TokenRotator + +__all__ = [ + "TokenRotator", +] diff --git a/slack_sdk/oauth/token_rotation/async_rotator.py b/slack_sdk/oauth/token_rotation/async_rotator.py new file mode 100644 index 000000000..c3506f004 --- /dev/null +++ b/slack_sdk/oauth/token_rotation/async_rotator.py @@ -0,0 +1,142 @@ +from time import time +from typing import Optional + +from slack_sdk.errors import SlackApiError, SlackTokenRotationError +from slack_sdk.web.async_client import AsyncWebClient +from slack_sdk.oauth.installation_store import Installation, Bot + + +class AsyncTokenRotator: + client: AsyncWebClient + client_id: str + client_secret: str + + def __init__( + self, + *, + client_id: str, + client_secret: str, + client: Optional[AsyncWebClient] = None, + ): + self.client = client if client is not None else AsyncWebClient(token=None) + self.client_id = client_id + self.client_secret = client_secret + + async def perform_token_rotation( + self, + *, + installation: Installation, + minutes_before_expiration: int = 120, # 2 hours by default + ) -> Optional[Installation]: + """Performs token rotation if the underlying tokens (bot / user) are expired / expiring. + + Args: + installation: the current installation data + minutes_before_expiration: the minutes before the token expiration + + Returns: + None if no rotation is necessary for now. + """ + + # TODO: make the following two calls in parallel for better performance + + # bot + rotated_bot: Optional[Bot] = await self.perform_bot_token_rotation( + bot=installation.to_bot(), + minutes_before_expiration=minutes_before_expiration, + ) + + # user + rotated_installation = await self.perform_user_token_rotation( + installation=installation, + minutes_before_expiration=minutes_before_expiration, + ) + + if rotated_bot is not None: + if rotated_installation is None: + rotated_installation = Installation(**installation.to_dict_for_copying()) + rotated_installation.bot_token = rotated_bot.bot_token + rotated_installation.bot_refresh_token = rotated_bot.bot_refresh_token + rotated_installation.bot_token_expires_at = rotated_bot.bot_token_expires_at + + return rotated_installation + + async def perform_bot_token_rotation( + self, + *, + bot: Bot, + minutes_before_expiration: int = 120, # 2 hours by default + ) -> Optional[Bot]: + """Performs bot token rotation if the underlying bot token is expired / expiring. + + Args: + bot: the current bot installation data + minutes_before_expiration: the minutes before the token expiration + + Returns: + None if no rotation is necessary for now. + """ + if bot.bot_token_expires_at is None: + return None + if bot.bot_token_expires_at > time() + minutes_before_expiration * 60: + return None + + try: + refresh_response = await self.client.oauth_v2_access( + client_id=self.client_id, + client_secret=self.client_secret, + grant_type="refresh_token", + refresh_token=bot.bot_refresh_token, + ) + # TODO: error handling + + if refresh_response.get("token_type") != "bot": + return None + + refreshed_bot = Bot(**bot.to_dict_for_copying()) + refreshed_bot.bot_token = refresh_response["access_token"] + refreshed_bot.bot_refresh_token = refresh_response.get("refresh_token") + refreshed_bot.bot_token_expires_at = int(time()) + int(refresh_response["expires_in"]) + return refreshed_bot + + except SlackApiError as e: + raise SlackTokenRotationError(e) + + async def perform_user_token_rotation( + self, + *, + installation: Installation, + minutes_before_expiration: int = 120, # 2 hours by default + ) -> Optional[Installation]: + """Performs user token rotation if the underlying user token is expired / expiring. + + Args: + installation: the current installation data + minutes_before_expiration: the minutes before the token expiration + + Returns: + None if no rotation is necessary for now. + """ + if installation.user_token_expires_at is None: + return None + if installation.user_token_expires_at > time() + minutes_before_expiration * 60: + return None + + try: + refresh_response = await self.client.oauth_v2_access( + client_id=self.client_id, + client_secret=self.client_secret, + grant_type="refresh_token", + refresh_token=installation.user_refresh_token, + ) + if refresh_response.get("token_type") != "user": + return None + + refreshed_installation = Installation(**installation.to_dict_for_copying()) + refreshed_installation.user_token = refresh_response.get("access_token") + refreshed_installation.user_refresh_token = refresh_response.get("refresh_token") + refreshed_installation.user_token_expires_at = int(time()) + int(refresh_response.get("expires_in")) # type: ignore[arg-type] # noqa: E501 + return refreshed_installation + + except SlackApiError as e: + raise SlackTokenRotationError(e) diff --git a/slack_sdk/oauth/token_rotation/rotator.py b/slack_sdk/oauth/token_rotation/rotator.py new file mode 100644 index 000000000..e7dab22cc --- /dev/null +++ b/slack_sdk/oauth/token_rotation/rotator.py @@ -0,0 +1,135 @@ +from time import time +from typing import Optional + +from slack_sdk.errors import SlackApiError, SlackTokenRotationError +from slack_sdk.web import WebClient +from slack_sdk.oauth.installation_store import Installation, Bot + + +class TokenRotator: + client: WebClient + client_id: str + client_secret: str + + def __init__(self, *, client_id: str, client_secret: str, client: Optional[WebClient] = None): + self.client = client if client is not None else WebClient(token=None) + self.client_id = client_id + self.client_secret = client_secret + + def perform_token_rotation( + self, + *, + installation: Installation, + minutes_before_expiration: int = 120, # 2 hours by default + ) -> Optional[Installation]: + """Performs token rotation if the underlying tokens (bot / user) are expired / expiring. + + Args: + installation: the current installation data + minutes_before_expiration: the minutes before the token expiration + + Returns: + None if no rotation is necessary for now. + """ + + # TODO: make the following two calls in parallel for better performance + + # bot + rotated_bot: Optional[Bot] = self.perform_bot_token_rotation( + bot=installation.to_bot(), + minutes_before_expiration=minutes_before_expiration, + ) + + # user + rotated_installation: Optional[Installation] = self.perform_user_token_rotation( + installation=installation, + minutes_before_expiration=minutes_before_expiration, + ) + + if rotated_bot is not None: + if rotated_installation is None: + rotated_installation = Installation(**installation.to_dict_for_copying()) + rotated_installation.bot_token = rotated_bot.bot_token + rotated_installation.bot_refresh_token = rotated_bot.bot_refresh_token + rotated_installation.bot_token_expires_at = rotated_bot.bot_token_expires_at + + return rotated_installation + + def perform_bot_token_rotation( + self, + *, + bot: Bot, + minutes_before_expiration: int = 120, # 2 hours by default + ) -> Optional[Bot]: + """Performs bot token rotation if the underlying bot token is expired / expiring. + + Args: + bot: the current bot installation data + minutes_before_expiration: the minutes before the token expiration + + Returns: + None if no rotation is necessary for now. + """ + if bot.bot_token_expires_at is None: + return None + if bot.bot_token_expires_at > time() + minutes_before_expiration * 60: + return None + + try: + refresh_response = self.client.oauth_v2_access( + client_id=self.client_id, + client_secret=self.client_secret, + grant_type="refresh_token", + refresh_token=bot.bot_refresh_token, + ) + if refresh_response.get("token_type") != "bot": + return None + + refreshed_bot = Bot(**bot.to_dict_for_copying()) + refreshed_bot.bot_token = refresh_response["access_token"] + refreshed_bot.bot_refresh_token = refresh_response.get("refresh_token") + refreshed_bot.bot_token_expires_at = int(time()) + int(refresh_response["expires_in"]) + return refreshed_bot + + except SlackApiError as e: + raise SlackTokenRotationError(e) + + def perform_user_token_rotation( + self, + *, + installation: Installation, + minutes_before_expiration: int = 120, # 2 hours by default + ) -> Optional[Installation]: + """Performs user token rotation if the underlying user token is expired / expiring. + + Args: + installation: the current installation data + minutes_before_expiration: the minutes before the token expiration + + Returns: + None if no rotation is necessary for now. + """ + if installation.user_token_expires_at is None: + return None + if installation.user_token_expires_at > time() + minutes_before_expiration * 60: + return None + + try: + refresh_response = self.client.oauth_v2_access( + client_id=self.client_id, + client_secret=self.client_secret, + grant_type="refresh_token", + refresh_token=installation.user_refresh_token, + ) + + if refresh_response.get("token_type") != "user": + return None + + refreshed_installation = Installation(**installation.to_dict_for_copying()) + refreshed_installation.user_token = refresh_response.get("access_token") + refreshed_installation.user_refresh_token = refresh_response.get("refresh_token") + refreshed_installation.user_token_expires_at = int(time()) + int(refresh_response["expires_in"]) + return refreshed_installation + + except SlackApiError as e: + raise SlackTokenRotationError(e) diff --git a/slack_sdk/proxy_env_variable_loader.py b/slack_sdk/proxy_env_variable_loader.py new file mode 100644 index 000000000..7df080b9b --- /dev/null +++ b/slack_sdk/proxy_env_variable_loader.py @@ -0,0 +1,25 @@ +"""Internal module for loading proxy-related env variables""" + +import logging +import os +from typing import Optional + +_default_logger = logging.getLogger(__name__) + + +def load_http_proxy_from_env(logger: logging.Logger = _default_logger) -> Optional[str]: + proxy_url = ( + os.environ.get("HTTPS_PROXY") + or os.environ.get("https_proxy") + or os.environ.get("HTTP_PROXY") + or os.environ.get("http_proxy") + ) + if proxy_url is None: + return None + if len(proxy_url.strip()) == 0: + # If the value is an empty string, the intention should be unsetting it + logger.debug("The Slack SDK ignored the proxy env variable as an empty value is set.") + return None + + logger.debug(f"HTTP proxy URL has been loaded from an env variable: {proxy_url}") + return proxy_url diff --git a/slack_sdk/py.typed b/slack_sdk/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/slack_sdk/rtm/__init__.py b/slack_sdk/rtm/__init__.py new file mode 100644 index 000000000..5849cde52 --- /dev/null +++ b/slack_sdk/rtm/__init__.py @@ -0,0 +1,570 @@ +"""A Python module for interacting with Slack's RTM API.""" + +import asyncio +import collections +import inspect +import logging +import os +import random +import signal +from asyncio import Future +from ssl import SSLContext +from threading import current_thread, main_thread +from typing import Any, Union, Sequence +from typing import Optional, Callable, DefaultDict + +import aiohttp + +import slack_sdk.errors as client_err +from slack_sdk.aiohttp_version_checker import validate_aiohttp_version +from slack_sdk.web.legacy_client import LegacyWebClient as WebClient + + +validate_aiohttp_version(aiohttp.__version__) + + +class RTMClient(object): + """An RTMClient allows apps to communicate with the Slack Platform's RTM API. + + The event-driven architecture of this client allows you to simply + link callbacks to their corresponding events. When an event occurs + this client executes your callback while passing along any + information it receives. + + Attributes: + token (str): A string specifying an xoxp or xoxb token. + run_async (bool): A boolean specifying if the client should + be run in async mode. Default is False. + auto_reconnect (bool): When true the client will automatically + reconnect when (not manually) disconnected. Default is True. + ssl (SSLContext): To use SSL support, pass an SSLContext object here. + Default is None. + proxy (str): To use proxy support, pass the string of the proxy server. + e.g. "http://proxy.com" + Authentication credentials can be passed in proxy URL. + e.g. "http://user:pass@some.proxy.com" + Default is None. + timeout (int): The amount of seconds the session should wait before timing out. + Default is 30. + base_url (str): The base url for all HTTP requests. + Note: This is only used in the WebClient. + Default is "https://slack.com/api/". + connect_method (str): An string specifying if the client + will connect with `rtm.connect` or `rtm.start`. + Default is `rtm.connect`. + ping_interval (int): automatically send "ping" command every + specified period of seconds. If set to 0, do not send automatically. + Default is 30. + loop (AbstractEventLoop): An event loop provided by asyncio. + If None is specified we attempt to use the current loop + with `get_event_loop`. Default is None. + + Methods: + ping: Sends a ping message over the websocket to Slack. + typing: Sends a typing indicator to the specified channel. + on: Stores and links callbacks to websocket and Slack events. + run_on: Decorator that stores and links callbacks to websocket and Slack events. + start: Starts an RTM Session with Slack. + stop: Closes the websocket connection and ensures it won't reconnect. + + Example: + ```python + import os + from slack import RTMClient + + @RTMClient.run_on(event="message") + def say_hello(**payload): + data = payload['data'] + web_client = payload['web_client'] + if 'Hello' in data['text']: + channel_id = data['channel'] + thread_ts = data['ts'] + user = data['user'] + + web_client.chat_postMessage( + channel=channel_id, + text=f"Hi <@{user}>!", + thread_ts=thread_ts + ) + + slack_token = os.environ["SLACK_API_TOKEN"] + rtm_client = RTMClient(token=slack_token) + rtm_client.start() + ``` + + Note: + The initial state returned when establishing an RTM connection will + be available as the data in payload for the 'open' event. This data is not and + will not be stored on the RTM Client. + + Any attributes or methods prefixed with _underscores are + intended to be "private" internal use only. They may be changed or + removed at anytime. + """ + + _callbacks: DefaultDict = collections.defaultdict(list) + + def __init__( + self, + *, + token: str, + run_async: Optional[bool] = False, + auto_reconnect: Optional[bool] = True, + ssl: Optional[SSLContext] = None, + proxy: Optional[str] = None, + timeout: Optional[int] = 30, + base_url: Optional[str] = WebClient.BASE_URL, + connect_method: Optional[str] = None, + ping_interval: Optional[int] = 30, + loop: Optional[asyncio.AbstractEventLoop] = None, + headers: Optional[dict] = {}, + ): + self.token = token.strip() + self.run_async = run_async + self.auto_reconnect = auto_reconnect + self.ssl = ssl + self.proxy = proxy + self.timeout = timeout + self.base_url = base_url + self.connect_method = connect_method + self.ping_interval = ping_interval + self.headers = headers + self._event_loop = loop or asyncio.get_event_loop() + self._web_client = None + self._websocket = None + self._session = None + self._logger = logging.getLogger(__name__) + self._last_message_id = 0 + self._connection_attempts = 0 + self._stopped = False + self._web_client = WebClient( + token=self.token, + base_url=self.base_url, # type: ignore[arg-type] + timeout=self.timeout, # type: ignore[arg-type] + ssl=self.ssl, + proxy=self.proxy, + run_async=self.run_async, # type: ignore[arg-type] + loop=self._event_loop, + session=self._session, + headers=self.headers, + ) + + @staticmethod + def run_on(*, event: str): + """A decorator to store and link a callback to an event.""" + + def decorator(callback): + RTMClient.on(event=event, callback=callback) + return callback + + return decorator + + @classmethod + def on(cls, *, event: str, callback: Callable): + """Stores and links the callback(s) to the event. + + Args: + event (str): A string that specifies a Slack or websocket event. + e.g. 'channel_joined' or 'open' + callback (Callable): Any object or a list of objects that can be called. + e.g. or + [,] + + Raises: + SlackClientError: The specified callback is not callable. + SlackClientError: The callback must accept keyword arguments (**kwargs). + """ + if isinstance(callback, list): + for cb in callback: + cls._validate_callback(cb) + previous_callbacks = cls._callbacks[event] + cls._callbacks[event] = list(set(previous_callbacks + callback)) + else: + cls._validate_callback(callback) + cls._callbacks[event].append(callback) + + def start(self) -> Union[asyncio.Future, Any]: + """Starts an RTM Session with Slack. + + Makes an authenticated call to Slack's RTM API to retrieve + a websocket URL and then connects to the message server. + As events stream-in we run any associated callbacks stored + on the client. + + If 'auto_reconnect' is specified we + retrieve a new url and reconnect any time the connection + is lost unintentionally or an exception is thrown. + + Raises: + SlackApiError: Unable to retrieve RTM URL from Slack. + """ + # Not yet implemented: Add Windows support for graceful shutdowns. + if os.name != "nt" and current_thread() == main_thread(): + signals = (signal.SIGHUP, signal.SIGTERM, signal.SIGINT) + for s in signals: + self._event_loop.add_signal_handler(s, self.stop) + + future: Future[Any] = asyncio.ensure_future(self._connect_and_read(), loop=self._event_loop) + + if self.run_async: + return future + return self._event_loop.run_until_complete(future) + + def stop(self): + """Closes the websocket connection and ensures it won't reconnect. + + If your application outputs the following errors, + call #async_stop() instead and await for the completion on your application side. + + asyncio/base_events.py:641: RuntimeWarning: + coroutine 'ClientWebSocketResponse.close' was never awaited self._ready.clear() + """ + self._logger.debug("The Slack RTMClient is shutting down.") + self._stopped = True + self._close_websocket() + + async def async_stop(self): + """Closes the websocket connection and ensures it won't reconnect.""" + self._logger.debug("The Slack RTMClient is shutting down.") + remaining_futures = self._close_websocket() + for future in remaining_futures: + await future + self._stopped = True + + def send_over_websocket(self, *, payload: dict): + """Sends a message to Slack over the WebSocket connection. + + Note: + The RTM API only supports posting simple messages formatted using + our default message formatting mode. It does not support + attachments or other message formatting modes. For this reason + we recommend users send messages via the Web API methods. + e.g. web_client.chat_postMessage() + + If the message "id" is not specified in the payload, it'll be added. + + Args: + payload (dict): The message to send over the wesocket. + e.g. + { + "id": 1, + "type": "typing", + "channel": "C024BE91L" + } + + Raises: + SlackClientNotConnectedError: Websocket connection is closed. + """ + return asyncio.ensure_future(self._send_json(payload), loop=self._event_loop) + + async def _send_json(self, payload): + if self._websocket is None or self._event_loop is None: + raise client_err.SlackClientNotConnectedError("Websocket connection is closed.") + if "id" not in payload: + payload["id"] = self._next_msg_id() + + return await self._websocket.send_json(payload) + + async def ping(self): + """Sends a ping message over the websocket to Slack. + + Not all web browsers support the WebSocket ping spec, + so the RTM protocol also supports ping/pong messages. + + Raises: + SlackClientNotConnectedError: Websocket connection is closed. + """ + payload = {"id": self._next_msg_id(), "type": "ping"} + await self._send_json(payload=payload) + + async def typing(self, *, channel: str): + """Sends a typing indicator to the specified channel. + + This indicates that this app is currently + writing a message to send to a channel. + + Args: + channel (str): The channel id. e.g. 'C024BE91L' + + Raises: + SlackClientNotConnectedError: Websocket connection is closed. + """ + payload = {"id": self._next_msg_id(), "type": "typing", "channel": channel} + await self._send_json(payload=payload) + + @staticmethod + def _validate_callback(callback): + """Checks if the specified callback is callable and accepts a kwargs param. + + Args: + callback (obj): Any object or a list of objects that can be called. + e.g. + + Raises: + SlackClientError: The specified callback is not callable. + SlackClientError: The callback must accept keyword arguments (**kwargs). + """ + + cb_name = callback.__name__ if hasattr(callback, "__name__") else callback + if not callable(callback): + msg = "The specified callback '{}' is not callable.".format(cb_name) + raise client_err.SlackClientError(msg) + callback_params = inspect.signature(callback).parameters.values() + if not any(param for param in callback_params if param.kind == param.VAR_KEYWORD): + msg = "The callback '{}' must accept keyword arguments (**kwargs).".format(cb_name) + raise client_err.SlackClientError(msg) + + def _next_msg_id(self): + """Retrieves the next message id. + + When sending messages to Slack every event should + have a unique (for that connection) positive integer ID. + + Returns: + An integer representing the message id. e.g. 98 + """ + self._last_message_id += 1 + return self._last_message_id + + async def _connect_and_read(self): + """Retrieves the WS url and connects to Slack's RTM API. + + Makes an authenticated call to Slack's Web API to retrieve + a websocket URL. Then connects to the message server and + reads event messages as they come in. + + If 'auto_reconnect' is specified we + retrieve a new url and reconnect any time the connection + is lost unintentionally or an exception is thrown. + + Raises: + SlackApiError: Unable to retrieve RTM URL from Slack. + websockets.exceptions: Errors thrown by the 'websockets' library. + """ + while not self._stopped: + try: + self._connection_attempts += 1 + async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=self.timeout)) as session: + self._session = session + url, data = await self._retrieve_websocket_info() + async with session.ws_connect( + url, + heartbeat=self.ping_interval, + ssl=self.ssl, + proxy=self.proxy, + ) as websocket: + self._logger.debug("The Websocket connection has been opened.") + self._websocket = websocket + await self._dispatch_event(event="open", data=data) + await self._read_messages() + # The websocket has been disconnected, or self._stopped is True + if not self._stopped and not self.auto_reconnect: + self._logger.warning("Not reconnecting the Websocket because auto_reconnect is False") + return + # No need to wait exponentially here, since the connection was + # established OK, but timed out, or was closed remotely + except ( + client_err.SlackClientNotConnectedError, + client_err.SlackApiError, + # Not yet implemented: Catch websocket exceptions thrown by aiohttp. + ) as exception: + await self._dispatch_event(event="error", data=exception) + error_code = exception.response.get("error", None) if hasattr(exception, "response") else None + if ( + self.auto_reconnect + and not self._stopped + and error_code != "invalid_auth" # "invalid_auth" is unrecoverable + ): + await self._wait_exponentially(exception) + continue + self._logger.exception("The Websocket encountered an error. Closing the connection...") + self._close_websocket() + raise + + async def _read_messages(self): + """Process messages received on the WebSocket connection.""" + while not self._stopped and self._websocket is not None: + try: + # Wait for a message to be received, but timeout after a second so that + # we can check if the socket has been closed, or if self._stopped is + # True + message = await self._websocket.receive(timeout=1) + except asyncio.TimeoutError: + if not self._websocket.closed: + # We didn't receive a message within the timeout interval, but + # aiohttp hasn't closed the socket, so ping responses must still be + # returning + continue + self._logger.warning( + "Websocket was closed (%s).", + self._websocket.close_code if self._websocket else "", + ) + await self._dispatch_event( + event="error", + data=self._websocket.exception() if self._websocket else "", + ) + self._websocket = None + await self._dispatch_event(event="close") + return + + if message.type == aiohttp.WSMsgType.TEXT: + try: + payload = message.json() + event = payload.pop("type", "Unknown") + await self._dispatch_event(event, data=payload) + except Exception as err: + data = message.data if message else message + self._logger.info(f"Caught a raised exception ({err}) while dispatching a TEXT message ({data})") + # Raised exceptions here happen in users' code and were just unhandled. + # As they're not intended for closing current WebSocket connection, + # this exception should not be propagated to higher level (#_connect_and_read()). + continue + elif message.type == aiohttp.WSMsgType.ERROR: + self._logger.error("Received an error on the websocket: %r", message) + await self._dispatch_event(event="error", data=message) + elif message.type in ( + aiohttp.WSMsgType.CLOSE, + aiohttp.WSMsgType.CLOSING, + aiohttp.WSMsgType.CLOSED, + ): + self._logger.warning("Websocket was closed.") + self._websocket = None + await self._dispatch_event(event="close") + else: + self._logger.debug("Received unhandled message type: %r", message) + + async def _dispatch_event(self, event, data=None): + """Dispatches the event and executes any associated callbacks. + + Note: To prevent the app from crashing due to callback errors. We + catch all exceptions and send all data to the logger. + + Args: + event (str): The type of event. e.g. 'bot_added' + data (dict): The data Slack sent. e.g. + { + "type": "bot_added", + "bot": { + "id": "B024BE7LH", + "app_id": "A4H1JB4AZ", + "name": "hugbot" + } + } + """ + if self._logger.level <= logging.DEBUG: + self._logger.debug("Received an event: '%s' - %s", event, data) + for callback in self._callbacks[event]: + self._logger.debug( + "Running %s callbacks for event: '%s'", + len(self._callbacks[event]), + event, + ) + try: + if self._stopped and event not in ["close", "error"]: + # Don't run callbacks if client was stopped unless they're + # close/error callbacks. + break + + if inspect.iscoroutinefunction(callback): + await callback(rtm_client=self, web_client=self._web_client, data=data) + else: + if self.run_async is True: + raise client_err.SlackRequestError( + f'The callback "{callback.__name__}" is NOT a coroutine. ' + "Running such with run_async=True is unsupported. " + "Consider adding async/await to the method " + "or going with run_async=False if your app is not really non-blocking." + ) + payload = { + "rtm_client": self, + "web_client": self._web_client, + "data": data, + } + callback(**payload) + except Exception as err: + name = callback.__name__ + module = callback.__module__ + msg = f"When calling '#{name}()' in the '{module}' module the following error was raised: {err}" + self._logger.error(msg) + raise + + async def _retrieve_websocket_info(self): + """Retrieves the WebSocket info from Slack. + + Returns: + A tuple of websocket information. + e.g. + ( + "wss://...", + { + "self": {"id": "U01234ABC","name": "robotoverlord"}, + "team": { + "domain": "exampledomain", + "id": "T123450FP", + "name": "ExampleName" + } + } + ) + + Raises: + SlackApiError: Unable to retrieve RTM URL from Slack. + """ + if self._web_client is None: + self._web_client = WebClient( + token=self.token, + base_url=self.base_url, + timeout=self.timeout, + ssl=self.ssl, + proxy=self.proxy, + run_async=True, + loop=self._event_loop, + session=self._session, + headers=self.headers, + ) + self._logger.debug("Retrieving websocket info.") + use_rtm_start = self.connect_method in ["rtm.start", "rtm_start"] + if self.run_async: + if use_rtm_start: + resp = await self._web_client.rtm_start() + else: + resp = await self._web_client.rtm_connect() + else: + if use_rtm_start: + resp = self._web_client.rtm_start() + else: + resp = self._web_client.rtm_connect() + + url = resp.get("url") + if url is None: + msg = "Unable to retrieve RTM URL from Slack." + raise client_err.SlackApiError(message=msg, response=resp) + return url, resp.data + + async def _wait_exponentially(self, exception, max_wait_time=300): + """Wait exponentially longer for each connection attempt. + + Calculate the number of seconds to wait and then add + a random number of milliseconds to avoid coincidental + synchronized client retries. Wait up to the maximum amount + of wait time specified via 'max_wait_time'. However, + if Slack returned how long to wait use that. + """ + if hasattr(exception, "response"): + wait_time = exception.response.get("headers", {}).get( + "Retry-After", + min((2**self._connection_attempts) + random.random(), max_wait_time), + ) + self._logger.debug("Waiting %s seconds before reconnecting.", wait_time) + await asyncio.sleep(float(wait_time)) + + def _close_websocket(self) -> Sequence[Future]: + """Closes the websocket connection.""" + futures = [] + close_method = getattr(self._websocket, "close", None) + if callable(close_method): + future = asyncio.ensure_future(close_method(), loop=self._event_loop) + futures.append(future) + self._websocket = None + event_f = asyncio.ensure_future(self._dispatch_event(event="close"), loop=self._event_loop) + futures.append(event_f) + return futures diff --git a/slack_sdk/rtm/v2/__init__.py b/slack_sdk/rtm/v2/__init__.py new file mode 100644 index 000000000..3ddf0519e --- /dev/null +++ b/slack_sdk/rtm/v2/__init__.py @@ -0,0 +1,5 @@ +from slack_sdk.rtm_v2 import RTMClient + +__all__ = [ + "RTMClient", +] diff --git a/slack_sdk/rtm_v2/__init__.py b/slack_sdk/rtm_v2/__init__.py new file mode 100644 index 000000000..be059303d --- /dev/null +++ b/slack_sdk/rtm_v2/__init__.py @@ -0,0 +1,392 @@ +"""A Python module for interacting with Slack's RTM API.""" + +import inspect +import json +import logging +import time +from concurrent.futures.thread import ThreadPoolExecutor +from logging import Logger +from queue import Queue, Empty +from ssl import SSLContext +from threading import Lock, Event +from typing import Optional, Callable, List, Union + +from slack_sdk.errors import SlackApiError, SlackClientError +from slack_sdk.proxy_env_variable_loader import load_http_proxy_from_env +from slack_sdk.socket_mode.builtin.connection import Connection, ConnectionState +from slack_sdk.socket_mode.interval_runner import IntervalRunner +from slack_sdk.web import WebClient + + +class RTMClient: + token: Optional[str] + bot_id: Optional[str] + default_auto_reconnect_enabled: bool + auto_reconnect_enabled: bool + ssl: Optional[SSLContext] + proxy: Optional[str] + timeout: int + base_url: str + ping_interval: int + logger: Logger + web_client: WebClient + + current_session: Optional[Connection] + current_session_state: Optional[ConnectionState] + wss_uri: Optional[str] + + message_queue: Queue + message_listeners: List[Callable[["RTMClient", dict], None]] + message_processor: IntervalRunner + message_workers: ThreadPoolExecutor + + closed: bool + connect_operation_lock: Lock + + on_message_listeners: List[Callable[[str], None]] + on_error_listeners: List[Callable[[Exception], None]] + on_close_listeners: List[Callable[[int, Optional[str]], None]] + + def __init__( + self, + *, + token: Optional[str] = None, + web_client: Optional[WebClient] = None, + auto_reconnect_enabled: bool = True, + ssl: Optional[SSLContext] = None, + proxy: Optional[str] = None, + timeout: int = 30, + base_url: str = WebClient.BASE_URL, + headers: Optional[dict] = None, + ping_interval: int = 5, + concurrency: int = 10, + logger: Optional[logging.Logger] = None, + on_message_listeners: Optional[List[Callable[[str], None]]] = None, + on_error_listeners: Optional[List[Callable[[Exception], None]]] = None, + on_close_listeners: Optional[List[Callable[[int, Optional[str]], None]]] = None, + trace_enabled: bool = False, + all_message_trace_enabled: bool = False, + ping_pong_trace_enabled: bool = False, + ): + self.token = token.strip() if token is not None else None + self.bot_id = None + self.default_auto_reconnect_enabled = auto_reconnect_enabled + # You may want temporarily turn off the auto_reconnect as necessary + self.auto_reconnect_enabled = self.default_auto_reconnect_enabled + self.ssl = ssl + self.proxy = proxy + self.timeout = timeout + self.base_url = base_url + self.headers = headers + self.ping_interval = ping_interval + self.logger = logger or logging.getLogger(__name__) + if self.proxy is None or len(self.proxy.strip()) == 0: + env_variable = load_http_proxy_from_env(self.logger) + if env_variable is not None: + self.proxy = env_variable + + self.web_client = web_client or WebClient( + token=self.token, + base_url=self.base_url, + timeout=self.timeout, + ssl=self.ssl, + proxy=self.proxy, + headers=self.headers, + logger=logger, + ) + + self.on_message_listeners = on_message_listeners or [] + + self.on_error_listeners = on_error_listeners or [] + self.on_close_listeners = on_close_listeners or [] + + self.trace_enabled = trace_enabled + self.all_message_trace_enabled = all_message_trace_enabled + self.ping_pong_trace_enabled = ping_pong_trace_enabled + + self.message_queue = Queue() + + def goodbye_listener(_self, event: dict): + if event.get("type") == "goodbye": + message = "Got a goodbye message. Reconnecting to the server ..." + self.logger.info(message) + self.connect_to_new_endpoint(force=True) + + self.message_listeners = [goodbye_listener] + self.socket_mode_request_listeners = [] + + self.current_session = None + self.current_session_state = ConnectionState() + self.current_session_runner = IntervalRunner(self._run_current_session, 0.1).start() + self.wss_uri = None + + self.current_app_monitor_started = False + self.current_app_monitor = IntervalRunner( + self._monitor_current_session, + self.ping_interval, + ) + + self.closed = False + self.connect_operation_lock = Lock() + + self.message_processor = IntervalRunner(self.process_messages, 0.001).start() + self.message_workers = ThreadPoolExecutor(max_workers=concurrency) + + # -------------------------------------------------------------- + # Decorator to register listeners + # -------------------------------------------------------------- + + def on(self, event_type: str) -> Callable: + """Registers a new event listener. + + Args: + event_type: str representing an event's type (e.g., message, reaction_added) + """ + + def __call__(*args, **kwargs): + func = args[0] + if func is not None: + if isinstance(func, Callable): + name = ( + func.__name__ + if hasattr(func, "__name__") + else f"{func.__class__.__module__}.{func.__class__.__name__}" + ) + inspect_result: inspect.FullArgSpec = inspect.getfullargspec(func) + if inspect_result is not None and len(inspect_result.args) != 2: + actual_args = ", ".join(inspect_result.args) + error = f"The listener '{name}' must accept two args: client, event (actual: {actual_args})" + raise SlackClientError(error) + + def new_message_listener(_self, event: dict): + actual_event_type = event.get("type") + if event.get("bot_id") == self.bot_id: + # SKip the events generated by this bot user + return + # https://github.com/slackapi/python-slack-sdk/issues/533 + if event_type == "*" or (actual_event_type is not None and actual_event_type == event_type): + func(_self, event) + + self.message_listeners.append(new_message_listener) + else: + error = f"The listener '{func}' is not a Callable (actual: {type(func).__name__})" + raise SlackClientError(error) + # Not to cause modification to the decorated method + return func + + return __call__ + + # -------------------------------------------------------------- + # Connections + # -------------------------------------------------------------- + + def is_connected(self) -> bool: + """Returns True if this client is connected.""" + return self.current_session is not None and self.current_session.is_active() + + def issue_new_wss_url(self) -> str: + """Acquires a new WSS URL using rtm.connect API method""" + try: + api_response = self.web_client.rtm_connect() + return api_response["url"] + except SlackApiError as e: + if e.response["error"] == "ratelimited": + delay = int(e.response.headers.get("Retry-After", "30")) # Tier1 + self.logger.info(f"Rate limited. Retrying in {delay} seconds...") + time.sleep(delay) + # Retry to issue a new WSS URL + return self.issue_new_wss_url() + else: + # other errors + self.logger.error(f"Failed to retrieve WSS URL: {e}") + raise e + + def connect_to_new_endpoint(self, force: bool = False): + """Acquires a new WSS URL and tries to connect to the endpoint.""" + with self.connect_operation_lock: + if force or not self.is_connected(): + self.logger.info("Connecting to a new endpoint...") + self.wss_uri = self.issue_new_wss_url() + self.connect() + self.logger.info("Connected to a new endpoint...") + + def connect(self): + """Starts talking to the RTM server through a WebSocket connection""" + if self.bot_id is None: + self.bot_id = self.web_client.auth_test()["bot_id"] + + old_session: Optional[Connection] = self.current_session + old_current_session_state: ConnectionState = self.current_session_state + + if self.wss_uri is None: + self.wss_uri = self.issue_new_wss_url() + + current_session = Connection( + url=self.wss_uri, + logger=self.logger, + ping_interval=self.ping_interval, + trace_enabled=self.trace_enabled, + all_message_trace_enabled=self.all_message_trace_enabled, + ping_pong_trace_enabled=self.ping_pong_trace_enabled, + receive_buffer_size=1024, + proxy=self.proxy, + on_message_listener=self.run_all_message_listeners, + on_error_listener=self.run_all_error_listeners, + on_close_listener=self.run_all_close_listeners, + connection_type_name="RTM", + ) + current_session.connect() + + if old_current_session_state is not None: + old_current_session_state.terminated = True + if old_session is not None: + old_session.close() + + self.current_session = current_session + self.current_session_state = ConnectionState() + self.auto_reconnect_enabled = self.default_auto_reconnect_enabled + + if not self.current_app_monitor_started: + self.current_app_monitor_started = True + self.current_app_monitor.start() + + self.logger.info(f"A new session has been established (session id: {self.session_id()})") + + def disconnect(self): + """Disconnects the current session.""" + self.current_session.disconnect() + + def close(self) -> None: + """ + Closes this instance and cleans up underlying resources. + After calling this method, this instance is no longer usable. + """ + self.closed = True + self.disconnect() + self.current_session.close() + + def start(self) -> None: + """Establishes an RTM connection and blocks the current thread.""" + self.connect() + Event().wait() + + def send(self, payload: Union[dict, str]) -> None: + if payload is None: + return + if self.current_session is None or not self.current_session.is_active(): + raise SlackClientError("The RTM client is not connected to the Slack servers") + if isinstance(payload, str): + self.current_session.send(payload) + else: + self.current_session.send(json.dumps(payload)) + + # -------------------------------------------------------------- + # WS Message Processor + # -------------------------------------------------------------- + + def enqueue_message(self, message: str): + self.message_queue.put(message) + if self.logger.level <= logging.DEBUG: + self.logger.debug(f"A new message enqueued (current queue size: {self.message_queue.qsize()})") + + def process_message(self): + try: + raw_message = self.message_queue.get(timeout=1) + if self.logger.level <= logging.DEBUG: + self.logger.debug(f"A message dequeued (current queue size: {self.message_queue.qsize()})") + + if raw_message is not None: + message: dict = {} + if raw_message.startswith("{"): + message = json.loads(raw_message) + + def _run_message_listeners(): + self.run_message_listeners(message) + + self.message_workers.submit(_run_message_listeners) + except Empty: + pass + + def process_messages(self) -> None: + while not self.closed: + try: + self.process_message() + except Exception as e: + self.logger.exception(f"Failed to process a message: {e}") + + def run_message_listeners(self, message: dict) -> None: + type = message.get("type") + if self.logger.level <= logging.DEBUG: + self.logger.debug(f"Message processing started (type: {type})") + try: + for listener in self.message_listeners: + try: + listener(self, message) + except Exception as e: + self.logger.exception(f"Failed to run a message listener: {e}") + except Exception as e: + self.logger.exception(f"Failed to run message listeners: {e}") + finally: + if self.logger.level <= logging.DEBUG: + self.logger.debug(f"Message processing completed (type: {type})") + + # -------------------------------------------------------------- + # Internals + # -------------------------------------------------------------- + + def session_id(self) -> Optional[str]: + if self.current_session is not None: + return self.current_session.session_id + return None + + def run_all_message_listeners(self, message: str): + if self.logger.level <= logging.DEBUG: + self.logger.debug(f"on_message invoked: (message: {message})") + self.enqueue_message(message) + for listener in self.on_message_listeners: + listener(message) + + def run_all_error_listeners(self, error: Exception): + self.logger.exception( + f"on_error invoked (session id: {self.session_id()}, " f"error: {type(error).__name__}, message: {error})" + ) + for listener in self.on_error_listeners: + listener(error) + + def run_all_close_listeners(self, code: int, reason: Optional[str] = None): + if self.logger.level <= logging.DEBUG: + self.logger.debug(f"on_close invoked (session id: {self.session_id()})") + if self.auto_reconnect_enabled: + self.logger.info("Received CLOSE event. Going to reconnect... " f"(session id: {self.session_id()})") + self.connect_to_new_endpoint() + for listener in self.on_close_listeners: + listener(code, reason) + + def _run_current_session(self): + if self.current_session is not None and self.current_session.is_active(): + session_id = self.session_id() + try: + self.logger.info("Starting to receive messages from a new connection" f" (session id: {session_id})") + self.current_session_state.terminated = False + self.current_session.run_until_completion(self.current_session_state) + self.logger.info("Stopped receiving messages from a connection" f" (session id: {session_id})") + except Exception as e: + self.logger.exception( + "Failed to start or stop the current session" f" (session id: {session_id}, error: {e})" + ) + + def _monitor_current_session(self): + if self.current_app_monitor_started: + try: + self.current_session.check_state() + + if self.auto_reconnect_enabled and (self.current_session is None or not self.current_session.is_active()): + self.logger.info( + "The session seems to be already closed. Going to reconnect... " f"(session id: {self.session_id()})" + ) + self.connect_to_new_endpoint() + except Exception as e: + self.logger.error( + "Failed to check the current session or reconnect to the server " + f"(session id: {self.session_id()}, error: {type(e).__name__}, message: {e})" + ) diff --git a/slack_sdk/scim/__init__.py b/slack_sdk/scim/__init__.py new file mode 100644 index 000000000..25ad76109 --- /dev/null +++ b/slack_sdk/scim/__init__.py @@ -0,0 +1,24 @@ +"""SCIM API is a set of APIs for provisioning and managing user accounts and groups. +SCIM is used by Single Sign-On (SSO) services and identity providers to manage people across a variety of tools, +including Slack. + +Refer to https://docs.slack.dev/tools/python-slack-sdk/scim for details. +""" + +from .v1.client import SCIMClient +from .v1.response import SCIMResponse +from .v1.response import SearchUsersResponse, ReadUserResponse +from .v1.response import SearchGroupsResponse, ReadGroupResponse +from .v1.user import User +from .v1.group import Group + +__all__ = [ + "SCIMClient", + "SCIMResponse", + "SearchUsersResponse", + "ReadUserResponse", + "SearchGroupsResponse", + "ReadGroupResponse", + "User", + "Group", +] diff --git a/slack_sdk/scim/async_client.py b/slack_sdk/scim/async_client.py new file mode 100644 index 000000000..17dd3302f --- /dev/null +++ b/slack_sdk/scim/async_client.py @@ -0,0 +1,5 @@ +from .v1.async_client import AsyncSCIMClient + +__all__ = [ + "AsyncSCIMClient", +] diff --git a/slack_sdk/scim/v1/__init__.py b/slack_sdk/scim/v1/__init__.py new file mode 100644 index 000000000..2e2842568 --- /dev/null +++ b/slack_sdk/scim/v1/__init__.py @@ -0,0 +1,6 @@ +"""SCIM API is a set of APIs for provisioning and managing user accounts and groups. +SCIM is used by Single Sign-On (SSO) services and identity providers to manage people across a variety of tools, +including Slack. + +Refer to https://docs.slack.dev/tools/python-slack-sdk/scim for details. +""" diff --git a/slack_sdk/scim/v1/async_client.py b/slack_sdk/scim/v1/async_client.py new file mode 100644 index 000000000..ad92ac49f --- /dev/null +++ b/slack_sdk/scim/v1/async_client.py @@ -0,0 +1,404 @@ +import json +import logging +from ssl import SSLContext +from typing import Any, Union, List +from typing import Dict, Optional +from urllib.parse import quote + +import aiohttp +from aiohttp import BasicAuth, ClientSession + +from .internal_utils import ( + _build_request_headers, + _debug_log_response, + get_user_agent, + _to_dict_without_not_given, + _build_query, +) +from .response import ( + SCIMResponse, + SearchUsersResponse, + ReadUserResponse, + SearchGroupsResponse, + ReadGroupResponse, + UserCreateResponse, + UserPatchResponse, + UserUpdateResponse, + UserDeleteResponse, + GroupCreateResponse, + GroupPatchResponse, + GroupUpdateResponse, + GroupDeleteResponse, +) +from .user import User +from .group import Group +from ...proxy_env_variable_loader import load_http_proxy_from_env + +from slack_sdk.http_retry.async_handler import AsyncRetryHandler +from slack_sdk.http_retry.builtin_async_handlers import async_default_handlers +from slack_sdk.http_retry.request import HttpRequest as RetryHttpRequest +from slack_sdk.http_retry.response import HttpResponse as RetryHttpResponse +from slack_sdk.http_retry.state import RetryState + + +class AsyncSCIMClient: + BASE_URL = "https://api.slack.com/scim/v1/" + + token: str + timeout: int + ssl: Optional[SSLContext] + proxy: Optional[str] + base_url: str + session: Optional[ClientSession] + trust_env_in_session: bool + auth: Optional[BasicAuth] + default_headers: Dict[str, str] + logger: logging.Logger + retry_handlers: List[AsyncRetryHandler] + + def __init__( + self, + token: str, + timeout: int = 30, + ssl: Optional[SSLContext] = None, + proxy: Optional[str] = None, + base_url: str = BASE_URL, + session: Optional[ClientSession] = None, + trust_env_in_session: bool = False, + auth: Optional[BasicAuth] = None, + default_headers: Optional[Dict[str, str]] = None, + user_agent_prefix: Optional[str] = None, + user_agent_suffix: Optional[str] = None, + logger: Optional[logging.Logger] = None, + retry_handlers: Optional[List[AsyncRetryHandler]] = None, + ): + """API client for SCIM API + See https://docs.slack.dev/admins/scim-api/ for more details + + Args: + token: An admin user's token, which starts with `xoxp-` + timeout: Request timeout (in seconds) + ssl: `ssl.SSLContext` to use for requests + proxy: Proxy URL (e.g., `localhost:9000`, `http://localhost:9000`) + base_url: The base URL for API calls + session: `aiohttp.ClientSession` instance + trust_env_in_session: True/False for `aiohttp.ClientSession` + auth: Basic auth info for `aiohttp.ClientSession` + default_headers: Request headers to add to all requests + user_agent_prefix: Prefix for User-Agent header value + user_agent_suffix: Suffix for User-Agent header value + logger: Custom logger + retry_handlers: Retry handlers + """ + self.token = token + self.timeout = timeout + self.ssl = ssl + self.proxy = proxy + self.base_url = base_url + self.session = session + self.trust_env_in_session = trust_env_in_session + self.auth = auth + self.default_headers = default_headers if default_headers else {} + self.default_headers["User-Agent"] = get_user_agent(user_agent_prefix, user_agent_suffix) + self.logger = logger if logger is not None else logging.getLogger(__name__) + self.retry_handlers = retry_handlers if retry_handlers is not None else async_default_handlers() + + if self.proxy is None or len(self.proxy.strip()) == 0: + env_variable = load_http_proxy_from_env(self.logger) + if env_variable is not None: + self.proxy = env_variable + + # ------------------------- + # Users + # ------------------------- + + async def search_users( + self, + *, + # Pagination required as of August 30, 2019. + count: int, + start_index: int, + filter: Optional[str] = None, + ) -> SearchUsersResponse: + return SearchUsersResponse( + await self.api_call( + http_verb="GET", + path="Users", + query_params={ + "filter": filter, + "count": count, + "startIndex": start_index, + }, + ) + ) + + async def read_user(self, id: str) -> ReadUserResponse: + return ReadUserResponse(await self.api_call(http_verb="GET", path=f"Users/{quote(id)}")) + + async def create_user(self, user: Union[Dict[str, Any], User]) -> UserCreateResponse: + return UserCreateResponse( + await self.api_call( + http_verb="POST", + path="Users", + body_params=user.to_dict() if isinstance(user, User) else _to_dict_without_not_given(user), + ) + ) + + async def patch_user(self, id: str, partial_user: Union[Dict[str, Any], User]) -> UserPatchResponse: + return UserPatchResponse( + await self.api_call( + http_verb="PATCH", + path=f"Users/{quote(id)}", + body_params=( + partial_user.to_dict() if isinstance(partial_user, User) else _to_dict_without_not_given(partial_user) + ), + ) + ) + + async def update_user(self, user: Union[Dict[str, Any], User]) -> UserUpdateResponse: + user_id = user.id if isinstance(user, User) else user["id"] + return UserUpdateResponse( + await self.api_call( + http_verb="PUT", + path=f"Users/{quote(user_id)}", + body_params=user.to_dict() if isinstance(user, User) else _to_dict_without_not_given(user), + ) + ) + + async def delete_user(self, id: str) -> UserDeleteResponse: + return UserDeleteResponse( + await self.api_call( + http_verb="DELETE", + path=f"Users/{quote(id)}", + ) + ) + + # ------------------------- + # Groups + # ------------------------- + + async def search_groups( + self, + *, + # Pagination required as of August 30, 2019. + count: int, + start_index: int, + filter: Optional[str] = None, + ) -> SearchGroupsResponse: + return SearchGroupsResponse( + await self.api_call( + http_verb="GET", + path="Groups", + query_params={ + "filter": filter, + "count": count, + "startIndex": start_index, + }, + ) + ) + + async def read_group(self, id: str) -> ReadGroupResponse: + return ReadGroupResponse(await self.api_call(http_verb="GET", path=f"Groups/{quote(id)}")) + + async def create_group(self, group: Union[Dict[str, Any], Group]) -> GroupCreateResponse: + return GroupCreateResponse( + await self.api_call( + http_verb="POST", + path="Groups", + body_params=group.to_dict() if isinstance(group, Group) else _to_dict_without_not_given(group), + ) + ) + + async def patch_group(self, id: str, partial_group: Union[Dict[str, Any], Group]) -> GroupPatchResponse: + return GroupPatchResponse( + await self.api_call( + http_verb="PATCH", + path=f"Groups/{quote(id)}", + body_params=( + partial_group.to_dict() + if isinstance(partial_group, Group) + else _to_dict_without_not_given(partial_group) + ), + ) + ) + + async def update_group(self, group: Union[Dict[str, Any], Group]) -> GroupUpdateResponse: + group_id = group.id if isinstance(group, Group) else group["id"] + return GroupUpdateResponse( + await self.api_call( + http_verb="PUT", + path=f"Groups/{quote(group_id)}", + body_params=group.to_dict() if isinstance(group, Group) else _to_dict_without_not_given(group), + ) + ) + + async def delete_group(self, id: str) -> GroupDeleteResponse: + return GroupDeleteResponse( + await self.api_call( + http_verb="DELETE", + path=f"Groups/{quote(id)}", + ) + ) + + # ------------------------- + + async def api_call( + self, + *, + http_verb: str, + path: str, + query_params: Optional[Dict[str, Any]] = None, + body_params: Optional[Dict[str, Any]] = None, + headers: Optional[Dict[str, str]] = None, + ) -> SCIMResponse: + url = f"{self.base_url}{path}" + query = _build_query(query_params) + if len(query) > 0: + url += f"?{query}" + return await self._perform_http_request( + http_verb=http_verb, + url=url, + body_params=body_params, + headers=_build_request_headers( + token=self.token, + default_headers=self.default_headers, + additional_headers=headers, + ), + ) + + async def _perform_http_request( + self, + *, + http_verb: str, + url: str, + body_params: Optional[Dict[str, Any]], + headers: Dict[str, str], + ) -> SCIMResponse: + if body_params is not None: + if body_params.get("schemas") is None: + body_params["schemas"] = ["urn:scim:schemas:core:1.0"] + body_params = json.dumps(body_params) + headers["Content-Type"] = "application/json;charset=utf-8" + + session: Optional[ClientSession] = None + use_running_session = self.session and not self.session.closed + if use_running_session: + session = self.session + else: + session = aiohttp.ClientSession( + timeout=aiohttp.ClientTimeout(total=self.timeout), + auth=self.auth, + trust_env=self.trust_env_in_session, + ) + + last_error: Optional[Exception] = None + resp: Optional[SCIMResponse] = None + try: + request_kwargs = { + "headers": headers, + "data": body_params, + "ssl": self.ssl, + "proxy": self.proxy, + } + retry_request = RetryHttpRequest( + method=http_verb, + url=url, + headers=headers, + body_params=body_params, + ) + + retry_state = RetryState() + counter_for_safety = 0 + while counter_for_safety < 100: + counter_for_safety += 1 + # If this is a retry, the next try started here. We can reset the flag. + retry_state.next_attempt_requested = False + retry_response: Optional[RetryHttpResponse] = None + response_body = "" + + if self.logger.level <= logging.DEBUG: + headers_for_logging = { + k: "(redacted)" if k.lower() == "authorization" else v for k, v in headers.items() + } + self.logger.debug( + f"Sending a request - url: {url}, params: {body_params}, headers: {headers_for_logging}" + ) + + try: + async with session.request(http_verb, url, **request_kwargs) as res: + try: + response_body = await res.text() + retry_response = RetryHttpResponse( + status_code=res.status, + headers=res.headers, + data=response_body.encode("utf-8") if response_body is not None else None, + ) + except aiohttp.ContentTypeError: + self.logger.debug(f"No response data returned from the following API call: {url}.") + retry_response = RetryHttpResponse( + status_code=res.status, + headers=res.headers, + ) + + if res.status == 429: + for handler in self.retry_handlers: + if await handler.can_retry_async( + state=retry_state, + request=retry_request, + response=retry_response, + ): + if self.logger.level <= logging.DEBUG: + self.logger.info( + f"A retry handler found: {type(handler).__name__} " + f"for {http_verb} {url} - rate_limited" + ) + await handler.prepare_for_next_attempt_async( + state=retry_state, + request=retry_request, + response=retry_response, + ) + break + + if retry_state.next_attempt_requested is False: + resp = SCIMResponse( + url=url, + status_code=res.status, + raw_body=response_body, + headers=res.headers, + ) + _debug_log_response(self.logger, resp) + return resp + + except Exception as e: + last_error = e + for handler in self.retry_handlers: + if await handler.can_retry_async( + state=retry_state, + request=retry_request, + response=retry_response, + error=e, + ): + if self.logger.level <= logging.DEBUG: + self.logger.info( + f"A retry handler found: {type(handler).__name__} " f"for {http_verb} {url} - {e}" + ) + await handler.prepare_for_next_attempt_async( + state=retry_state, + request=retry_request, + response=retry_response, + error=e, + ) + break + + if retry_state.next_attempt_requested is False: + raise last_error + + if resp is not None: + return resp + raise last_error + + finally: + if not use_running_session: + await session.close() + + return resp diff --git a/slack_sdk/scim/v1/client.py b/slack_sdk/scim/v1/client.py new file mode 100644 index 000000000..82710c6cc --- /dev/null +++ b/slack_sdk/scim/v1/client.py @@ -0,0 +1,421 @@ +"""SCIM API is a set of APIs for provisioning and managing user accounts and groups. +SCIM is used by Single Sign-On (SSO) services and identity providers to manage people across a variety of tools, +including Slack. + +Refer to https://docs.slack.dev/tools/python-slack-sdk/scim/ for details. +""" + +import json +import logging +import urllib +from http.client import HTTPResponse +from ssl import SSLContext +from typing import Dict, Optional, Union, Any, List +from urllib.error import HTTPError +from urllib.parse import quote +from urllib.request import Request, urlopen, OpenerDirector, ProxyHandler, HTTPSHandler + +from slack_sdk.errors import SlackRequestError +from .internal_utils import ( + _build_query, + _build_request_headers, + _debug_log_response, + get_user_agent, + _to_dict_without_not_given, +) +from .response import ( + SCIMResponse, + SearchUsersResponse, + ReadUserResponse, + SearchGroupsResponse, + ReadGroupResponse, + UserCreateResponse, + UserPatchResponse, + UserUpdateResponse, + UserDeleteResponse, + GroupCreateResponse, + GroupPatchResponse, + GroupUpdateResponse, + GroupDeleteResponse, +) +from .user import User +from .group import Group + +from slack_sdk.http_retry import default_retry_handlers +from slack_sdk.http_retry.handler import RetryHandler +from slack_sdk.http_retry.request import HttpRequest as RetryHttpRequest +from slack_sdk.http_retry.response import HttpResponse as RetryHttpResponse +from slack_sdk.http_retry.state import RetryState + +from ...proxy_env_variable_loader import load_http_proxy_from_env + + +class SCIMClient: + BASE_URL = "https://api.slack.com/scim/v1/" + + token: str + timeout: int + ssl: Optional[SSLContext] + proxy: Optional[str] + base_url: str + default_headers: Dict[str, str] + logger: logging.Logger + retry_handlers: List[RetryHandler] + + def __init__( + self, + token: str, + timeout: int = 30, + ssl: Optional[SSLContext] = None, + proxy: Optional[str] = None, + base_url: str = BASE_URL, + default_headers: Optional[Dict[str, str]] = None, + user_agent_prefix: Optional[str] = None, + user_agent_suffix: Optional[str] = None, + logger: Optional[logging.Logger] = None, + retry_handlers: Optional[List[RetryHandler]] = None, + ): + """API client for SCIM API + See https://docs.slack.dev/admins/scim-api/ for more details + + Args: + token: An admin user's token, which starts with `xoxp-` + timeout: Request timeout (in seconds) + ssl: `ssl.SSLContext` to use for requests + proxy: Proxy URL (e.g., `localhost:9000`, `http://localhost:9000`) + base_url: The base URL for API calls + default_headers: Request headers to add to all requests + user_agent_prefix: Prefix for User-Agent header value + user_agent_suffix: Suffix for User-Agent header value + logger: Custom logger + retry_handlers: Retry handlers + """ + self.token = token + self.timeout = timeout + self.ssl = ssl + self.proxy = proxy + self.base_url = base_url + self.default_headers = default_headers if default_headers else {} + self.default_headers["User-Agent"] = get_user_agent(user_agent_prefix, user_agent_suffix) + self.logger = logger if logger is not None else logging.getLogger(__name__) + self.retry_handlers = retry_handlers if retry_handlers is not None else default_retry_handlers() + + if self.proxy is None or len(self.proxy.strip()) == 0: + env_variable = load_http_proxy_from_env(self.logger) + if env_variable is not None: + self.proxy = env_variable + + # ------------------------- + # Users + # ------------------------- + + def search_users( + self, + *, + # Pagination required as of August 30, 2019. + count: int, + start_index: int, + filter: Optional[str] = None, + ) -> SearchUsersResponse: + return SearchUsersResponse( + self.api_call( + http_verb="GET", + path="Users", + query_params={ + "filter": filter, + "count": count, + "startIndex": start_index, + }, + ) + ) + + def read_user(self, id: str) -> ReadUserResponse: + return ReadUserResponse(self.api_call(http_verb="GET", path=f"Users/{quote(id)}")) + + def create_user(self, user: Union[Dict[str, Any], User]) -> UserCreateResponse: + return UserCreateResponse( + self.api_call( + http_verb="POST", + path="Users", + body_params=user.to_dict() if isinstance(user, User) else _to_dict_without_not_given(user), + ) + ) + + def patch_user(self, id: str, partial_user: Union[Dict[str, Any], User]) -> UserPatchResponse: + return UserPatchResponse( + self.api_call( + http_verb="PATCH", + path=f"Users/{quote(id)}", + body_params=( + partial_user.to_dict() if isinstance(partial_user, User) else _to_dict_without_not_given(partial_user) + ), + ) + ) + + def update_user(self, user: Union[Dict[str, Any], User]) -> UserUpdateResponse: + user_id = user.id if isinstance(user, User) else user["id"] + return UserUpdateResponse( + self.api_call( + http_verb="PUT", + path=f"Users/{quote(user_id)}", + body_params=user.to_dict() if isinstance(user, User) else _to_dict_without_not_given(user), + ) + ) + + def delete_user(self, id: str) -> UserDeleteResponse: + return UserDeleteResponse( + self.api_call( + http_verb="DELETE", + path=f"Users/{quote(id)}", + ) + ) + + # ------------------------- + # Groups + # ------------------------- + + def search_groups( + self, + *, + # Pagination required as of August 30, 2019. + count: int, + start_index: int, + filter: Optional[str] = None, + ) -> SearchGroupsResponse: + return SearchGroupsResponse( + self.api_call( + http_verb="GET", + path="Groups", + query_params={ + "filter": filter, + "count": count, + "startIndex": start_index, + }, + ) + ) + + def read_group(self, id: str) -> ReadGroupResponse: + return ReadGroupResponse(self.api_call(http_verb="GET", path=f"Groups/{quote(id)}")) + + def create_group(self, group: Union[Dict[str, Any], Group]) -> GroupCreateResponse: + return GroupCreateResponse( + self.api_call( + http_verb="POST", + path="Groups", + body_params=group.to_dict() if isinstance(group, Group) else _to_dict_without_not_given(group), + ) + ) + + def patch_group(self, id: str, partial_group: Union[Dict[str, Any], Group]) -> GroupPatchResponse: + return GroupPatchResponse( + self.api_call( + http_verb="PATCH", + path=f"Groups/{quote(id)}", + body_params=( + partial_group.to_dict() + if isinstance(partial_group, Group) + else _to_dict_without_not_given(partial_group) + ), + ) + ) + + def update_group(self, group: Union[Dict[str, Any], Group]) -> GroupUpdateResponse: + group_id = group.id if isinstance(group, Group) else group["id"] + return GroupUpdateResponse( + self.api_call( + http_verb="PUT", + path=f"Groups/{quote(group_id)}", + body_params=group.to_dict() if isinstance(group, Group) else _to_dict_without_not_given(group), + ) + ) + + def delete_group(self, id: str) -> GroupDeleteResponse: + return GroupDeleteResponse( + self.api_call( + http_verb="DELETE", + path=f"Groups/{quote(id)}", + ) + ) + + # ------------------------- + + def api_call( + self, + *, + http_verb: str, + path: str, + query_params: Optional[Dict[str, Any]] = None, + body_params: Optional[Dict[str, Any]] = None, + headers: Optional[Dict[str, str]] = None, + ) -> SCIMResponse: + """Performs a Slack API request and returns the result.""" + url = f"{self.base_url}{path}" + query = _build_query(query_params) + if len(query) > 0: + url += f"?{query}" + + return self._perform_http_request( + http_verb=http_verb, + url=url, + body=body_params, + headers=_build_request_headers( + token=self.token, + default_headers=self.default_headers, + additional_headers=headers, + ), + ) + + def _perform_http_request( + self, + *, + http_verb: str = "GET", + url: str, + body: Optional[Dict[str, Any]] = None, + headers: Dict[str, str], + ) -> SCIMResponse: + if body is not None: + if body.get("schemas") is None: + body["schemas"] = ["urn:scim:schemas:core:1.0"] + body = json.dumps(body) + headers["Content-Type"] = "application/json;charset=utf-8" + + if self.logger.level <= logging.DEBUG: + headers_for_logging = {k: "(redacted)" if k.lower() == "authorization" else v for k, v in headers.items()} + self.logger.debug(f"Sending a request - {http_verb} url: {url}, body: {body}, headers: {headers_for_logging}") + + # NOTE: Intentionally ignore the `http_verb` here + # Slack APIs accepts any API method requests with POST methods + req = Request( + method=http_verb, + url=url, + data=body.encode("utf-8") if body is not None else None, + headers=headers, + ) + resp = None + last_error = None + + retry_state = RetryState() + counter_for_safety = 0 + while counter_for_safety < 100: + counter_for_safety += 1 + # If this is a retry, the next try started here. We can reset the flag. + retry_state.next_attempt_requested = False + + try: + resp = self._perform_http_request_internal(url, req) + # The resp is a 200 OK response + return resp + + except HTTPError as e: + # read the response body here + charset = e.headers.get_content_charset() or "utf-8" + response_body: str = e.read().decode(charset) + # As adding new values to HTTPError#headers can be ignored, building a new dict object here + response_headers = dict(e.headers.items()) + resp = SCIMResponse( + url=url, + status_code=e.code, + raw_body=response_body, + headers=response_headers, + ) + if e.code == 429: + # for backward-compatibility with WebClient (v.2.5.0 or older) + if "retry-after" not in resp.headers and "Retry-After" in resp.headers: + resp.headers["retry-after"] = resp.headers["Retry-After"] + if "Retry-After" not in resp.headers and "retry-after" in resp.headers: + resp.headers["Retry-After"] = resp.headers["retry-after"] + _debug_log_response(self.logger, resp) + + # Try to find a retry handler for this error + retry_request = RetryHttpRequest.from_urllib_http_request(req) + retry_response = RetryHttpResponse( + status_code=e.code, + headers={k: [v] for k, v in e.headers.items()}, + data=response_body.encode("utf-8") if response_body is not None else None, + ) + for handler in self.retry_handlers: + if handler.can_retry( + state=retry_state, + request=retry_request, + response=retry_response, + error=e, + ): + if self.logger.level <= logging.DEBUG: + self.logger.info( + f"A retry handler found: {type(handler).__name__} for {req.method} {req.full_url} - {e}" + ) + handler.prepare_for_next_attempt( + state=retry_state, + request=retry_request, + response=retry_response, + error=e, + ) + break + + if retry_state.next_attempt_requested is False: + return resp + + except Exception as err: + last_error = err + self.logger.error(f"Failed to send a request to Slack API server: {err}") + + # Try to find a retry handler for this error + retry_request = RetryHttpRequest.from_urllib_http_request(req) + for handler in self.retry_handlers: + if handler.can_retry( + state=retry_state, + request=retry_request, + response=None, + error=err, + ): + if self.logger.level <= logging.DEBUG: + self.logger.info( + f"A retry handler found: {type(handler).__name__} for {req.method} {req.full_url} - {err}" + ) + handler.prepare_for_next_attempt( + state=retry_state, + request=retry_request, + response=None, + error=err, + ) + self.logger.info(f"Going to retry the same request: {req.method} {req.full_url}") + break + + if retry_state.next_attempt_requested is False: + raise err + + if resp is not None: + return resp + raise last_error + + def _perform_http_request_internal(self, url: str, req: Request) -> SCIMResponse: + opener: Optional[OpenerDirector] = None + # for security (BAN-B310) + if url.lower().startswith("http"): + if self.proxy is not None: + if isinstance(self.proxy, str): + opener = urllib.request.build_opener( + ProxyHandler({"http": self.proxy, "https": self.proxy}), + HTTPSHandler(context=self.ssl), + ) + else: + raise SlackRequestError(f"Invalid proxy detected: {self.proxy} must be a str value") + else: + raise SlackRequestError(f"Invalid URL detected: {url}") + + # NOTE: BAN-B310 is already checked above + http_resp: Optional[HTTPResponse] = None + if opener: + http_resp = opener.open(req, timeout=self.timeout) + else: + http_resp = urlopen(req, context=self.ssl, timeout=self.timeout) + charset: str = http_resp.headers.get_content_charset() or "utf-8" + response_body: str = http_resp.read().decode(charset) + resp = SCIMResponse( + url=url, + status_code=http_resp.status, + raw_body=response_body, + headers=http_resp.headers, + ) + _debug_log_response(self.logger, resp) + return resp diff --git a/slack_sdk/scim/v1/default_arg.py b/slack_sdk/scim/v1/default_arg.py new file mode 100644 index 000000000..52dca49c4 --- /dev/null +++ b/slack_sdk/scim/v1/default_arg.py @@ -0,0 +1,5 @@ +class DefaultArg: + pass + + +NotGiven = DefaultArg() diff --git a/slack_sdk/scim/v1/group.py b/slack_sdk/scim/v1/group.py new file mode 100644 index 000000000..5bee76460 --- /dev/null +++ b/slack_sdk/scim/v1/group.py @@ -0,0 +1,78 @@ +from typing import Optional, List, Union, Dict, Any + +from .default_arg import DefaultArg, NotGiven +from .internal_utils import _to_dict_without_not_given, _is_iterable + + +class GroupMember: + display: Union[Optional[str], DefaultArg] + value: Union[Optional[str], DefaultArg] + unknown_fields: Dict[str, Any] + + def __init__( + self, + *, + display: Union[Optional[str], DefaultArg] = NotGiven, + value: Union[Optional[str], DefaultArg] = NotGiven, + **kwargs, + ) -> None: + self.display = display + self.value = value + self.unknown_fields = kwargs + + def to_dict(self): + return _to_dict_without_not_given(self) + + +class GroupMeta: + created: Union[Optional[str], DefaultArg] + location: Union[Optional[str], DefaultArg] + unknown_fields: Dict[str, Any] + + def __init__( + self, + *, + created: Union[Optional[str], DefaultArg] = NotGiven, + location: Union[Optional[str], DefaultArg] = NotGiven, + **kwargs, + ) -> None: + self.created = created + self.location = location + self.unknown_fields = kwargs + + def to_dict(self): + return _to_dict_without_not_given(self) + + +class Group: + display_name: Union[Optional[str], DefaultArg] + id: Union[Optional[str], DefaultArg] + members: Union[Optional[List[GroupMember]], DefaultArg] + meta: Union[Optional[GroupMeta], DefaultArg] + schemas: Union[Optional[List[str]], DefaultArg] + unknown_fields: Dict[str, Any] + + def __init__( + self, + *, + display_name: Union[Optional[str], DefaultArg] = NotGiven, + id: Union[Optional[str], DefaultArg] = NotGiven, + members: Union[Optional[List[GroupMember]], DefaultArg] = NotGiven, + meta: Union[Optional[GroupMeta], DefaultArg] = NotGiven, + schemas: Union[Optional[List[str]], DefaultArg] = NotGiven, + **kwargs, + ) -> None: + self.display_name = display_name + self.id = id + self.members = ( + [a if isinstance(a, GroupMember) else GroupMember(**a) for a in members] if _is_iterable(members) else members + ) + self.meta = GroupMeta(**meta) if meta is not None and isinstance(meta, dict) else meta + self.schemas = schemas + self.unknown_fields = kwargs + + def to_dict(self): + return _to_dict_without_not_given(self) + + def __repr__(self): + return f"" diff --git a/slack_sdk/scim/v1/internal_utils.py b/slack_sdk/scim/v1/internal_utils.py new file mode 100644 index 000000000..cbb90dd72 --- /dev/null +++ b/slack_sdk/scim/v1/internal_utils.py @@ -0,0 +1,145 @@ +import copy +import logging +import re +from typing import Dict, Callable +from typing import Union, Optional, Any +from urllib.parse import quote + +from .default_arg import DefaultArg, NotGiven +from slack_sdk.web.internal_utils import get_user_agent + + +def _build_query(params: Optional[Dict[str, Any]]) -> str: + if params is not None and len(params) > 0: + return "&".join({f"{quote(str(k))}={quote(str(v))}" for k, v in params.items() if v is not None}) + return "" + + +def _is_iterable(obj: Union[Optional[Any], DefaultArg]) -> bool: + return obj is not None and obj is not NotGiven + + +def _to_dict_without_not_given(obj: Any) -> dict: + dict_value = {} + given_dict = obj if isinstance(obj, dict) else vars(obj) + for key, value in given_dict.items(): + if key == "unknown_fields": + if value is not None: + converted = _to_dict_without_not_given(value) + dict_value.update(converted) + continue + + dict_key = _to_camel_case_key(key) + if value is NotGiven: + continue + if isinstance(value, list): + dict_value[dict_key] = [elem.to_dict() if hasattr(elem, "to_dict") else elem for elem in value] + elif isinstance(value, dict): + dict_value[dict_key] = _to_dict_without_not_given(value) + else: + dict_value[dict_key] = value.to_dict() if hasattr(value, "to_dict") else value + return dict_value + + +def _create_copy(original: Any) -> Any: + return copy.deepcopy(original) + + +def _to_camel_case_key(key: str) -> str: + next_to_capital = False + result = "" + for c in key: + if c == "_": + next_to_capital = True + elif next_to_capital: + result += c.upper() + next_to_capital = False + else: + result += c + return result + + +def _to_snake_cased(original: Optional[Dict[str, Any]]) -> Optional[Dict[str, Any]]: + return _convert_dict_keys( + original, + {}, + lambda s: re.sub( + "^_", + "", + "".join(["_" + c.lower() if c.isupper() else c for c in s]), + ), + ) + + +def _to_camel_cased(original: Optional[Dict[str, Any]]) -> Optional[Dict[str, Any]]: + return _convert_dict_keys( + original, + {}, + _to_camel_case_key, + ) + + +def _convert_dict_keys( + original_dict: Optional[Dict[str, Any]], + result_dict: Dict[str, Any], + convert: Callable[[str], str], +) -> Optional[Dict[str, Any]]: + if original_dict is None: + return result_dict + + for original_key, original_value in original_dict.items(): + new_key = convert(original_key) + if isinstance(original_value, dict): + result_dict[new_key] = {} + new_value = _convert_dict_keys(original_value, result_dict[new_key], convert) + result_dict[new_key] = new_value + elif isinstance(original_value, list): + result_dict[new_key] = [] + is_dict = len(original_value) > 0 and isinstance(original_value[0], dict) + for element in original_value: + if is_dict: + if isinstance(element, dict): + new_element = {} + for elem_key, elem_value in element.items(): + new_element[convert(elem_key)] = ( + _convert_dict_keys(elem_value, {}, convert) + if isinstance(elem_value, dict) + else _create_copy(elem_value) + ) + result_dict[new_key].append(new_element) + else: + result_dict[new_key].append(_create_copy(original_value)) + else: + result_dict[new_key] = _create_copy(original_value) + return result_dict + + +def _build_request_headers( + token: str, + default_headers: Dict[str, str], + additional_headers: Optional[Dict[str, str]], +) -> Dict[str, str]: + request_headers = { + "Content-Type": "application/json;charset=utf-8", + "Authorization": f"Bearer {token}", + } + if default_headers is None or "User-Agent" not in default_headers: + request_headers["User-Agent"] = get_user_agent() + if default_headers is not None: + request_headers.update(default_headers) + if additional_headers is not None: + request_headers.update(additional_headers) + return request_headers + + +def _debug_log_response( + logger, + resp: "SCIMResponse", # noqa: F821 +) -> None: + if logger.level <= logging.DEBUG: + logger.debug( + "Received the following response - " + f"status: {resp.status_code}, " + f"headers: {(dict(resp.headers))}, " + f"body: {resp.raw_body}" + ) diff --git a/slack_sdk/scim/v1/response.py b/slack_sdk/scim/v1/response.py new file mode 100644 index 000000000..5f1495b21 --- /dev/null +++ b/slack_sdk/scim/v1/response.py @@ -0,0 +1,263 @@ +import json +from typing import Dict, Any, List, Optional + +from slack_sdk.scim.v1.group import Group +from slack_sdk.scim.v1.internal_utils import _to_snake_cased +from slack_sdk.scim.v1.user import User + + +class Errors: + code: int + description: str + + def __init__(self, code: int, description: str) -> None: + self.code = code + self.description = description + + def to_dict(self) -> dict: + return {"code": self.code, "description": self.description} + + +class SCIMResponse: + url: str + status_code: int + headers: Dict[str, Any] + raw_body: Optional[str] + body: Optional[Dict[str, Any]] + snake_cased_body: Optional[Dict[str, Any]] + + errors: Optional[Errors] + + @property + def snake_cased_body(self) -> Optional[Dict[str, Any]]: + if self._snake_cased_body is None: + self._snake_cased_body = _to_snake_cased(self.body) + return self._snake_cased_body + + @property + def errors(self) -> Optional[Errors]: + errors = self.snake_cased_body.get("errors") + if errors is None: + return None + return Errors(**errors) + + def __init__( + self, + *, + url: str, + status_code: int, + raw_body: Optional[str], + headers: dict, + ): + self.url = url + self.status_code = status_code + self.headers = headers + self.raw_body = raw_body + self.body = json.loads(raw_body) if raw_body is not None and raw_body.startswith("{") else None + self._snake_cased_body = None # build this when it's accessed for the first time + + def __repr__(self): + dict_value = {} + for key, value in vars(self).items(): + dict_value[key] = value.to_dict() if hasattr(value, "to_dict") else value + + if dict_value: + return f"" + else: + return self.__str__() + + +# --------------------------------- +# Users +# --------------------------------- + + +class SearchUsersResponse(SCIMResponse): + users: List[User] + + @property + def users(self) -> List[User]: + return [User(**r) for r in self.snake_cased_body.get("resources")] + + def __init__(self, underlying: SCIMResponse): + self.underlying = underlying + self.url = underlying.url + self.status_code = underlying.status_code + self.headers = underlying.headers + self.raw_body = underlying.raw_body + self.body = underlying.body + self._snake_cased_body = None + + +class ReadUserResponse(SCIMResponse): + user: User + + @property + def user(self) -> User: + return User(**self.snake_cased_body) + + def __init__(self, underlying: SCIMResponse): + self.underlying = underlying + self.url = underlying.url + self.status_code = underlying.status_code + self.headers = underlying.headers + self.raw_body = underlying.raw_body + self.body = underlying.body + self._snake_cased_body = None + + +class UserCreateResponse(SCIMResponse): + user: User + + @property + def user(self) -> User: + return User(**self.snake_cased_body) + + def __init__(self, underlying: SCIMResponse): + self.underlying = underlying + self.url = underlying.url + self.status_code = underlying.status_code + self.headers = underlying.headers + self.raw_body = underlying.raw_body + self.body = underlying.body + self._snake_cased_body = None + + +class UserPatchResponse(SCIMResponse): + user: User + + @property + def user(self) -> User: + return User(**self.snake_cased_body) + + def __init__(self, underlying: SCIMResponse): + self.underlying = underlying + self.url = underlying.url + self.status_code = underlying.status_code + self.headers = underlying.headers + self.raw_body = underlying.raw_body + self.body = underlying.body + self._snake_cased_body = None + + +class UserUpdateResponse(SCIMResponse): + user: User + + @property + def user(self) -> User: + return User(**self.snake_cased_body) + + def __init__(self, underlying: SCIMResponse): + self.underlying = underlying + self.url = underlying.url + self.status_code = underlying.status_code + self.headers = underlying.headers + self.raw_body = underlying.raw_body + self.body = underlying.body + self._snake_cased_body = None + + +class UserDeleteResponse(SCIMResponse): + def __init__(self, underlying: SCIMResponse): + self.underlying = underlying + self.url = underlying.url + self.status_code = underlying.status_code + self.headers = underlying.headers + self.raw_body = underlying.raw_body + self.body = underlying.body + self._snake_cased_body = None + + +# --------------------------------- +# Groups +# --------------------------------- + + +class SearchGroupsResponse(SCIMResponse): + groups: List[Group] + + @property + def groups(self) -> List[Group]: + return [Group(**r) for r in self.snake_cased_body.get("resources")] + + def __init__(self, underlying: SCIMResponse): + self.underlying = underlying + self.url = underlying.url + self.status_code = underlying.status_code + self.headers = underlying.headers + self.raw_body = underlying.raw_body + self.body = underlying.body + self._snake_cased_body = None + + +class ReadGroupResponse(SCIMResponse): + group: Group + + @property + def group(self) -> Group: + return Group(**self.snake_cased_body) + + def __init__(self, underlying: SCIMResponse): + self.underlying = underlying + self.url = underlying.url + self.status_code = underlying.status_code + self.headers = underlying.headers + self.raw_body = underlying.raw_body + self.body = underlying.body + self._snake_cased_body = None + + +class GroupCreateResponse(SCIMResponse): + group: Group + + @property + def group(self) -> Group: + return Group(**self.snake_cased_body) + + def __init__(self, underlying: SCIMResponse): + self.underlying = underlying + self.url = underlying.url + self.status_code = underlying.status_code + self.headers = underlying.headers + self.raw_body = underlying.raw_body + self.body = underlying.body + self._snake_cased_body = None + + +class GroupPatchResponse(SCIMResponse): + def __init__(self, underlying: SCIMResponse): + self.underlying = underlying + self.url = underlying.url + self.status_code = underlying.status_code + self.headers = underlying.headers + self.raw_body = underlying.raw_body + self.body = underlying.body + self._snake_cased_body = None + + +class GroupUpdateResponse(SCIMResponse): + group: Group + + @property + def group(self) -> Group: + return Group(**self.snake_cased_body) + + def __init__(self, underlying: SCIMResponse): + self.underlying = underlying + self.url = underlying.url + self.status_code = underlying.status_code + self.headers = underlying.headers + self.raw_body = underlying.raw_body + self.body = underlying.body + self._snake_cased_body = None + + +class GroupDeleteResponse(SCIMResponse): + def __init__(self, underlying: SCIMResponse): + self.underlying = underlying + self.url = underlying.url + self.status_code = underlying.status_code + self.headers = underlying.headers + self.raw_body = underlying.raw_body + self.body = underlying.body + self._snake_cased_body = None diff --git a/slack_sdk/scim/v1/types.py b/slack_sdk/scim/v1/types.py new file mode 100644 index 000000000..db9c7445d --- /dev/null +++ b/slack_sdk/scim/v1/types.py @@ -0,0 +1,27 @@ +from typing import Optional, Union, Dict, Any + +from .default_arg import DefaultArg, NotGiven +from .internal_utils import _to_dict_without_not_given + + +class TypeAndValue: + primary: Union[Optional[bool], DefaultArg] + type: Union[Optional[str], DefaultArg] + value: Union[Optional[str], DefaultArg] + unknown_fields: Dict[str, Any] + + def __init__( + self, + *, + primary: Union[Optional[bool], DefaultArg] = NotGiven, + type: Union[Optional[str], DefaultArg] = NotGiven, + value: Union[Optional[str], DefaultArg] = NotGiven, + **kwargs, + ) -> None: + self.primary = primary + self.type = type + self.value = value + self.unknown_fields = kwargs + + def to_dict(self) -> dict: + return _to_dict_without_not_given(self) diff --git a/slack_sdk/scim/v1/user.py b/slack_sdk/scim/v1/user.py new file mode 100644 index 000000000..bd8df2213 --- /dev/null +++ b/slack_sdk/scim/v1/user.py @@ -0,0 +1,222 @@ +from typing import Optional, Any, List, Dict, Union + +from .default_arg import DefaultArg, NotGiven +from .internal_utils import _to_dict_without_not_given, _is_iterable +from .types import TypeAndValue + + +class UserAddress: + country: Union[Optional[str], DefaultArg] + locality: Union[Optional[str], DefaultArg] + postal_code: Union[Optional[str], DefaultArg] + primary: Union[Optional[bool], DefaultArg] + region: Union[Optional[str], DefaultArg] + street_address: Union[Optional[str], DefaultArg] + unknown_fields: Dict[str, Any] + + def __init__( + self, + *, + country: Union[Optional[str], DefaultArg] = NotGiven, + locality: Union[Optional[str], DefaultArg] = NotGiven, + postal_code: Union[Optional[str], DefaultArg] = NotGiven, + primary: Union[Optional[bool], DefaultArg] = NotGiven, + region: Union[Optional[str], DefaultArg] = NotGiven, + street_address: Union[Optional[str], DefaultArg] = NotGiven, + **kwargs, + ) -> None: + self.country = country + self.locality = locality + self.postal_code = postal_code + self.primary = primary + self.region = region + self.street_address = street_address + self.unknown_fields = kwargs + + def to_dict(self) -> dict: + return _to_dict_without_not_given(self) + + +class UserEmail(TypeAndValue): + pass + + +class UserPhoneNumber(TypeAndValue): + pass + + +class UserRole(TypeAndValue): + pass + + +class UserGroup: + display: Union[Optional[str], DefaultArg] + value: Union[Optional[str], DefaultArg] + unknown_fields: Dict[str, Any] + + def __init__( + self, + *, + display: Union[Optional[str], DefaultArg] = NotGiven, + value: Union[Optional[str], DefaultArg] = NotGiven, + **kwargs, + ) -> None: + self.display = display + self.value = value + self.unknown_fields = kwargs + + def to_dict(self) -> dict: + return _to_dict_without_not_given(self) + + +class UserMeta: + created: Union[Optional[str], DefaultArg] + location: Union[Optional[str], DefaultArg] + unknown_fields: Dict[str, Any] + + def __init__( + self, + created: Union[Optional[str], DefaultArg] = NotGiven, + location: Union[Optional[str], DefaultArg] = NotGiven, + **kwargs, + ) -> None: + self.created = created + self.location = location + self.unknown_fields = kwargs + + def to_dict(self) -> dict: + return _to_dict_without_not_given(self) + + +class UserName: + family_name: Union[Optional[str], DefaultArg] + given_name: Union[Optional[str], DefaultArg] + unknown_fields: Dict[str, Any] + + def __init__( + self, + family_name: Union[Optional[str], DefaultArg] = NotGiven, + given_name: Union[Optional[str], DefaultArg] = NotGiven, + **kwargs, + ) -> None: + self.family_name = family_name + self.given_name = given_name + self.unknown_fields = kwargs + + def to_dict(self) -> dict: + return _to_dict_without_not_given(self) + + +class UserPhoto: + type: Union[Optional[str], DefaultArg] + value: Union[Optional[str], DefaultArg] + unknown_fields: Dict[str, Any] + + def __init__( + self, + type: Union[Optional[str], DefaultArg] = NotGiven, + value: Union[Optional[str], DefaultArg] = NotGiven, + **kwargs, + ) -> None: + self.type = type + self.value = value + self.unknown_fields = kwargs + + def to_dict(self) -> dict: + return _to_dict_without_not_given(self) + + +class User: + active: Union[Optional[bool], DefaultArg] + addresses: Union[Optional[List[UserAddress]], DefaultArg] + display_name: Union[Optional[str], DefaultArg] + emails: Union[Optional[List[TypeAndValue]], DefaultArg] + external_id: Union[Optional[str], DefaultArg] + groups: Union[Optional[List[UserGroup]], DefaultArg] + id: Union[Optional[str], DefaultArg] + meta: Union[Optional[UserMeta], DefaultArg] + name: Union[Optional[UserName], DefaultArg] + nick_name: Union[Optional[str], DefaultArg] + phone_numbers: Union[Optional[List[TypeAndValue]], DefaultArg] + photos: Union[Optional[List[UserPhoto]], DefaultArg] + profile_url: Union[Optional[str], DefaultArg] + roles: Union[Optional[List[TypeAndValue]], DefaultArg] + schemas: Union[Optional[List[str]], DefaultArg] + timezone: Union[Optional[str], DefaultArg] + title: Union[Optional[str], DefaultArg] + user_name: Union[Optional[str], DefaultArg] + unknown_fields: Dict[str, Any] + + def __init__( + self, + *, + active: Union[Optional[bool], DefaultArg] = NotGiven, + addresses: Union[Optional[List[Union[UserAddress, Dict[str, Any]]]], DefaultArg] = NotGiven, + display_name: Union[Optional[str], DefaultArg] = NotGiven, + emails: Union[Optional[List[Union[TypeAndValue, Dict[str, Any]]]], DefaultArg] = NotGiven, + external_id: Union[Optional[str], DefaultArg] = NotGiven, + groups: Union[Optional[List[Union[UserGroup, Dict[str, Any]]]], DefaultArg] = NotGiven, + id: Union[Optional[str], DefaultArg] = NotGiven, + meta: Union[Optional[Union[UserMeta, Dict[str, Any]]], DefaultArg] = NotGiven, + name: Union[Optional[Union[UserName, Dict[str, Any]]], DefaultArg] = NotGiven, + nick_name: Union[Optional[str], DefaultArg] = NotGiven, + phone_numbers: Union[Optional[List[Union[TypeAndValue, Dict[str, Any]]]], DefaultArg] = NotGiven, + photos: Union[Optional[List[Union[UserPhoto, Dict[str, Any]]]], DefaultArg] = NotGiven, + profile_url: Union[Optional[str], DefaultArg] = NotGiven, + roles: Union[Optional[List[Union[TypeAndValue, Dict[str, Any]]]], DefaultArg] = NotGiven, + schemas: Union[Optional[List[str]], DefaultArg] = NotGiven, + timezone: Union[Optional[str], DefaultArg] = NotGiven, + title: Union[Optional[str], DefaultArg] = NotGiven, + user_name: Union[Optional[str], DefaultArg] = NotGiven, + **kwargs, + ) -> None: + self.active = active + self.addresses = ( + [a if isinstance(a, UserAddress) else UserAddress(**a) for a in addresses] # type: ignore + if _is_iterable(addresses) + else addresses + ) + self.display_name = display_name + self.emails = ( + [a if isinstance(a, TypeAndValue) else TypeAndValue(**a) for a in emails] # type: ignore + if _is_iterable(emails) + else emails + ) + self.external_id = external_id + self.groups = ( + [a if isinstance(a, UserGroup) else UserGroup(**a) for a in groups] # type: ignore + if _is_iterable(groups) + else groups + ) + self.id = id + self.meta = UserMeta(**meta) if meta is not None and isinstance(meta, dict) else meta + self.name = UserName(**name) if name is not None and isinstance(name, dict) else name + self.nick_name = nick_name + self.phone_numbers = ( + [a if isinstance(a, TypeAndValue) else TypeAndValue(**a) for a in phone_numbers] # type: ignore + if _is_iterable(phone_numbers) + else phone_numbers + ) + self.photos = ( + [a if isinstance(a, UserPhoto) else UserPhoto(**a) for a in photos] # type: ignore + if _is_iterable(photos) + else photos + ) + self.profile_url = profile_url + self.roles = ( + [a if isinstance(a, TypeAndValue) else TypeAndValue(**a) for a in roles] # type: ignore + if _is_iterable(roles) + else roles + ) + self.schemas = schemas + self.timezone = timezone + self.title = title + self.user_name = user_name + + self.unknown_fields = kwargs + + def to_dict(self): + return _to_dict_without_not_given(self) + + def __repr__(self): + return f"" diff --git a/slack_sdk/signature/__init__.py b/slack_sdk/signature/__init__.py new file mode 100644 index 000000000..aa46e5348 --- /dev/null +++ b/slack_sdk/signature/__init__.py @@ -0,0 +1,72 @@ +"""Slack request signature verifier""" + +import hashlib +import hmac +from time import time +from typing import Dict, Optional, Union + + +class Clock: + def now(self) -> float: + return time() + + +class SignatureVerifier: + def __init__(self, signing_secret: str, clock: Clock = Clock()): + """Slack request signature verifier + + Slack signs its requests using a secret that's unique to your app. + With the help of signing secrets, your app can more confidently verify + whether requests from us are authentic. + https://docs.slack.dev/authentication/verifying-requests-from-slack/ + """ + self.signing_secret = signing_secret + self.clock = clock + + def is_valid_request( + self, + body: Union[str, bytes], + headers: Dict[str, str], + ) -> bool: + """Verifies if the given signature is valid""" + if headers is None: + return False + normalized_headers = {k.lower(): v for k, v in headers.items()} + return self.is_valid( + body=body, + timestamp=normalized_headers.get("x-slack-request-timestamp", None), # type: ignore[arg-type] + signature=normalized_headers.get("x-slack-signature", None), # type: ignore[arg-type] + ) + + def is_valid( + self, + body: Union[str, bytes], + timestamp: str, + signature: str, + ) -> bool: + """Verifies if the given signature is valid""" + if timestamp is None or signature is None: + return False + + if abs(self.clock.now() - int(timestamp)) > 60 * 5: + return False + + calculated_signature = self.generate_signature(timestamp=timestamp, body=body) + if calculated_signature is None: + return False + return hmac.compare_digest(calculated_signature, signature) + + def generate_signature(self, *, timestamp: str, body: Union[str, bytes]) -> Optional[str]: + """Generates a signature""" + if timestamp is None: + return None + if body is None: + body = "" + if isinstance(body, bytes): + body = body.decode("utf-8") + + format_req = str.encode(f"v0:{timestamp}:{body}") + encoded_secret = str.encode(self.signing_secret) + request_hash = hmac.new(encoded_secret, format_req, hashlib.sha256).hexdigest() + calculated_signature = f"v0={request_hash}" + return calculated_signature diff --git a/slack_sdk/socket_mode/__init__.py b/slack_sdk/socket_mode/__init__.py new file mode 100644 index 000000000..b8e1883c1 --- /dev/null +++ b/slack_sdk/socket_mode/__init__.py @@ -0,0 +1,12 @@ +"""Socket Mode is a method of connecting your app to Slack’s APIs using WebSockets instead of HTTP. +You can use slack_sdk.socket_mode.SocketModeClient for managing Socket Mode connections +and performing interactions with Slack. + +https://docs.slack.dev/apis/events-api/using-socket-mode/ +""" + +from .builtin import SocketModeClient + +__all__ = [ + "SocketModeClient", +] diff --git a/slack_sdk/socket_mode/aiohttp/__init__.py b/slack_sdk/socket_mode/aiohttp/__init__.py new file mode 100644 index 000000000..7fde17752 --- /dev/null +++ b/slack_sdk/socket_mode/aiohttp/__init__.py @@ -0,0 +1,463 @@ +"""aiohttp based Socket Mode client + +* https://docs.slack.dev/apis/events-api/using-socket-mode/ +* https://docs.slack.dev/tools/python-slack-sdk/socket-mode/ +* https://pypi.org/project/aiohttp/ + +""" + +import asyncio +import logging +import time +from asyncio import AbstractEventLoop +from asyncio import Future, Lock +from asyncio import Queue +from logging import Logger +from typing import Union, Optional, List, Callable, Awaitable + +import aiohttp +from aiohttp import ClientWebSocketResponse, WSMessage, WSMsgType, ClientConnectionError + +from slack_sdk.proxy_env_variable_loader import load_http_proxy_from_env +from slack_sdk.socket_mode.async_client import AsyncBaseSocketModeClient +from slack_sdk.socket_mode.async_listeners import ( + AsyncWebSocketMessageListener, + AsyncSocketModeRequestListener, +) +from slack_sdk.socket_mode.request import SocketModeRequest +from slack_sdk.web.async_client import AsyncWebClient + + +class SocketModeClient(AsyncBaseSocketModeClient): + logger: Logger + web_client: AsyncWebClient + app_token: str + wss_uri: Optional[str] # type: ignore[assignment] + auto_reconnect_enabled: bool + message_queue: Queue + message_listeners: List[ + Union[ + AsyncWebSocketMessageListener, + Callable[["AsyncBaseSocketModeClient", dict, Optional[str]], Awaitable[None]], + ] + ] + socket_mode_request_listeners: List[ + Union[ + AsyncSocketModeRequestListener, + Callable[["AsyncBaseSocketModeClient", SocketModeRequest], Awaitable[None]], + ] + ] + + message_receiver: Optional[Future] + message_processor: Future + + proxy: Optional[str] + ping_interval: float + trace_enabled: bool + + last_ping_pong_time: Optional[float] + current_session: Optional[ClientWebSocketResponse] + current_session_monitor: Optional[Future] + + default_auto_reconnect_enabled: bool + closed: bool + stale: bool + connect_operation_lock: Lock + + on_message_listeners: List[Callable[[WSMessage], Awaitable[None]]] + on_error_listeners: List[Callable[[WSMessage], Awaitable[None]]] + on_close_listeners: List[Callable[[WSMessage], Awaitable[None]]] + + def __init__( + self, + app_token: str, + logger: Optional[Logger] = None, + web_client: Optional[AsyncWebClient] = None, + proxy: Optional[str] = None, + auto_reconnect_enabled: bool = True, + ping_interval: float = 5, + trace_enabled: bool = False, + on_message_listeners: Optional[List[Callable[[WSMessage], Awaitable[None]]]] = None, + on_error_listeners: Optional[List[Callable[[WSMessage], Awaitable[None]]]] = None, + on_close_listeners: Optional[List[Callable[[WSMessage], Awaitable[None]]]] = None, + loop: Optional[AbstractEventLoop] = None, + ): + """Socket Mode client + + Args: + app_token: App-level token + logger: Custom logger + web_client: Web API client + auto_reconnect_enabled: True if automatic reconnection is enabled (default: True) + ping_interval: interval for ping-pong with Slack servers (seconds) + trace_enabled: True if more verbose logs to see what's happening under the hood + proxy: the HTTP proxy URL + on_message_listeners: listener functions for on_message + on_error_listeners: listener functions for on_error + on_close_listeners: listener functions for on_close + loop: an existing asyncio event loop + """ + self.app_token = app_token + self.logger = logger or logging.getLogger(__name__) + self.web_client = web_client or AsyncWebClient() + self.closed = False + self.stale = False + self.connect_operation_lock = Lock() + self.proxy = proxy + if self.proxy is None or len(self.proxy.strip()) == 0: + env_variable = load_http_proxy_from_env(self.logger) + if env_variable is not None: + self.proxy = env_variable + + self.default_auto_reconnect_enabled = auto_reconnect_enabled + self.auto_reconnect_enabled = self.default_auto_reconnect_enabled + self.ping_interval = ping_interval + self.trace_enabled = trace_enabled + self.last_ping_pong_time = None + + self.wss_uri = None + self.message_queue = Queue() + self.message_listeners = [] + self.socket_mode_request_listeners = [] + self.current_session = None + self.current_session_monitor = None + + # https://docs.aiohttp.org/en/stable/client_reference.html + # Unless you are connecting to a large, unknown number of different servers + # over the lifetime of your application, + # it is suggested you use a single session for the lifetime of your application + # to benefit from connection pooling. + self.aiohttp_client_session = aiohttp.ClientSession(loop=loop) + + self.on_message_listeners = on_message_listeners or [] + self.on_error_listeners = on_error_listeners or [] + self.on_close_listeners = on_close_listeners or [] + + self.message_receiver = None + self.message_processor = asyncio.ensure_future(self.process_messages()) + + async def monitor_current_session(self) -> None: + # In the asyncio runtime, accessing a shared object (self.current_session here) from + # multiple tasks can cause race conditions and errors. + # To avoid such, we access only the session that is active when this loop starts. + session: ClientWebSocketResponse = self.current_session # type: ignore[assignment] + session_id: str = self.build_session_id(session) + + if self.logger.level <= logging.DEBUG: + self.logger.debug(f"A new monitor_current_session() execution loop for {session_id} started") + try: + logging_interval = 100 + counter_for_logging = 0 + + while not self.closed: + if session != self.current_session: + if self.logger.level <= logging.DEBUG: + self.logger.debug(f"The monitor_current_session task for {session_id} is now cancelled") + break + try: + if self.trace_enabled and self.logger.level <= logging.DEBUG: + # The logging here is for detailed investigation on potential issues in this client. + # If you don't see this log for a while, it means that + # this receive_messages execution is no longer working for some reason. + counter_for_logging += 1 + if counter_for_logging >= logging_interval: + counter_for_logging = 0 + log_message = ( + "#monitor_current_session method has been verifying if this session is active " + f"(session: {session_id}, logging interval: {logging_interval})" + ) + self.logger.debug(log_message) + + await asyncio.sleep(self.ping_interval) + + if session is not None and session.closed is False: + t = time.time() + if self.last_ping_pong_time is None: + self.last_ping_pong_time = float(t) + try: + await session.ping(f"sdk-ping-pong:{t}".encode("utf-8")) + except Exception as e: + # The ping() method can fail for some reason. + # To establish a new connection even in this scenario, + # we ignore the exception here. + self.logger.warning(f"Failed to send a ping message ({session_id}): {e}") + + if self.auto_reconnect_enabled: + should_reconnect = False + if session is None or session.closed: + self.logger.info(f"The session ({session_id}) seems to be already closed. Reconnecting...") + should_reconnect = True + + if await self.is_ping_pong_failing(): + disconnected_seconds = int(time.time() - self.last_ping_pong_time) # type: ignore[operator] + self.logger.info( + f"The session ({session_id}) seems to be stale. Reconnecting..." + f" reason: disconnected for {disconnected_seconds}+ seconds)" + ) + self.stale = True + self.last_ping_pong_time = None + should_reconnect = True + + if should_reconnect is True or not await self.is_connected(): + await self.connect_to_new_endpoint() + + except Exception as e: + self.logger.error( + f"Failed to check the current session ({session_id}) or reconnect to the server " + f"(error: {type(e).__name__}, message: {e})" + ) + except asyncio.CancelledError: + if self.logger.level <= logging.DEBUG: + self.logger.debug(f"The monitor_current_session task for {session_id} is now cancelled") + raise + + async def receive_messages(self) -> None: + # In the asyncio runtime, accessing a shared object (self.current_session here) from + # multiple tasks can cause race conditions and errors. + # To avoid such, we access only the session that is active when this loop starts. + session = self.current_session + session_id = self.build_session_id(session) # type: ignore[arg-type] + if self.logger.level <= logging.DEBUG: + self.logger.debug(f"A new receive_messages() execution loop with {session_id} started") + try: + consecutive_error_count = 0 + logging_interval = 100 + counter_for_logging = 0 + + while not self.closed: + if session != self.current_session: + if self.logger.level <= logging.DEBUG: + self.logger.debug(f"The running receive_messages task for {session_id} is now cancelled") + break + try: + message: WSMessage = await session.receive() # type: ignore[union-attr] + # just in case, checking if the value is not None + if message is not None: + if self.logger.level <= logging.DEBUG: + # The following logging prints every single received message + # except empty message data ones. + m_type = WSMsgType(message.type) + message_type = m_type.name if m_type is not None else message.type + message_data = message.data + if isinstance(message_data, bytes): + message_data = message_data.decode("utf-8") + if message_data is not None and isinstance(message_data, (str, bytes)) and len(message_data) > 0: + # To skip the empty message that Slack server-side often sends + self.logger.debug( + f"Received message " + f"(type: {message_type}, " + f"data: {message_data}, " + f"extra: {message.extra}, " + f"session: {session_id})" + ) + + if self.trace_enabled: + # The logging here is for detailed trouble shooting of potential issues in this client. + # If you don't see this log for a while, it can mean that + # this receive_messages execution is no longer working for some reason. + counter_for_logging += 1 + if counter_for_logging >= logging_interval: + counter_for_logging = 0 + log_message = ( + "#receive_messages method has been working without any issues " + f"(session: {session_id}, logging interval: {logging_interval})" + ) + self.logger.debug(log_message) + + if message.type == WSMsgType.TEXT: + message_data = message.data + await self.enqueue_message(message_data) + for listener in self.on_message_listeners: + await listener(message) + elif message.type == WSMsgType.CLOSE: + if self.auto_reconnect_enabled: + self.logger.info(f"Received CLOSE event from {session_id}. Reconnecting...") + await self.connect_to_new_endpoint() + for listener in self.on_close_listeners: + await listener(message) + elif message.type == WSMsgType.ERROR: + for listener in self.on_error_listeners: + await listener(message) + elif message.type == WSMsgType.CLOSED: + await asyncio.sleep(self.ping_interval) + continue + elif message.type == WSMsgType.PING: + await session.pong(message.data) # type: ignore[union-attr] + continue + elif message.type == WSMsgType.PONG: + if message.data is not None: + str_message_data = message.data.decode("utf-8") + elements = str_message_data.split(":") + if len(elements) == 2 and elements[0] == "sdk-ping-pong": + try: + self.last_ping_pong_time = float(elements[1]) + except Exception as e: + self.logger.warning( + f"Failed to parse the last_ping_pong_time value from {str_message_data}" + f" - error : {e}, session: {session_id}" + ) + continue + + consecutive_error_count = 0 + + except Exception as e: + consecutive_error_count += 1 + self.logger.error(f"Failed to receive or enqueue a message: {type(e).__name__}, {e} ({session_id})") + if isinstance(e, ClientConnectionError): + await asyncio.sleep(self.ping_interval) + else: + await asyncio.sleep(consecutive_error_count) + except asyncio.CancelledError: + if self.logger.level <= logging.DEBUG: + self.logger.debug(f"The running receive_messages task for {session_id} is now cancelled") + raise + + async def is_ping_pong_failing(self) -> bool: + if self.last_ping_pong_time is None: + return False + disconnected_seconds = int(time.time() - self.last_ping_pong_time) + return disconnected_seconds >= (self.ping_interval * 4) + + async def is_connected(self) -> bool: + connected: bool = ( + not self.closed + and not self.stale + and self.current_session is not None + and not self.current_session.closed + and not await self.is_ping_pong_failing() + ) + if self.logger.level <= logging.DEBUG and connected is False: + # Prints more detailed information about the inactive connection + is_ping_pong_failing = await self.is_ping_pong_failing() + session_id = await self.session_id() + self.logger.debug( + "Inactive connection detected (" + f"session_id: {session_id}, " + f"closed: {self.closed}, " + f"stale: {self.stale}, " + f"current_session.closed: {self.current_session and self.current_session.closed}, " + f"is_ping_pong_failing: {is_ping_pong_failing}" + ")" + ) + return connected + + async def session_id(self) -> str: + return self.build_session_id(self.current_session) # type: ignore[arg-type] + + async def connect(self): + # This loop is used to ensure when a new session is created, + # a new monitor and a new message receiver are also created. + # If a new session is created but we failed to create the new + # monitor or the new message, we should try it. + while True: + try: + old_session: Optional[ClientWebSocketResponse] = ( + None if self.current_session is None else self.current_session + ) + + # If the old session is broken (e.g. reset by peer), it might fail to close it. + # We don't want to retry when this kind of cases happen. + try: + # We should close old session before create a new one. Because when disconnect + # reason is `too_many_websockets`, we need to close the old one first to + # to decrease the number of connections. + self.auto_reconnect_enabled = False + if old_session is not None: + await old_session.close() + old_session_id = self.build_session_id(old_session) + self.logger.info(f"The old session ({old_session_id}) has been abandoned") + except Exception as e: + self.logger.exception(f"Failed to close the old session : {e}") + + if self.wss_uri is None: + # If the underlying WSS URL does not exist, + # acquiring a new active WSS URL from the server-side first + self.wss_uri = await self.issue_new_wss_url() + + self.current_session = await self.aiohttp_client_session.ws_connect( + self.wss_uri, + autoping=False, + heartbeat=self.ping_interval, + proxy=self.proxy, + ssl=self.web_client.ssl, + ) + session_id: str = await self.session_id() + self.auto_reconnect_enabled = self.default_auto_reconnect_enabled + self.stale = False + self.logger.info(f"A new session ({session_id}) has been established") + + # The first ping from the new connection + if self.logger.level <= logging.DEBUG: + self.logger.debug(f"Sending a ping message with the newly established connection ({session_id})...") + t = time.time() + await self.current_session.ping(f"sdk-ping-pong:{t}".encode("utf-8")) + + if self.current_session_monitor is not None: + self.current_session_monitor.cancel() + self.current_session_monitor = asyncio.ensure_future(self.monitor_current_session()) + if self.logger.level <= logging.DEBUG: + self.logger.debug(f"A new monitor_current_session() executor has been recreated for {session_id}") + + if self.message_receiver is not None: + self.message_receiver.cancel() + self.message_receiver = asyncio.ensure_future(self.receive_messages()) + if self.logger.level <= logging.DEBUG: + self.logger.debug(f"A new receive_messages() executor has been recreated for {session_id}") + break + except Exception as e: + self.logger.exception(f"Failed to connect (error: {e}); Retrying...") + await asyncio.sleep(self.ping_interval) + + async def disconnect(self): + if self.current_session is not None: + await self.current_session.close() + session_id = await self.session_id() + self.logger.info(f"The current session ({session_id}) has been abandoned by disconnect() method call") + + async def send_message(self, message: str): + session_id = await self.session_id() + if self.logger.level <= logging.DEBUG: + self.logger.debug(f"Sending a message: {message} from session: {session_id}") + try: + await self.current_session.send_str(message) # type: ignore[union-attr] + except ConnectionError as e: + # We rarely get this exception while replacing the underlying WebSocket connections. + # We can do one more try here as the self.current_session should be ready now. + if self.logger.level <= logging.DEBUG: + self.logger.debug( + f"Failed to send a message (error: {e}, message: {message}, session: {session_id})" + " as the underlying connection was replaced. Retrying the same request only one time..." + ) + # Although acquiring self.connect_operation_lock also for the first method call is the safest way, + # we avoid synchronizing a lot for better performance. That's why we are doing a retry here. + try: + await self.connect_operation_lock.acquire() + if await self.is_connected(): + await self.current_session.send_str(message) # type: ignore[union-attr] + else: + self.logger.warning( + f"The current session ({session_id}) is no longer active. " "Failed to send a message" + ) + raise e + finally: + if self.connect_operation_lock.locked() is True: + self.connect_operation_lock.release() + + async def close(self): + self.closed = True + self.auto_reconnect_enabled = False + await self.disconnect() + if self.message_processor is not None: + self.message_processor.cancel() + if self.current_session_monitor is not None: + self.current_session_monitor.cancel() + if self.message_receiver is not None: + self.message_receiver.cancel() + if self.aiohttp_client_session is not None: + await self.aiohttp_client_session.close() + + @classmethod + def build_session_id(cls, session: ClientWebSocketResponse) -> str: + if session is None: + return "" + return "s_" + str(hash(session)) diff --git a/slack_sdk/socket_mode/async_client.py b/slack_sdk/socket_mode/async_client.py new file mode 100644 index 000000000..5225dc285 --- /dev/null +++ b/slack_sdk/socket_mode/async_client.py @@ -0,0 +1,168 @@ +import asyncio +import json +import logging +from asyncio import Queue, Lock +from asyncio.futures import Future +from logging import Logger +from typing import Dict, Union, Any, Optional, List, Callable, Awaitable + +from slack_sdk.errors import SlackApiError +from slack_sdk.socket_mode.async_listeners import ( + AsyncWebSocketMessageListener, + AsyncSocketModeRequestListener, +) +from slack_sdk.socket_mode.request import SocketModeRequest +from slack_sdk.socket_mode.response import SocketModeResponse +from slack_sdk.web.async_client import AsyncWebClient + + +class AsyncBaseSocketModeClient: + logger: Logger + web_client: AsyncWebClient + app_token: str + wss_uri: str + auto_reconnect_enabled: bool + trace_enabled: bool + closed: bool + connect_operation_lock: Lock + + message_queue: Queue + message_listeners: List[ + Union[ + AsyncWebSocketMessageListener, + Callable[["AsyncBaseSocketModeClient", dict, Optional[str]], Awaitable[None]], + ] + ] + socket_mode_request_listeners: List[ + Union[ + AsyncSocketModeRequestListener, + Callable[["AsyncBaseSocketModeClient", SocketModeRequest], Awaitable[None]], + ] + ] + + async def issue_new_wss_url(self) -> str: + try: + response = await self.web_client.apps_connections_open(app_token=self.app_token) + return response["url"] + except SlackApiError as e: + if e.response["error"] == "ratelimited": + # NOTE: ratelimited errors rarely occur with this endpoint + delay = int(e.response.headers.get("Retry-After", "30")) # Tier1 + self.logger.info(f"Rate limited. Retrying in {delay} seconds...") + await asyncio.sleep(delay) + # Retry to issue a new WSS URL + return await self.issue_new_wss_url() + else: + # other errors + self.logger.error(f"Failed to retrieve WSS URL: {e}") + raise e + + async def is_connected(self) -> bool: + return False + + async def session_id(self) -> str: + return "" + + async def connect(self): + raise NotImplementedError() + + async def disconnect(self): + raise NotImplementedError() + + async def connect_to_new_endpoint(self, force: bool = False): + session_id = await self.session_id() + try: + await self.connect_operation_lock.acquire() + if self.trace_enabled: + self.logger.debug(f"For reconnection, the connect_operation_lock was acquired (session: {session_id})") + if force or not await self.is_connected(): + self.wss_uri = await self.issue_new_wss_url() + await self.connect() + finally: + if self.connect_operation_lock.locked() is True: + self.connect_operation_lock.release() + if self.trace_enabled: + self.logger.debug(f"The connect_operation_lock for reconnection was released (session: {session_id})") + + async def close(self): + self.closed = True + await self.disconnect() + + async def send_message(self, message: str): + raise NotImplementedError() + + async def send_socket_mode_response(self, response: Union[Dict[str, Any], SocketModeResponse]): + if isinstance(response, SocketModeResponse): + await self.send_message(json.dumps(response.to_dict())) + else: + await self.send_message(json.dumps(response)) + + async def enqueue_message(self, message: str): + await self.message_queue.put(message) + if self.logger.level <= logging.DEBUG: + queue_size = self.message_queue.qsize() + session_id = await self.session_id() + self.logger.debug(f"A new message enqueued (current queue size: {queue_size}, session: {session_id})") + + async def process_messages(self): + session_id = await self.session_id() + try: + while not self.closed: + try: + await self.process_message() + except asyncio.CancelledError: + # if self.closed is True, the connection is already closed + # In this case, we can ignore the exception here + if not self.closed: + raise + except Exception as e: + self.logger.exception(f"Failed to process a message: {e}, session: {session_id}") + except asyncio.CancelledError: + if self.trace_enabled: + self.logger.debug(f"The running process_messages task for {session_id} is now cancelled") + raise + + async def process_message(self): + raw_message = await self.message_queue.get() + if raw_message is not None: + message: dict = {} + if raw_message.startswith("{"): + message = json.loads(raw_message) + _: Future[None] = asyncio.ensure_future(self.run_message_listeners(message, raw_message)) + + async def run_message_listeners(self, message: dict, raw_message: str) -> None: + session_id = await self.session_id() + type, envelope_id = message.get("type"), message.get("envelope_id") + if self.logger.level <= logging.DEBUG: + self.logger.debug( + f"Message processing started (type: {type}, envelope_id: {envelope_id}, session: {session_id})" + ) + try: + if message.get("type") == "disconnect": + await self.connect_to_new_endpoint(force=True) + return + + for listener in self.message_listeners: + try: + await listener(self, message, raw_message) # type: ignore[call-arg, arg-type, misc] + except Exception as e: + self.logger.exception(f"Failed to run a message listener: {e}, session: {session_id}") + + if len(self.socket_mode_request_listeners) > 0: + request = SocketModeRequest.from_dict(message) + if request is not None: + for listener in self.socket_mode_request_listeners: # type: ignore[assignment] + try: + await listener(self, request) # type: ignore[call-arg, arg-type] + except Exception as e: + self.logger.exception(f"Failed to run a request listener: {e}, session: {session_id}") + except Exception as e: + self.logger.exception(f"Failed to run message listeners: {e}, session: {session_id}") + finally: + if self.logger.level <= logging.DEBUG: + self.logger.debug( + f"Message processing completed (" + f"type: {type}, " + f"envelope_id: {envelope_id}, " + f"session: {session_id})" + ) diff --git a/slack_sdk/socket_mode/async_listeners.py b/slack_sdk/socket_mode/async_listeners.py new file mode 100644 index 000000000..9b3ac86c3 --- /dev/null +++ b/slack_sdk/socket_mode/async_listeners.py @@ -0,0 +1,20 @@ +from typing import Optional, Callable + +from slack_sdk.socket_mode.request import SocketModeRequest + + +class AsyncWebSocketMessageListener(Callable): # type: ignore[misc] + async def __call__( + client: "AsyncBaseSocketModeClient", # type: ignore[name-defined] # noqa: F821 + message: dict, + raw_message: Optional[str] = None, + ): # noqa: F821 + raise NotImplementedError() + + +class AsyncSocketModeRequestListener(Callable): # type: ignore[misc] + async def __call__( + client: "AsyncBaseSocketModeClient", # type: ignore[name-defined] # noqa: F821 + request: SocketModeRequest, + ): # noqa: F821 + raise NotImplementedError() diff --git a/slack_sdk/socket_mode/builtin/__init__.py b/slack_sdk/socket_mode/builtin/__init__.py new file mode 100644 index 000000000..c49d4c91f --- /dev/null +++ b/slack_sdk/socket_mode/builtin/__init__.py @@ -0,0 +1,5 @@ +from .client import SocketModeClient + +__all__ = [ + "SocketModeClient", +] diff --git a/slack_sdk/socket_mode/builtin/client.py b/slack_sdk/socket_mode/builtin/client.py new file mode 100644 index 000000000..be80e0526 --- /dev/null +++ b/slack_sdk/socket_mode/builtin/client.py @@ -0,0 +1,291 @@ +"""The built-in Socket Mode client + +* https://docs.slack.dev/apis/events-api/using-socket-mode/ +* https://docs.slack.dev/tools/python-slack-sdk/socket-mode/ + +""" + +import logging +from concurrent.futures.thread import ThreadPoolExecutor +from logging import Logger +from queue import Queue +from threading import Lock +from typing import Union, Optional, List, Callable, Dict + +from slack_sdk.socket_mode.client import BaseSocketModeClient +from slack_sdk.socket_mode.listeners import ( + WebSocketMessageListener, + SocketModeRequestListener, +) +from slack_sdk.socket_mode.request import SocketModeRequest +from slack_sdk.web import WebClient +from .connection import Connection, ConnectionState +from ..interval_runner import IntervalRunner +from ..logger.messages import debug_redacted_message_string +from ...errors import SlackClientConfigurationError, SlackClientNotConnectedError +from ...proxy_env_variable_loader import load_http_proxy_from_env + + +class SocketModeClient(BaseSocketModeClient): + logger: Logger + web_client: WebClient + app_token: str + wss_uri: Optional[str] # type: ignore[assignment] + message_queue: Queue + message_listeners: List[ + Union[ + WebSocketMessageListener, + Callable[["BaseSocketModeClient", dict, Optional[str]], None], + ] + ] + socket_mode_request_listeners: List[ + Union[ + SocketModeRequestListener, + Callable[["BaseSocketModeClient", SocketModeRequest], None], + ] + ] + + current_session: Optional[Connection] + current_session_state: ConnectionState + current_session_runner: IntervalRunner + + current_app_monitor: IntervalRunner + current_app_monitor_started: bool + + message_processor: IntervalRunner + message_workers: ThreadPoolExecutor + + auto_reconnect_enabled: bool + default_auto_reconnect_enabled: bool + trace_enabled: bool + receive_buffer_size: int # bytes size + + connect_operation_lock: Lock + + on_message_listeners: List[Callable[[str], None]] + on_error_listeners: List[Callable[[Exception], None]] + on_close_listeners: List[Callable[[int, Optional[str]], None]] + + def __init__( + self, + app_token: str, + logger: Optional[Logger] = None, + web_client: Optional[WebClient] = None, + auto_reconnect_enabled: bool = True, + trace_enabled: bool = False, + all_message_trace_enabled: bool = False, + ping_pong_trace_enabled: bool = False, + ping_interval: float = 5, + receive_buffer_size: int = 1024, + concurrency: int = 10, + proxy: Optional[str] = None, + proxy_headers: Optional[Dict[str, str]] = None, + on_message_listeners: Optional[List[Callable[[str], None]]] = None, + on_error_listeners: Optional[List[Callable[[Exception], None]]] = None, + on_close_listeners: Optional[List[Callable[[int, Optional[str]], None]]] = None, + ): + """Socket Mode client + + Args: + app_token: App-level token + logger: Custom logger + web_client: Web API client + auto_reconnect_enabled: True if automatic reconnection is enabled (default: True) + trace_enabled: True if more detailed debug-logging is enabled (default: False) + all_message_trace_enabled: True if all message dump in debug logs is enabled (default: False) + ping_pong_trace_enabled: True if trace logging for all ping-pong communications is enabled (default: False) + ping_interval: interval for ping-pong with Slack servers (seconds) + receive_buffer_size: the chunk size of a single socket recv operation (default: 1024) + concurrency: the size of thread pool (default: 10) + proxy: the HTTP proxy URL + proxy_headers: additional HTTP header for proxy connection + on_message_listeners: listener functions for on_message + on_error_listeners: listener functions for on_error + on_close_listeners: listener functions for on_close + """ + self.app_token = app_token + self.logger = logger or logging.getLogger(__name__) + self.web_client = web_client or WebClient() + self.default_auto_reconnect_enabled = auto_reconnect_enabled + self.auto_reconnect_enabled = self.default_auto_reconnect_enabled + self.trace_enabled = trace_enabled + self.all_message_trace_enabled = all_message_trace_enabled + self.ping_pong_trace_enabled = ping_pong_trace_enabled + self.ping_interval = ping_interval + self.receive_buffer_size = receive_buffer_size + if self.receive_buffer_size < 16: + raise SlackClientConfigurationError("Too small receive_buffer_size detected.") + + self.wss_uri = None + self.message_queue = Queue() + self.message_listeners = [] + self.socket_mode_request_listeners = [] + + self.current_session = None + self.current_session_state = ConnectionState() + self.current_session_runner = IntervalRunner(self._run_current_session, 0.1).start() + + self.current_app_monitor_started = False + self.current_app_monitor = IntervalRunner(self._monitor_current_session, self.ping_interval) + + self.closed = False + self.connect_operation_lock = Lock() + + self.message_processor = IntervalRunner(self.process_messages, 0.001).start() + self.message_workers = ThreadPoolExecutor(max_workers=concurrency) + + self.proxy = proxy + if self.proxy is None or len(self.proxy.strip()) == 0: + env_variable = load_http_proxy_from_env(self.logger) + if env_variable is not None: + self.proxy = env_variable + self.proxy_headers = proxy_headers + + self.on_message_listeners = on_message_listeners or [] + self.on_error_listeners = on_error_listeners or [] + self.on_close_listeners = on_close_listeners or [] + + def session_id(self) -> Optional[str]: + if self.current_session is not None: + return self.current_session.session_id + return None + + def is_connected(self) -> bool: + return self.current_session is not None and self.current_session.is_active() + + def connect(self) -> None: + old_session: Optional[Connection] = self.current_session + old_current_session_state: ConnectionState = self.current_session_state + + if self.wss_uri is None: + self.wss_uri = self.issue_new_wss_url() + + current_session = Connection( + url=self.wss_uri, + logger=self.logger, + ping_interval=self.ping_interval, + trace_enabled=self.trace_enabled, + all_message_trace_enabled=self.all_message_trace_enabled, + ping_pong_trace_enabled=self.ping_pong_trace_enabled, + receive_buffer_size=self.receive_buffer_size, + proxy=self.proxy, + proxy_headers=self.proxy_headers, + on_message_listener=self._on_message, + on_error_listener=self._on_error, + on_close_listener=self._on_close, + ssl_context=self.web_client.ssl, + ) + current_session.connect() + + if old_current_session_state is not None: + old_current_session_state.terminated = True + if old_session is not None: + old_session.close() + + self.current_session = current_session + self.current_session_state = ConnectionState() + self.auto_reconnect_enabled = self.default_auto_reconnect_enabled + + if not self.current_app_monitor_started: + self.current_app_monitor_started = True + self.current_app_monitor.start() + + self.logger.info(f"A new session has been established (session id: {self.session_id()})") + + def disconnect(self) -> None: + if self.current_session is not None: + self.current_session.close() + + def send_message(self, message: str) -> None: + if self.logger.level <= logging.DEBUG: + self.logger.debug(f"Sending a message (session id: {self.session_id()}, message: {message})") + try: + self.current_session.send(message) # type: ignore[union-attr] + except SlackClientNotConnectedError as e: + # We rarely get this exception while replacing the underlying WebSocket connections. + # We can do one more try here as the self.current_session should be ready now. + if self.logger.level <= logging.DEBUG: + self.logger.debug( + f"Failed to send a message (session id: {self.session_id()}, error: {e}, message: {message})" + " as the underlying connection was replaced. Retrying the same request only one time..." + ) + # Although acquiring self.connect_operation_lock also for the first method call is the safest way, + # we avoid synchronizing a lot for better performance. That's why we are doing a retry here. + with self.connect_operation_lock: + if self.is_connected(): + self.current_session.send(message) # type: ignore[union-attr] + else: + self.logger.warning( + f"The current session (session id: {self.session_id()}) is no longer active. " + "Failed to send a message" + ) + raise e + + def close(self): + self.closed = True + self.auto_reconnect_enabled = False + self.disconnect() + if self.current_app_monitor.is_alive(): + self.current_app_monitor.shutdown() + if self.message_processor.is_alive(): + self.message_processor.shutdown() + self.message_workers.shutdown() + + def _on_message(self, message: str): + if self.logger.level <= logging.DEBUG: + self.logger.debug(f"on_message invoked: (message: {debug_redacted_message_string(message)})") + self.enqueue_message(message) + for listener in self.on_message_listeners: + listener(message) + + def _on_error(self, error: Exception): + error_message = ( + f"on_error invoked (session id: {self.session_id()}, " f"error: {type(error).__name__}, message: {error})" + ) + if self.trace_enabled: + self.logger.exception(error_message) + else: + self.logger.error(error_message) + + for listener in self.on_error_listeners: + listener(error) + + def _on_close(self, code: int, reason: Optional[str] = None): + if self.logger.level <= logging.DEBUG: + self.logger.debug(f"on_close invoked (session id: {self.session_id()})") + if self.auto_reconnect_enabled: + self.logger.info("Received CLOSE event. Reconnecting... " f"(session id: {self.session_id()})") + self.connect_to_new_endpoint() + for listener in self.on_close_listeners: + listener(code, reason) + + def _run_current_session(self): + if self.current_session is not None and self.current_session.is_active(): + session_id = self.session_id() + try: + self.logger.info("Starting to receive messages from a new connection" f" (session id: {session_id})") + self.current_session_state.terminated = False + self.current_session.run_until_completion(self.current_session_state) + self.logger.info("Stopped receiving messages from a connection" f" (session id: {session_id})") + except Exception as e: + error_message = "Failed to start or stop the current session" f" (session id: {session_id}, error: {e})" + if self.trace_enabled: + self.logger.exception(error_message) + else: + self.logger.error(error_message) + + def _monitor_current_session(self): + if self.current_app_monitor_started: + try: + self.current_session.check_state() + + if self.auto_reconnect_enabled and (self.current_session is None or not self.current_session.is_active()): + self.logger.info( + "The session seems to be already closed. Reconnecting... " f"(session id: {self.session_id()})" + ) + self.connect_to_new_endpoint() + except Exception as e: + self.logger.error( + "Failed to check the current session or reconnect to the server " + f"(session id: {self.session_id()}, error: {type(e).__name__}, message: {e})" + ) diff --git a/slack_sdk/socket_mode/builtin/connection.py b/slack_sdk/socket_mode/builtin/connection.py new file mode 100644 index 000000000..c8b9c437e --- /dev/null +++ b/slack_sdk/socket_mode/builtin/connection.py @@ -0,0 +1,452 @@ +import socket +import ssl +import struct +import time +from logging import Logger +from threading import Lock +from typing import Optional, Callable, Union, List, Tuple, Dict +from urllib.parse import urlparse +from uuid import uuid4 + +from slack_sdk.errors import SlackClientNotConnectedError, SlackClientConfigurationError +from .frame_header import FrameHeader +from .internals import ( + _parse_handshake_response, + _validate_sec_websocket_accept, + _generate_sec_websocket_key, + _to_readable_opcode, + _receive_messages, + _build_data_frame_for_sending, + _parse_text_payload, + _establish_new_socket_connection, +) + + +class ConnectionState: + # The flag supposed to be used for telling SocketModeClient + # when this connection is no longer available + terminated: bool + + def __init__(self): + self.terminated = False + + +class Connection: + url: str + logger: Logger + proxy: Optional[str] + proxy_headers: Optional[Dict[str, str]] + + trace_enabled: bool + ping_pong_trace_enabled: bool + last_ping_pong_time: Optional[float] + + session_id: str + sock: Optional[ssl.SSLSocket] + + on_message_listener: Optional[Callable[[str], None]] + on_error_listener: Optional[Callable[[Exception], None]] + on_close_listener: Optional[Callable[[int, Optional[str]], None]] + + def __init__( + self, + url: str, + logger: Logger, + proxy: Optional[str] = None, + proxy_headers: Optional[Dict[str, str]] = None, + ping_interval: float = 5, # seconds + receive_timeout: float = 3, + receive_buffer_size: int = 1024, + trace_enabled: bool = False, + all_message_trace_enabled: bool = False, + ping_pong_trace_enabled: bool = False, + on_message_listener: Optional[Callable[[str], None]] = None, + on_error_listener: Optional[Callable[[Exception], None]] = None, + on_close_listener: Optional[Callable[[int, Optional[str]], None]] = None, + connection_type_name: str = "Socket Mode", + ssl_context: Optional[ssl.SSLContext] = None, + ): + self.url = url + self.logger = logger + self.proxy = proxy + self.proxy_headers = proxy_headers + + self.ping_interval = ping_interval + self.receive_timeout = receive_timeout + self.receive_buffer_size = receive_buffer_size + if self.receive_buffer_size < 16: + raise SlackClientConfigurationError("Too small receive_buffer_size detected.") + + self.session_id = str(uuid4()) + self.trace_enabled = trace_enabled + self.all_message_trace_enabled = all_message_trace_enabled + self.ping_pong_trace_enabled = ping_pong_trace_enabled + self.last_ping_pong_time = None + self.consecutive_check_state_error_count = 0 + self.sock = None + # To avoid ssl.SSLError: [SSL: BAD_LENGTH] bad length + self.sock_receive_lock = Lock() + self.sock_send_lock = Lock() + + self.on_message_listener = on_message_listener + self.on_error_listener = on_error_listener + self.on_close_listener = on_close_listener + self.connection_type_name = connection_type_name + + self.ssl_context = ssl_context + + def connect(self) -> None: + try: + parsed_url = urlparse(self.url.strip()) + hostname: str = parsed_url.hostname # type: ignore[assignment] + port: int = parsed_url.port or (443 if parsed_url.scheme == "wss" else 80) + if self.trace_enabled: + self.logger.debug( + f"Connecting to the address for handshake: {hostname}:{port} " f"(session id: {self.session_id})" + ) + sock: Union[ssl.SSLSocket, socket] = _establish_new_socket_connection( # type: ignore[valid-type] + session_id=self.session_id, + server_hostname=hostname, + server_port=port, + logger=self.logger, + sock_send_lock=self.sock_send_lock, + receive_timeout=self.receive_timeout, + proxy=self.proxy, + proxy_headers=self.proxy_headers, + trace_enabled=self.trace_enabled, + ssl_context=self.ssl_context, + ) + + # WebSocket handshake + try: + path = f"{parsed_url.path}?{parsed_url.query}" + sec_websocket_key = _generate_sec_websocket_key() + message = f"""GET {path} HTTP/1.1 + Host: {parsed_url.hostname} + Upgrade: websocket + Connection: Upgrade + Sec-WebSocket-Key: {sec_websocket_key} + Sec-WebSocket-Version: 13 + + """ + req: str = "\r\n".join([line.lstrip() for line in message.split("\n")]) + if self.trace_enabled: + self.logger.debug( + f"{self.connection_type_name} handshake request (session id: {self.session_id}):\n{req}" + ) + with self.sock_send_lock: + sock.send(req.encode("utf-8")) # type: ignore[union-attr] + + status, headers, text = _parse_handshake_response(sock) + if self.trace_enabled: + self.logger.debug( + f"{self.connection_type_name} handshake response (session id: {self.session_id}):\n{text}" + ) + # HTTP/1.1 101 Switching Protocols + if status == 101: + if not _validate_sec_websocket_accept(sec_websocket_key, headers): + raise SlackClientNotConnectedError( + f"Invalid response header detected in {self.connection_type_name} handshake response" + f" (session id: {self.session_id})" + ) + # set this successfully connected socket + self.sock = sock + self.ping(f"{self.session_id}:{time.time()}") + else: + message = ( + f"Received an unexpected response for handshake " + f"(status: {status}, response: {text}, session id: {self.session_id})" + ) + self.logger.warning(message) + + except socket.error as e: + code: Optional[int] = None + if e.args and len(e.args) > 1 and isinstance(e.args[0], int): + code = e.args[0] + if code is not None: + error_message = f"Error code: {code} (session id: {self.session_id}, error: {e})" + if self.trace_enabled: + self.logger.exception(error_message) + else: + self.logger.error(error_message) + raise + + except Exception as e: + error_message = f"Failed to establish a connection (session id: {self.session_id}, error: {e})" + if self.trace_enabled: + self.logger.exception(error_message) + else: + self.logger.error(error_message) + + if self.on_error_listener is not None: + self.on_error_listener(e) + + self.disconnect() + + def disconnect(self) -> None: + if self.sock is not None: + with self.sock_send_lock: + with self.sock_receive_lock: + # Synchronize before closing this instance's socket + self.sock.close() + self.sock = None + # After this, all operations using self.sock will be skipped + + self.logger.info(f"The connection has been closed (session id: {self.session_id})") + + def is_active(self) -> bool: + return self.sock is not None + + def close(self) -> None: + self.disconnect() + + def ping(self, payload: Union[str, bytes] = "") -> None: + if self.trace_enabled and self.ping_pong_trace_enabled: + if isinstance(payload, bytes): + payload = payload.decode("utf-8") + self.logger.debug("Sending a ping data frame " f"(session id: {self.session_id}, payload: {payload})") + data = _build_data_frame_for_sending(payload, FrameHeader.OPCODE_PING) + with self.sock_send_lock: + if self.sock is not None: + self.sock.send(data) + else: + if self.ping_pong_trace_enabled: + self.logger.debug("Skipped sending a ping message as the underlying socket is no longer available.") + + def pong(self, payload: Union[str, bytes] = "") -> None: + if self.trace_enabled and self.ping_pong_trace_enabled: + if isinstance(payload, bytes): + payload = payload.decode("utf-8") + self.logger.debug("Sending a pong data frame " f"(session id: {self.session_id}, payload: {payload})") + data = _build_data_frame_for_sending(payload, FrameHeader.OPCODE_PONG) + with self.sock_send_lock: + if self.sock is not None: + self.sock.send(data) + else: + if self.ping_pong_trace_enabled: + self.logger.debug("Skipped sending a pong message as the underlying socket is no longer available.") + + def send(self, payload: str) -> None: + if self.trace_enabled: + if isinstance(payload, bytes): + payload = payload.decode("utf-8") + self.logger.debug("Sending a text data frame " f"(session id: {self.session_id}, payload: {payload})") + data = _build_data_frame_for_sending(payload, FrameHeader.OPCODE_TEXT) + with self.sock_send_lock: + try: + self.sock.send(data) # type: ignore[union-attr] + except Exception as e: + # In most cases, we want to retry this operation with a newly established connection. + # Getting this exception means that this connection has been replaced with a new one + # and it's no longer usable. + # The SocketModeClient implementation can do one retry when it gets this exception. + raise SlackClientNotConnectedError( + f"Failed to send a message as the connection is no longer active " + f"(session_id: {self.session_id}, error: {e})" + ) + + def check_state(self) -> None: + try: + if self.sock is not None: + try: + self.ping(f"{self.session_id}:{time.time()}") + except ssl.SSLZeroReturnError as e: + self.logger.info( + "Unable to send a ping message. Closing the connection..." + f" (session id: {self.session_id}, reason: {e})" + ) + self.disconnect() + return + + if self.last_ping_pong_time is not None: + disconnected_seconds = int(time.time() - self.last_ping_pong_time) + if self.trace_enabled and disconnected_seconds > self.ping_interval: + message = ( + f"{disconnected_seconds} seconds have passed " + f"since this client last received a pong response from the server " + f"(session id: {self.session_id})" + ) + self.logger.debug(message) + + is_stale = disconnected_seconds > self.ping_interval * 4 + if is_stale: + self.logger.info( + "The connection seems to be stale. Disconnecting..." + f" (session id: {self.session_id}," + f" reason: disconnected for {disconnected_seconds}+ seconds)" + ) + self.disconnect() + return + else: + self.logger.debug("This connection is already closed." f" (session id: {self.session_id})") + self.consecutive_check_state_error_count = 0 + except Exception as e: + error_message = ( + "Failed to check the state of sock " + f"(session id: {self.session_id}, error: {type(e).__name__}, message: {e})" + ) + if self.trace_enabled: + self.logger.exception(error_message) + else: + self.logger.error(error_message) + + self.consecutive_check_state_error_count += 1 + if self.consecutive_check_state_error_count >= 5: + self.disconnect() + + def run_until_completion(self, state: ConnectionState) -> None: + repeated_messages = {"payload": 0} + ping_count = 0 + pong_count = 0 + ping_pong_log_summary_size = 1000 + while not state.terminated: + try: + if self.is_active(): + received_messages: List[Tuple[Optional[FrameHeader], bytes]] = _receive_messages( + sock=self.sock, # type: ignore[arg-type] + sock_receive_lock=self.sock_receive_lock, + logger=self.logger, + receive_buffer_size=self.receive_buffer_size, + all_message_trace_enabled=self.all_message_trace_enabled, + ) + for message in received_messages: + header, data = message + + # ----------------- + # trace logging + + if self.trace_enabled is True: + opcode: str = _to_readable_opcode(header.opcode) if header else "-" + payload: str = _parse_text_payload(data, self.logger) + count: Optional[int] = repeated_messages.get(payload) + if count is None: + count = 1 + else: + count += 1 + repeated_messages = {payload: count} + if not self.ping_pong_trace_enabled and header is not None and header.opcode is not None: + if header.opcode == FrameHeader.OPCODE_PING: + ping_count += 1 + if ping_count % ping_pong_log_summary_size == 0: + self.logger.debug( + f"Received {ping_pong_log_summary_size} ping data frame " + f"(session id: {self.session_id})" + ) + ping_count = 0 + if header.opcode == FrameHeader.OPCODE_PONG: + pong_count += 1 + if pong_count % ping_pong_log_summary_size == 0: + self.logger.debug( + f"Received {ping_pong_log_summary_size} pong data frame " + f"(session id: {self.session_id})" + ) + pong_count = 0 + + ping_pong_to_skip = ( + header is not None + and header.opcode is not None + and (header.opcode == FrameHeader.OPCODE_PING or header.opcode == FrameHeader.OPCODE_PONG) + and not self.ping_pong_trace_enabled + ) + if not ping_pong_to_skip and count < 5: + # if so many same payloads came in, the trace logging should be skipped. + # e.g., after receiving "UNAUTHENTICATED: cache_error", many "opcode: -, payload: " + self.logger.debug( + "Received a new data frame " + f"(session id: {self.session_id}, opcode: {opcode}, payload: {payload})" + ) + + if header is None: + # Skip no header message + continue + + # ----------------- + # message with opcode + + if header.opcode == FrameHeader.OPCODE_PING: + self.pong(data) + elif header.opcode == FrameHeader.OPCODE_PONG: + str_message = data.decode("utf-8") + elements = str_message.split(":") + if len(elements) >= 2: + session_id, ping_time = elements[0], elements[1] + if self.session_id == session_id: + try: + self.last_ping_pong_time = float(ping_time) + except Exception as e: + self.logger.debug( + "Failed to parse a pong message " f" (message: {str_message}, error: {e}" + ) + elif header.opcode == FrameHeader.OPCODE_TEXT: + if self.on_message_listener is not None: + text = data.decode("utf-8") + self.on_message_listener(text) + elif header.opcode == FrameHeader.OPCODE_CLOSE: + if self.on_close_listener is not None: + if len(data) >= 2: + (code,) = struct.unpack("!H", data[:2]) + reason = data[2:].decode("utf-8") + self.on_close_listener(code, reason) + else: + self.on_close_listener(1005, "") + self.disconnect() + state.terminated = True + else: + # Just warn logging + opcode = _to_readable_opcode(header.opcode) if header else "-" + payload: Union[bytes, str] = data # type: ignore[no-redef] + if header.opcode != FrameHeader.OPCODE_BINARY: + try: + payload = data.decode("utf-8") if data is not None else "" + except Exception as e: + self.logger.info(f"Failed to convert the data to text {e}") + message = ( + "Received an unsupported data frame " # type: ignore[assignment] + f"(session id: {self.session_id}, opcode: {opcode}, payload: {payload})" + ) + self.logger.warning(message) + else: + time.sleep(0.2) + except socket.timeout: + time.sleep(0.01) + except OSError as e: + # getting errno.EBADF and the socket is no longer available + if e.errno == 9 and state.terminated: + self.logger.debug( + "The reason why you got [Errno 9] Bad file descriptor here is " "the socket is no longer available." + ) + else: + if self.on_error_listener is not None: + self.on_error_listener(e) + else: + error_message = "Got an OSError while receiving data" f" (session id: {self.session_id}, error: {e})" + if self.trace_enabled: + self.logger.exception(error_message) + else: + self.logger.error(error_message) + + # As this connection no longer works in any way, terminating it + if self.is_active(): + try: + self.disconnect() + except Exception as disconnection_error: + error_message = ( + "Failed to disconnect" f" (session id: {self.session_id}, error: {disconnection_error})" + ) + if self.trace_enabled: + self.logger.exception(error_message) + else: + self.logger.error(error_message) + state.terminated = True + break + except Exception as e: + if self.on_error_listener is not None: + self.on_error_listener(e) + else: + error_message = "Got an exception while receiving data" f" (session id: {self.session_id}, error: {e})" + if self.trace_enabled: + self.logger.exception(error_message) + else: + self.logger.error(error_message) + + state.terminated = True diff --git a/slack_sdk/socket_mode/builtin/frame_header.py b/slack_sdk/socket_mode/builtin/frame_header.py new file mode 100644 index 000000000..8851b3ee0 --- /dev/null +++ b/slack_sdk/socket_mode/builtin/frame_header.py @@ -0,0 +1,47 @@ +class FrameHeader: + fin: int + rsv1: int + rsv2: int + rsv3: int + opcode: int + masked: int + length: int + + # Opcode + # https://tools.ietf.org/html/rfc6455#section-5.2 + # Non-control frames + # %x0 denotes a continuation frame + OPCODE_CONTINUATION = 0x0 + # %x1 denotes a text frame + OPCODE_TEXT = 0x1 + # %x2 denotes a binary frame + OPCODE_BINARY = 0x2 + # %x3-7 are reserved for further non-control frames + + # Control frames + # %x8 denotes a connection close + OPCODE_CLOSE = 0x8 + # %x9 denotes a ping + OPCODE_PING = 0x9 + # %xA denotes a pong + OPCODE_PONG = 0xA + + # %xB-F are reserved for further control frames + + def __init__( + self, + opcode: int, + fin: int = 1, + rsv1: int = 0, + rsv2: int = 0, + rsv3: int = 0, + masked: int = 0, + length: int = 0, + ): + self.opcode = opcode + self.fin = fin + self.rsv1 = rsv1 + self.rsv2 = rsv2 + self.rsv3 = rsv3 + self.masked = masked + self.length = length diff --git a/slack_sdk/socket_mode/builtin/internals.py b/slack_sdk/socket_mode/builtin/internals.py new file mode 100644 index 000000000..7b8365615 --- /dev/null +++ b/slack_sdk/socket_mode/builtin/internals.py @@ -0,0 +1,408 @@ +import errno +import hashlib +import itertools +import os +import random +import socket +from socket import socket as Socket +import ssl +import struct +from base64 import encodebytes, b64encode +from hmac import compare_digest +from logging import Logger +from threading import Lock +from typing import Tuple, Optional, Union, List, Callable, Dict +from urllib.parse import urlparse, unquote + +from .frame_header import FrameHeader + + +def _parse_connect_response(sock: Socket) -> Tuple[Optional[int], str]: + status = None + lines = [] + while True: + line = [] + while True: + c = sock.recv(1) + if not c: + raise ConnectionError("Connection is closed") + line.append(c) + if c == b"\n": + break + line = b"".join(line).decode("utf-8").strip() # type: ignore[assignment] + if line is None or len(line) == 0: + break + lines.append(line) + if not status: + status_line = line.split(" ", 2) # type: ignore[attr-defined] + status = int(status_line[1]) + return status, "\n".join(lines) # type: ignore[arg-type] + + +def _use_or_create_ssl_context(ssl_context: Optional[ssl.SSLContext] = None): + return ssl_context if ssl_context is not None else ssl.create_default_context() + + +def _establish_new_socket_connection( + session_id: str, + server_hostname: str, + server_port: int, + logger: Logger, + sock_send_lock: Lock, + receive_timeout: float, + proxy: Optional[str], + proxy_headers: Optional[Dict[str, str]], + trace_enabled: bool, + ssl_context: Optional[ssl.SSLContext] = None, +) -> Union[ssl.SSLSocket, Socket]: + ssl_context = _use_or_create_ssl_context(ssl_context) + + if proxy is not None: + parsed_proxy = urlparse(proxy) + proxy_host, proxy_port = parsed_proxy.hostname, parsed_proxy.port or 80 + sock = socket.create_connection((proxy_host, proxy_port), receive_timeout) + if hasattr(socket, "TCP_NODELAY"): + sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + if hasattr(socket, "SO_KEEPALIVE"): + sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) + message = [f"CONNECT {server_hostname}:{server_port} HTTP/1.0"] + if parsed_proxy.username is not None and parsed_proxy.password is not None: + # In the case where the proxy is "http://{username}:{password}@{hostname}:{port}" + raw_value = f"{unquote(parsed_proxy.username)}:{unquote(parsed_proxy.password)}" + auth = b64encode(raw_value.encode("utf-8")).decode("ascii") + message.append(f"Proxy-Authorization: Basic {auth}") + if proxy_headers is not None: + for k, v in proxy_headers.items(): + message.append(f"{k}: {v}") + message.append("") + message.append("") + req: str = "\r\n".join([line.lstrip() for line in message]) + if trace_enabled: + logger.debug(f"Proxy connect request (session id: {session_id}):\n{req}") + with sock_send_lock: + sock.send(req.encode("utf-8")) + status, text = _parse_connect_response(sock) + if trace_enabled: + log_message = f"Proxy connect response (session id: {session_id}):\n{text}" + logger.debug(log_message) + if status != 200: + raise Exception(f"Failed to connect to the proxy (proxy: {proxy}, connect status code: {status})") + + sock = ssl_context.wrap_socket( + sock, + do_handshake_on_connect=True, + suppress_ragged_eofs=True, + server_hostname=server_hostname, + ) + return sock + + if server_port != 443: + # only for library testing + logger.info(f"Using non-ssl socket to connect ({server_hostname}:{server_port})") + sock = socket.create_connection((server_hostname, server_port), timeout=3) + return sock + + sock = socket.create_connection((server_hostname, server_port), receive_timeout) + sock = ssl_context.wrap_socket( + sock, + do_handshake_on_connect=True, + suppress_ragged_eofs=True, + server_hostname=server_hostname, + ) + return sock + + +def _read_http_response_line(sock: ssl.SSLSocket) -> str: + cs = [] + while True: + b: bytes = sock.recv(1) + if not b: + raise ConnectionError("Connection is closed") + c: str = b.decode("utf-8") + if c == "\r": + break + if c != "\n": + cs.append(c) + return "".join(cs) + + +def _parse_handshake_response(sock: ssl.SSLSocket) -> Tuple[Optional[int], dict, str]: + """Parses the handshake response. + + Args: + sock: The current active socket + + Returns: + (http status, headers, whole response as a str) + """ + lines = [] + status = None + headers: Dict[str, str] = {} + while True: + line = _read_http_response_line(sock) + if status is None: + elements = line.split(" ") + if len(elements) > 2: + status = int(elements[1]) + else: + elements = line.split(":") + if len(elements) == 2: + headers[elements[0].strip().lower()] = elements[1].strip() + if line is None or len(line.strip()) == 0: + break + lines.append(line) + text = "\n".join(lines) + return (status, headers, text) + + +def _generate_sec_websocket_key() -> str: + return encodebytes(os.urandom(16)).decode("utf-8").strip() + + +def _validate_sec_websocket_accept(sec_websocket_key: str, headers: dict) -> bool: + v = (sec_websocket_key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11").encode("utf-8") + expected = encodebytes(hashlib.sha1(v).digest()).decode("utf-8").strip() + actual = headers.get("sec-websocket-accept", "").strip() + return compare_digest(expected, actual) + + +def _to_readable_opcode(opcode: int) -> str: + if opcode == FrameHeader.OPCODE_CONTINUATION: + return "continuation" + if opcode == FrameHeader.OPCODE_TEXT: + return "text" + if opcode == FrameHeader.OPCODE_BINARY: + return "binary" + if opcode == FrameHeader.OPCODE_CLOSE: + return "close" + if opcode == FrameHeader.OPCODE_PING: + return "ping" + if opcode == FrameHeader.OPCODE_PONG: + return "pong" + return "-" + + +def _parse_text_payload(data: Optional[bytes], logger: Logger) -> str: + try: + if data is not None and isinstance(data, bytes): + return data.decode("utf-8") + else: + return "" + except UnicodeDecodeError as e: + logger.debug(f"Failed to parse a payload (data: {data!r}, error: {e})") + return "" + + +def _receive_messages( + sock: ssl.SSLSocket, + sock_receive_lock: Lock, + logger: Logger, + receive_buffer_size: int = 1024, + all_message_trace_enabled: bool = False, +) -> List[Tuple[Optional[FrameHeader], bytes]]: + def receive(specific_buffer_size: Optional[int] = None): + size = specific_buffer_size if specific_buffer_size is not None else receive_buffer_size + with sock_receive_lock: + try: + received_bytes = sock.recv(size) + if all_message_trace_enabled: + if len(received_bytes) > 0: + logger.debug(f"Received bytes: {received_bytes!r}") + return received_bytes + except OSError as e: + # For Linux/macOS, errno.EBADF is the expected error for bad connections. + # The errno.ENOTSOCK can be sent when running on Windows OS. + if e.errno in (errno.EBADF, errno.ENOTSOCK): + # Note that bad connections can be detected by monitoring threads + # the Socket Mode client automatically reconnects to a new endpoint later. + logger.debug("The connection seems to be already closed.") + return bytes() + raise e + + return _fetch_messages( + messages=[], + receive=receive, + remaining_bytes=None, + current_mask_key=None, + current_header=None, + current_data=bytes(), + logger=logger, + ) + + +def _fetch_messages( + messages: List[Tuple[Optional[FrameHeader], bytes]], + receive: Callable[[Optional[int]], bytes], # buffer size + logger: Logger, + remaining_bytes: Optional[bytes] = None, + current_mask_key: Optional[str] = None, + current_header: Optional[FrameHeader] = None, + current_data: Optional[bytes] = None, +) -> List[Tuple[Optional[FrameHeader], bytes]]: + if remaining_bytes is None: + # Fetch more to complete the current message + remaining_bytes = receive() # type: ignore[call-arg] + + if remaining_bytes is None or len(remaining_bytes) == 0: + # no more bytes + if current_header is not None: + _append_message(messages, current_header, current_data) # type: ignore[arg-type] + return messages + + if current_header is None: + # new message + if len(remaining_bytes) <= 2: + remaining_bytes += receive() # type: ignore[call-arg] + + if remaining_bytes[0] == 10: # \n + if current_data is not None and len(current_data) >= 0: + _append_message(messages, current_header, current_data) + _append_message(messages, None, remaining_bytes[:1]) + remaining_bytes = remaining_bytes[1:] + if len(remaining_bytes) == 0: + return messages + else: + return _fetch_messages( + messages=messages, + receive=receive, + remaining_bytes=remaining_bytes, + logger=logger, + ) + + # https://tools.ietf.org/html/rfc6455#section-5.2 + b1, b2 = remaining_bytes[0], remaining_bytes[1] + + # determine data length and the first index of the data part + current_data_length: int = b2 & 0b01111111 + idx_after_length_part: int = 2 + if current_data_length == 126: + if len(remaining_bytes) < 4: + remaining_bytes += receive(1024) + current_data_length = struct.unpack("!H", bytes(remaining_bytes[2:4]))[0] + idx_after_length_part = 4 + elif current_data_length == 127: + if len(remaining_bytes) < 10: + remaining_bytes += receive(1024) + current_data_length = struct.unpack("!Q", bytes(remaining_bytes[2:10]))[0] + idx_after_length_part = 10 + + current_header = FrameHeader( + fin=b1 & 0b10000000, + rsv1=b1 & 0b01000000, + rsv2=b1 & 0b00100000, + rsv3=b1 & 0b00010000, + opcode=b1 & 0b00001111, + masked=b2 & 0b10000000, + length=current_data_length, + ) + if current_header.masked > 0: + if current_mask_key is None: + idx1, idx2 = idx_after_length_part, idx_after_length_part + 4 + current_mask_key = remaining_bytes[idx1:idx2] # type: ignore[assignment] + idx_after_length_part += 4 + + start, end = idx_after_length_part, idx_after_length_part + current_data_length + data_to_append = remaining_bytes[start:end] + + current_data = bytes() + if current_header.masked > 0: + for i in range(data_to_append): # type: ignore[call-overload] + mask = current_mask_key[i % 4] # type: ignore[index] + data_to_append[i] ^= mask # type: ignore[index] + current_data += data_to_append + else: + current_data += data_to_append + if len(current_data) == current_data_length: + _append_message(messages, current_header, current_data) + remaining_bytes = remaining_bytes[end:] + if len(remaining_bytes) > 0: + # continue with the remaining data + return _fetch_messages( + messages=messages, + receive=receive, + remaining_bytes=remaining_bytes, + logger=logger, + ) + else: + return messages + elif len(current_data) < current_data_length: + # need more bytes to complete this message + return _fetch_messages( + messages=messages, + receive=receive, + current_mask_key=current_mask_key, + current_header=current_header, + current_data=current_data, + logger=logger, + ) + else: + # This pattern is unexpected but set data with the expected length anyway + _append_message(current_header, current_data[:current_data_length]) # type: ignore[call-arg, arg-type] + return messages + + # work in progress with the current_header/current_data + if current_header is not None: + length_needed = current_header.length - len(current_data) # type: ignore[arg-type] + if length_needed > len(remaining_bytes): + current_data += remaining_bytes # type: ignore[operator] + # need more bytes to complete this message + return _fetch_messages( + messages=messages, + receive=receive, + current_mask_key=current_mask_key, + current_header=current_header, + current_data=current_data, + logger=logger, + ) + else: + current_data += remaining_bytes[:length_needed] # type: ignore[operator] + _append_message(messages, current_header, current_data) + remaining_bytes = remaining_bytes[length_needed:] + if len(remaining_bytes) == 0: + return messages + else: + # continue with the remaining data + return _fetch_messages( + messages=messages, + receive=receive, + remaining_bytes=remaining_bytes, + logger=logger, + ) + + return messages + + +def _append_message( + messages: List[Tuple[Optional[FrameHeader], bytes]], + header: Optional[FrameHeader], + data: bytes, +) -> None: + messages.append((header, data)) + + +def _build_data_frame_for_sending( + payload: Union[str, bytes], + opcode: int, + fin: int = 1, + rsv1: int = 0, + rsv2: int = 0, + rsv3: int = 0, + masked: int = 1, +): + b1 = fin << 7 | rsv1 << 6 | rsv2 << 5 | rsv3 << 4 | opcode + header: bytes = bytes([b1]) + + original_payload_data: bytes = payload.encode("utf-8") if isinstance(payload, str) else payload + payload_length = len(original_payload_data) + if payload_length <= 125: + b2 = masked << 7 | payload_length + header += bytes([b2]) + else: + b2 = masked << 7 | 126 + header += struct.pack("!BH", b2, payload_length) + + mask_key: List[int] = random.choices(range(256), k=4) + header += bytes(mask_key) + + payload_data: bytes = bytes(byte ^ mask for byte, mask in zip(original_payload_data, itertools.cycle(mask_key))) + return header + payload_data diff --git a/slack_sdk/socket_mode/client.py b/slack_sdk/socket_mode/client.py new file mode 100644 index 000000000..0fa0f9a25 --- /dev/null +++ b/slack_sdk/socket_mode/client.py @@ -0,0 +1,157 @@ +import json +import logging +import time +from queue import Queue, Empty +from concurrent.futures.thread import ThreadPoolExecutor +from logging import Logger +from threading import Lock +from typing import Dict, Union, Any, Optional, List, Callable + +from slack_sdk.errors import SlackApiError +from slack_sdk.socket_mode.interval_runner import IntervalRunner +from slack_sdk.socket_mode.listeners import ( + WebSocketMessageListener, + SocketModeRequestListener, +) +from slack_sdk.socket_mode.request import SocketModeRequest +from slack_sdk.socket_mode.response import SocketModeResponse +from slack_sdk.web import WebClient + + +class BaseSocketModeClient: + logger: Logger + web_client: WebClient + app_token: str + wss_uri: str + message_queue: Queue + message_listeners: List[ + Union[ + WebSocketMessageListener, + Callable[["BaseSocketModeClient", dict, Optional[str]], None], + ] + ] + socket_mode_request_listeners: List[ + Union[ + SocketModeRequestListener, + Callable[["BaseSocketModeClient", SocketModeRequest], None], + ] + ] + + message_processor: IntervalRunner + message_workers: ThreadPoolExecutor + + closed: bool + connect_operation_lock: Lock + + def issue_new_wss_url(self) -> str: + try: + response = self.web_client.apps_connections_open(app_token=self.app_token) + return response["url"] + except SlackApiError as e: + if e.response["error"] == "ratelimited": + # NOTE: ratelimited errors rarely occur with this endpoint + delay = int(e.response.headers.get("Retry-After", "30")) # Tier1 + self.logger.info(f"Rate limited. Retrying in {delay} seconds...") + time.sleep(delay) + # Retry to issue a new WSS URL + return self.issue_new_wss_url() + else: + # other errors + self.logger.error(f"Failed to retrieve WSS URL: {e}") + raise e + + def is_connected(self) -> bool: + return False + + def connect(self) -> None: + raise NotImplementedError() + + def disconnect(self) -> None: + raise NotImplementedError() + + def connect_to_new_endpoint(self, force: bool = False): + try: + self.connect_operation_lock.acquire(blocking=True, timeout=5) + if force or not self.is_connected(): + self.logger.info("Connecting to a new endpoint...") + self.wss_uri = self.issue_new_wss_url() + self.connect() + self.logger.info("Connected to a new endpoint...") + finally: + self.connect_operation_lock.release() + + def close(self) -> None: + self.closed = True + self.disconnect() + + def send_message(self, message: str) -> None: + raise NotImplementedError() + + def send_socket_mode_response(self, response: Union[Dict[str, Any], SocketModeResponse]) -> None: + if isinstance(response, SocketModeResponse): + self.send_message(json.dumps(response.to_dict())) + else: + self.send_message(json.dumps(response)) + + def enqueue_message(self, message: str): + self.message_queue.put(message) + if self.logger.level <= logging.DEBUG: + self.logger.debug(f"A new message enqueued (current queue size: {self.message_queue.qsize()})") + + def process_message(self): + try: + raw_message = self.message_queue.get(timeout=1) + if self.logger.level <= logging.DEBUG: + self.logger.debug(f"A message dequeued (current queue size: {self.message_queue.qsize()})") + + if raw_message is not None: + message: dict = {} + if raw_message.startswith("{"): + message = json.loads(raw_message) + if message.get("type") == "disconnect": + self.connect_to_new_endpoint(force=True) + else: + + def _run_message_listeners(): + self.run_message_listeners(message, raw_message) + + self.message_workers.submit(_run_message_listeners) + except Empty: + pass + + def run_message_listeners(self, message: dict, raw_message: str) -> None: + type, envelope_id = message.get("type"), message.get("envelope_id") + if self.logger.level <= logging.DEBUG: + self.logger.debug(f"Message processing started (type: {type}, envelope_id: {envelope_id})") + try: + # just in case, adding the same logic to reconnect here + if message.get("type") == "disconnect": + self.connect_to_new_endpoint(force=True) + return + + for listener in self.message_listeners: + try: + listener(self, message, raw_message) # type: ignore[call-arg, arg-type, misc] + except Exception as e: + self.logger.exception(f"Failed to run a message listener: {e}") + + if len(self.socket_mode_request_listeners) > 0: + request = SocketModeRequest.from_dict(message) + if request is not None: + for listener in self.socket_mode_request_listeners: # type: ignore[assignment] + try: + listener(self, request) # type: ignore[call-arg, arg-type] + except Exception as e: + self.logger.exception(f"Failed to run a request listener: {e}") + except Exception as e: + self.logger.exception(f"Failed to run message listeners: {e}") + finally: + if self.logger.level <= logging.DEBUG: + self.logger.debug(f"Message processing completed (type: {type}, envelope_id: {envelope_id})") + + def process_messages(self) -> None: + while not self.closed: + try: + self.process_message() + except Exception as e: + self.logger.exception(f"Failed to process a message: {e}") diff --git a/slack_sdk/socket_mode/interval_runner.py b/slack_sdk/socket_mode/interval_runner.py new file mode 100644 index 000000000..2e7f132be --- /dev/null +++ b/slack_sdk/socket_mode/interval_runner.py @@ -0,0 +1,33 @@ +import threading +from threading import Thread, Event +from typing import Callable + + +class IntervalRunner: + event: Event + thread: Thread + + def __init__(self, target: Callable[[], None], interval_seconds: float = 0.1): + self.event = threading.Event() + self.target = target + self.interval_seconds = interval_seconds + self.thread = threading.Thread(target=self._run) + self.thread.daemon = True + + def _run(self) -> None: + while not self.event.is_set(): + self.target() + self.event.wait(self.interval_seconds) + + def start(self) -> "IntervalRunner": + self.thread.start() + return self + + def is_alive(self) -> bool: + return self.thread is not None and self.thread.is_alive() + + def shutdown(self): + if self.is_alive(): + self.event.set() + self.thread.join() + self.thread = None diff --git a/slack_sdk/socket_mode/listeners.py b/slack_sdk/socket_mode/listeners.py new file mode 100644 index 000000000..37203fc35 --- /dev/null +++ b/slack_sdk/socket_mode/listeners.py @@ -0,0 +1,17 @@ +from typing import Optional + +from slack_sdk.socket_mode.request import SocketModeRequest + + +class WebSocketMessageListener: + def __call__( + client: "BaseSocketModeClient", # type: ignore[name-defined] # noqa: F821 + message: dict, + raw_message: Optional[str] = None, + ): # noqa: F821 + raise NotImplementedError() + + +class SocketModeRequestListener: + def __call__(client: "BaseSocketModeClient", request: SocketModeRequest): # type: ignore[name-defined] # noqa: F821, F821, E501 + raise NotImplementedError() diff --git a/slack_sdk/socket_mode/logger/__init__.py b/slack_sdk/socket_mode/logger/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/slack_sdk/socket_mode/logger/messages.py b/slack_sdk/socket_mode/logger/messages.py new file mode 100644 index 000000000..d10f7ee20 --- /dev/null +++ b/slack_sdk/socket_mode/logger/messages.py @@ -0,0 +1,6 @@ +import re + + +def debug_redacted_message_string(message: str) -> str: + xwfp_token_pattern = re.compile(r"\"xwfp-[A-Za-z0-9\-]+\"") # ex: "xwfp-abc-ABC-1234" + return re.sub(xwfp_token_pattern, "[[REDACTED]]", message) diff --git a/slack_sdk/socket_mode/request.py b/slack_sdk/socket_mode/request.py new file mode 100644 index 000000000..4610e8ba7 --- /dev/null +++ b/slack_sdk/socket_mode/request.py @@ -0,0 +1,57 @@ +from typing import Union, Optional + +from slack_sdk.models import JsonObject + + +class SocketModeRequest: + type: str + envelope_id: str + payload: dict + accepts_response_payload: bool + retry_attempt: Optional[int] # events_api + retry_reason: Optional[str] # events_api + + def __init__( + self, + type: str, + envelope_id: str, + payload: Union[dict, JsonObject, str], + accepts_response_payload: Optional[bool] = None, + retry_attempt: Optional[int] = None, + retry_reason: Optional[str] = None, + ): + self.type = type + self.envelope_id = envelope_id + + if isinstance(payload, JsonObject): + self.payload = payload.to_dict() + elif isinstance(payload, dict): + self.payload = payload + elif isinstance(payload, str): + self.payload = {"text": payload} + else: + unexpected_payload_type = type(payload) + raise ValueError(f"Unsupported payload data type ({unexpected_payload_type})") + + self.accepts_response_payload = accepts_response_payload or False + self.retry_attempt = retry_attempt + self.retry_reason = retry_reason + + @classmethod + def from_dict(cls, message: dict) -> Optional["SocketModeRequest"]: + if all(k in message for k in ("type", "envelope_id", "payload")): + return SocketModeRequest( + type=message["type"], + envelope_id=message["envelope_id"], + payload=message["payload"], + accepts_response_payload=message.get("accepts_response_payload") or False, + retry_attempt=message.get("retry_attempt"), + retry_reason=message.get("retry_reason"), + ) + return None + + def to_dict(self) -> dict: + d = {"envelope_id": self.envelope_id} + if self.payload is not None: + d["payload"] = self.payload # type: ignore[assignment] + return d diff --git a/slack_sdk/socket_mode/response.py b/slack_sdk/socket_mode/response.py new file mode 100644 index 000000000..7abcc14ac --- /dev/null +++ b/slack_sdk/socket_mode/response.py @@ -0,0 +1,28 @@ +from typing import Union, Optional + +from slack_sdk.models import JsonObject + + +class SocketModeResponse: + envelope_id: str + payload: Optional[dict] + + def __init__(self, envelope_id: str, payload: Optional[Union[dict, JsonObject, str]] = None): + self.envelope_id = envelope_id + + if payload is None: + self.payload = None + elif isinstance(payload, JsonObject): + self.payload = payload.to_dict() + elif isinstance(payload, dict): + self.payload = payload + elif isinstance(payload, str): + self.payload = {"text": payload} + else: + raise ValueError(f"Unsupported payload data type ({type(payload)})") + + def to_dict(self) -> dict: + d = {"envelope_id": self.envelope_id} + if self.payload is not None: + d["payload"] = self.payload # type: ignore[assignment] + return d diff --git a/slack_sdk/socket_mode/websocket_client/__init__.py b/slack_sdk/socket_mode/websocket_client/__init__.py new file mode 100644 index 000000000..ae7df4420 --- /dev/null +++ b/slack_sdk/socket_mode/websocket_client/__init__.py @@ -0,0 +1,264 @@ +"""websocket-client bassd Socket Mode client + +* https://docs.slack.dev/apis/events-api/using-socket-mode/ +* https://docs.slack.dev/tools/python-slack-sdk/socket-mode/ +* https://pypi.org/project/websocket-client/ + +""" + +import logging +from concurrent.futures.thread import ThreadPoolExecutor +from logging import Logger +from queue import Queue +from threading import Lock +from typing import Callable, List, Optional, Tuple, Union + +import websocket +from websocket import WebSocketApp, WebSocketException + +from slack_sdk.socket_mode.client import BaseSocketModeClient +from slack_sdk.socket_mode.interval_runner import IntervalRunner +from slack_sdk.socket_mode.listeners import ( + WebSocketMessageListener, + SocketModeRequestListener, +) +from slack_sdk.socket_mode.request import SocketModeRequest +from slack_sdk.web import WebClient + +from ..logger.messages import debug_redacted_message_string + + +class SocketModeClient(BaseSocketModeClient): + logger: Logger + web_client: WebClient + app_token: str + wss_uri: Optional[str] # type: ignore[assignment] + message_queue: Queue + message_listeners: List[ + Union[ + WebSocketMessageListener, + Callable[["BaseSocketModeClient", dict, Optional[str]], None], + ] + ] + socket_mode_request_listeners: List[ + Union[ + SocketModeRequestListener, + Callable[["BaseSocketModeClient", SocketModeRequest], None], + ] + ] + + current_app_monitor: IntervalRunner + current_app_monitor_started: bool + message_processor: IntervalRunner + message_workers: ThreadPoolExecutor + + current_session: Optional[WebSocketApp] + current_session_runner: IntervalRunner + + auto_reconnect_enabled: bool + default_auto_reconnect_enabled: bool + + closed: bool + connect_operation_lock: Lock + + on_open_listeners: List[Callable[[WebSocketApp], None]] + on_message_listeners: List[Callable[[WebSocketApp, str], None]] + on_error_listeners: List[Callable[[WebSocketApp, Exception], None]] + on_close_listeners: List[Callable[[WebSocketApp], None]] + + def __init__( + self, + app_token: str, + logger: Optional[Logger] = None, + web_client: Optional[WebClient] = None, + auto_reconnect_enabled: bool = True, + ping_interval: float = 10, + concurrency: int = 10, + trace_enabled: bool = False, + http_proxy_host: Optional[str] = None, + http_proxy_port: Optional[int] = None, + http_proxy_auth: Optional[Tuple[str, str]] = None, + proxy_type: Optional[str] = None, + on_open_listeners: Optional[List[Callable[[WebSocketApp], None]]] = None, + on_message_listeners: Optional[List[Callable[[WebSocketApp, str], None]]] = None, + on_error_listeners: Optional[List[Callable[[WebSocketApp, Exception], None]]] = None, + on_close_listeners: Optional[List[Callable[[WebSocketApp], None]]] = None, + ): + """ + + Args: + app_token: App-level token + logger: Custom logger + web_client: Web API client + auto_reconnect_enabled: True if automatic reconnection is enabled (default: True) + ping_interval: interval for ping-pong with Slack servers (seconds) + concurrency: the size of thread pool (default: 10) + http_proxy_host: the HTTP proxy host + http_proxy_port: the HTTP proxy port + http_proxy_auth: the HTTP proxy username & password + proxy_type: the HTTP proxy type + on_open_listeners: listener functions for on_open + on_message_listeners: listener functions for on_message + on_error_listeners: listener functions for on_error + on_close_listeners: listener functions for on_close + """ + self.app_token = app_token + self.logger = logger or logging.getLogger(__name__) + self.web_client = web_client or WebClient() + self.default_auto_reconnect_enabled = auto_reconnect_enabled + self.auto_reconnect_enabled = self.default_auto_reconnect_enabled + self.ping_interval = ping_interval + self.wss_uri = None + self.message_queue = Queue() + self.message_listeners = [] + self.socket_mode_request_listeners = [] + + self.current_session = None + self.current_session_runner = IntervalRunner(self._run_current_session, 0.5).start() + + self.current_app_monitor_started = False + self.current_app_monitor = IntervalRunner(self._monitor_current_session, self.ping_interval) + + self.closed = False + self.connect_operation_lock = Lock() + + self.message_processor = IntervalRunner(self.process_messages, 0.001).start() + self.message_workers = ThreadPoolExecutor(max_workers=concurrency) + + # NOTE: only global settings is provided by the library + websocket.enableTrace(trace_enabled) + + self.http_proxy_host = http_proxy_host + self.http_proxy_port = http_proxy_port + self.http_proxy_auth = http_proxy_auth + self.proxy_type = proxy_type + + self.on_open_listeners = on_open_listeners or [] + self.on_message_listeners = on_message_listeners or [] + self.on_error_listeners = on_error_listeners or [] + self.on_close_listeners = on_close_listeners or [] + + def is_connected(self) -> bool: + return self.current_session is not None and self.current_session.sock is not None + + def connect(self) -> None: + def on_open(ws: WebSocketApp): + if self.logger.level <= logging.DEBUG: + self.logger.debug("on_open invoked") + for listener in self.on_open_listeners: + listener(ws) + + def on_message(ws: WebSocketApp, message: str): + if self.logger.level <= logging.DEBUG: + self.logger.debug(f"on_message invoked: (message: {debug_redacted_message_string(message)})") + self.enqueue_message(message) + for listener in self.on_message_listeners: + listener(ws, message) + + def on_error(ws: WebSocketApp, error: Exception): + self.logger.error(f"on_error invoked (error: {type(error).__name__}, message: {error})") + for listener in self.on_error_listeners: + listener(ws, error) + + def on_close( + ws: WebSocketApp, + close_status_code: Optional[int] = None, + close_msg: Optional[str] = None, + ): + if self.logger.level <= logging.DEBUG: + self.logger.debug(f"on_close invoked: (code: {close_status_code}, message: {close_msg})") + if self.auto_reconnect_enabled: + self.logger.info("Received CLOSE event. Reconnecting...") + self.connect_to_new_endpoint() + for listener in self.on_close_listeners: + listener(ws) + + old_session: Optional[WebSocketApp] = self.current_session + + if self.wss_uri is None: + self.wss_uri = self.issue_new_wss_url() + + self.current_session = websocket.WebSocketApp( + self.wss_uri, + on_open=on_open, + on_message=on_message, + on_error=on_error, + on_close=on_close, + ) + self.auto_reconnect_enabled = self.default_auto_reconnect_enabled + + if not self.current_app_monitor_started: + self.current_app_monitor_started = True + self.current_app_monitor.start() + + if old_session is not None: + old_session.close() + + self.logger.info("A new session has been established") + + def disconnect(self) -> None: + if self.current_session is not None: + self.current_session.close() + + def send_message(self, message: str) -> None: + if self.logger.level <= logging.DEBUG: + self.logger.debug(f"Sending a message: {message}") + try: + self.current_session.send(message) # type: ignore[union-attr] + except WebSocketException as e: + # We rarely get this exception while replacing the underlying WebSocket connections. + # We can do one more try here as the self.current_session should be ready now. + if self.logger.level <= logging.DEBUG: + self.logger.debug( + f"Failed to send a message (error: {e}, message: {message})" + " as the underlying connection was replaced. Retrying the same request only one time..." + ) + # Although acquiring self.connect_operation_lock also for the first method call is the safest way, + # we avoid synchronizing a lot for better performance. That's why we are doing a retry here. + with self.connect_operation_lock: + if self.is_connected(): + self.current_session.send(message) # type: ignore[union-attr] + else: + self.logger.warning( + f"The current session (session id: {self.session_id()}) is no longer active. " # type: ignore[attr-defined] # noqa: E501 + "Failed to send a message" + ) + raise e + + def close(self) -> None: + self.closed = True + self.auto_reconnect_enabled = False + self.disconnect() + self.current_app_monitor.shutdown() + self.message_processor.shutdown() + self.message_workers.shutdown() + + def _run_current_session(self): + if self.current_session is not None: + try: + self.logger.info("Starting to receive messages from a new connection") + self.current_session.run_forever( + ping_interval=self.ping_interval, + http_proxy_host=self.http_proxy_host, + http_proxy_port=self.http_proxy_port, + http_proxy_auth=self.http_proxy_auth, + proxy_type=self.proxy_type, + ) + self.logger.info("Stopped receiving messages from a connection") + except Exception as e: + self.logger.exception(f"Failed to start or stop the current session: {e}") + # To let the monitoring job detect the connection issue, closing this session + if self.current_session is not None: + self.current_session.close() + + def _monitor_current_session(self): + if self.current_app_monitor_started: + try: + if self.auto_reconnect_enabled and (self.current_session is None or self.current_session.sock is None): + self.logger.info("The session seems to be already closed. Reconnecting...") + self.connect_to_new_endpoint() + except Exception as e: + self.logger.error( + "Failed to check the current session or reconnect to the server " + f"(error: {type(e).__name__}, message: {e})" + ) diff --git a/slack_sdk/socket_mode/websockets/__init__.py b/slack_sdk/socket_mode/websockets/__init__.py new file mode 100644 index 000000000..eaa00cf65 --- /dev/null +++ b/slack_sdk/socket_mode/websockets/__init__.py @@ -0,0 +1,272 @@ +"""websockets bassd Socket Mode client + +* https://docs.slack.dev/apis/events-api/using-socket-mode/ +* https://docs.slack.dev/tools/python-slack-sdk/socket-mode/ +* https://pypi.org/project/websockets/ + +""" + +import asyncio +import logging +from asyncio import Future, Lock +from logging import Logger +from asyncio import Queue +from typing import Union, Optional, List, Callable, Awaitable + +import websockets +from websockets.exceptions import WebSocketException + +try: + from websockets.asyncio.client import ClientConnection +except ImportError: + # To keep compatibility with websockets <14.x we use WebSocketClientProtocol + # To keep compatibility with websockets 8.x, we use this import over .legacy.client + from websockets import WebSocketClientProtocol as ClientConnection # type: ignore[no-redef, attr-defined] + + +from slack_sdk.socket_mode.async_client import AsyncBaseSocketModeClient +from slack_sdk.socket_mode.async_listeners import ( + AsyncWebSocketMessageListener, + AsyncSocketModeRequestListener, +) +from slack_sdk.socket_mode.request import SocketModeRequest +from slack_sdk.web.async_client import AsyncWebClient + +from ..logger.messages import debug_redacted_message_string + + +def _session_closed(session: Optional[ClientConnection]) -> bool: + if session is None: + return True + if hasattr(session, "closed"): + # The session is a WebSocketClientProtocol instance + return session.closed + # WebSocket close code, defined in https://datatracker.ietf.org/doc/html/rfc6455.html#section-7.1.5 + # None if the connection isn’t closed yet. + return session.close_code is not None + + +class SocketModeClient(AsyncBaseSocketModeClient): + logger: Logger + web_client: AsyncWebClient + app_token: str + wss_uri: Optional[str] # type: ignore[assignment] + auto_reconnect_enabled: bool + message_queue: Queue + message_listeners: List[ + Union[ + AsyncWebSocketMessageListener, + Callable[["AsyncBaseSocketModeClient", dict, Optional[str]], Awaitable[None]], + ] + ] + socket_mode_request_listeners: List[ + Union[ + AsyncSocketModeRequestListener, + Callable[["AsyncBaseSocketModeClient", SocketModeRequest], Awaitable[None]], + ] + ] + + message_receiver: Optional[Future] + message_processor: Future + + ping_interval: float + trace_enabled: bool + + current_session: Optional[ClientConnection] + current_session_monitor: Optional[Future] + + default_auto_reconnect_enabled: bool + closed: bool + connect_operation_lock: Lock + + def __init__( + self, + app_token: str, + logger: Optional[Logger] = None, + web_client: Optional[AsyncWebClient] = None, + auto_reconnect_enabled: bool = True, + ping_interval: float = 10, + trace_enabled: bool = False, + ): + """Socket Mode client + + Args: + app_token: App-level token + logger: Custom logger + web_client: Web API client + auto_reconnect_enabled: True if automatic reconnection is enabled (default: True) + ping_interval: interval for ping-pong with Slack servers (seconds) + trace_enabled: True if more verbose logs to see what's happening under the hood + """ + self.app_token = app_token + self.logger = logger or logging.getLogger(__name__) + self.web_client = web_client or AsyncWebClient() + self.closed = False + self.connect_operation_lock = Lock() + self.default_auto_reconnect_enabled = auto_reconnect_enabled + self.auto_reconnect_enabled = self.default_auto_reconnect_enabled + self.ping_interval = ping_interval + self.trace_enabled = trace_enabled + self.wss_uri = None + self.message_queue = Queue() + self.message_listeners = [] + self.socket_mode_request_listeners = [] + self.current_session = None + self.current_session_monitor = None + + self.message_receiver = None + self.message_processor = asyncio.ensure_future(self.process_messages()) + + async def monitor_current_session(self) -> None: + # In the asyncio runtime, accessing a shared object (self.current_session here) from + # multiple tasks can cause race conditions and errors. + # To avoid such, we access only the session that is active when this loop starts. + session: ClientConnection = self.current_session # type: ignore[assignment] + session_id: str = await self.session_id() + if self.logger.level <= logging.DEBUG: + self.logger.debug(f"A new monitor_current_session() execution loop for {session_id} started") + try: + while not self.closed: + if session != self.current_session: + if self.logger.level <= logging.DEBUG: + self.logger.debug(f"The monitor_current_session task for {session_id} is now cancelled") + break + await asyncio.sleep(self.ping_interval) + try: + if self.auto_reconnect_enabled and _session_closed(session=session): + self.logger.info(f"The session ({session_id}) seems to be already closed. Reconnecting...") + await self.connect_to_new_endpoint() + except Exception as e: + self.logger.error( + "Failed to check the current session or reconnect to the server " + f"(error: {type(e).__name__}, message: {e}, session: {session_id})" + ) + except asyncio.CancelledError: + if self.logger.level <= logging.DEBUG: + self.logger.debug(f"The monitor_current_session task for {session_id} is now cancelled") + raise + + async def receive_messages(self) -> None: + # In the asyncio runtime, accessing a shared object (self.current_session here) from + # multiple tasks can cause race conditions and errors. + # To avoid such, we access only the session that is active when this loop starts. + session: ClientConnection = self.current_session # type: ignore[assignment] + session_id: str = await self.session_id() + consecutive_error_count = 0 + if self.logger.level <= logging.DEBUG: + self.logger.debug(f"A new receive_messages() execution loop with {session_id} started") + try: + while not self.closed: + if session != self.current_session: + if self.logger.level <= logging.DEBUG: + self.logger.debug(f"The running receive_messages task for {session_id} is now cancelled") + break + try: + message = await session.recv() + if message is not None: + if isinstance(message, bytes): + message = message.decode("utf-8") + if self.logger.level <= logging.DEBUG: + self.logger.debug( + f"Received message: {debug_redacted_message_string(message)}, session: {session_id}" + ) + await self.enqueue_message(message) + consecutive_error_count = 0 + except Exception as e: + consecutive_error_count += 1 + self.logger.error( + f"Failed to receive or enqueue a message: {type(e).__name__}, error: {e}, session: {session_id}" + ) + if isinstance(e, websockets.ConnectionClosedError): + await asyncio.sleep(self.ping_interval) + else: + await asyncio.sleep(consecutive_error_count) + except asyncio.CancelledError: + if self.logger.level <= logging.DEBUG: + self.logger.debug(f"The running receive_messages task for {session_id} is now cancelled") + raise + + async def is_connected(self) -> bool: + return not self.closed and not _session_closed(self.current_session) + + async def session_id(self) -> str: + return self.build_session_id(self.current_session) # type: ignore[arg-type] + + async def connect(self): + if self.wss_uri is None: + self.wss_uri = await self.issue_new_wss_url() + old_session: Optional[ClientConnection] = None if self.current_session is None else self.current_session + # NOTE: websockets does not support proxy settings + self.current_session = await websockets.connect( + uri=self.wss_uri, + ping_interval=self.ping_interval, + ) + session_id = await self.session_id() + self.auto_reconnect_enabled = self.default_auto_reconnect_enabled + self.logger.info(f"A new session ({session_id}) has been established") + + if self.current_session_monitor is not None: + self.current_session_monitor.cancel() + self.current_session_monitor = asyncio.ensure_future(self.monitor_current_session()) + + if self.logger.level <= logging.DEBUG: + self.logger.debug(f"A new monitor_current_session() executor has been recreated for {session_id}") + + if self.message_receiver is not None: + self.message_receiver.cancel() + self.message_receiver = asyncio.ensure_future(self.receive_messages()) + + if self.logger.level <= logging.DEBUG: + self.logger.debug(f"A new receive_messages() executor has been recreated for {session_id}") + + if old_session is not None: + await old_session.close() + old_session_id = self.build_session_id(old_session) + self.logger.info(f"The old session ({old_session_id}) has been abandoned") + + async def disconnect(self): + if self.current_session is not None: + await self.current_session.close() + + async def send_message(self, message: str): + session = self.current_session + session_id = self.build_session_id(session) # type: ignore[arg-type] + if self.logger.level <= logging.DEBUG: + self.logger.debug(f"Sending a message: {message}, session: {session_id}") + try: + await session.send(message) # type: ignore[union-attr] + except WebSocketException as e: + # We rarely get this exception while replacing the underlying WebSocket connections. + # We can do one more try here as the self.current_session should be ready now. + if self.logger.level <= logging.DEBUG: + self.logger.debug( + f"Failed to send a message (error: {e}, message: {message}, session: {session_id})" + " as the underlying connection was replaced. Retrying the same request only one time..." + ) + # Although acquiring self.connect_operation_lock also for the first method call is the safest way, + # we avoid synchronizing a lot for better performance. That's why we are doing a retry here. + try: + if await self.is_connected(): + await self.current_session.send(message) # type: ignore[union-attr] + else: + self.logger.warning(f"The current session ({session_id}) is no longer active. Failed to send a message") + raise e + finally: + if self.connect_operation_lock.locked() is True: + self.connect_operation_lock.release() + + async def close(self): + self.closed = True + self.auto_reconnect_enabled = False + await self.disconnect() + self.message_processor.cancel() + if self.current_session_monitor is not None: + self.current_session_monitor.cancel() + if self.message_receiver is not None: + self.message_receiver.cancel() + + @classmethod + def build_session_id(cls, session: ClientConnection) -> str: + if session is None: + return "" + return "s_" + str(hash(session)) diff --git a/slack_sdk/version.py b/slack_sdk/version.py new file mode 100644 index 000000000..fb572e0ba --- /dev/null +++ b/slack_sdk/version.py @@ -0,0 +1,3 @@ +"""Check the latest version at https://pypi.org/project/slack-sdk/""" + +__version__ = "3.39.0" diff --git a/slack_sdk/web/__init__.py b/slack_sdk/web/__init__.py new file mode 100644 index 000000000..41f3b5a77 --- /dev/null +++ b/slack_sdk/web/__init__.py @@ -0,0 +1,10 @@ +"""The Slack Web API allows you to build applications that interact with Slack +in more complex ways than the integrations we provide out of the box.""" + +from .client import WebClient +from .slack_response import SlackResponse + +__all__ = [ + "WebClient", + "SlackResponse", +] diff --git a/slack_sdk/web/async_base_client.py b/slack_sdk/web/async_base_client.py new file mode 100644 index 000000000..ebb0eb3d0 --- /dev/null +++ b/slack_sdk/web/async_base_client.py @@ -0,0 +1,255 @@ +import logging +from ssl import SSLContext +from typing import Optional, Union, Dict, Any, List + +import aiohttp +from aiohttp import FormData, BasicAuth + +from .async_internal_utils import ( + _files_to_data, + _request_with_session, +) +from .async_slack_response import AsyncSlackResponse +from .deprecation import show_deprecation_warning_if_any +from .file_upload_v2_result import FileUploadV2Result +from .internal_utils import ( + convert_bool_to_0_or_1, + _build_req_args, + _get_url, + get_user_agent, +) +from ..proxy_env_variable_loader import load_http_proxy_from_env + +from slack_sdk.http_retry.builtin_async_handlers import async_default_handlers +from slack_sdk.http_retry.async_handler import AsyncRetryHandler + + +class AsyncBaseClient: + BASE_URL = "https://slack.com/api/" + + def __init__( + self, + token: Optional[str] = None, + base_url: str = BASE_URL, + timeout: int = 30, + ssl: Optional[SSLContext] = None, + proxy: Optional[str] = None, + session: Optional[aiohttp.ClientSession] = None, + trust_env_in_session: bool = False, + headers: Optional[dict] = None, + user_agent_prefix: Optional[str] = None, + user_agent_suffix: Optional[str] = None, + # for Org-Wide App installation + team_id: Optional[str] = None, + logger: Optional[logging.Logger] = None, + retry_handlers: Optional[List[AsyncRetryHandler]] = None, + ): + self.token = None if token is None else token.strip() + """A string specifying an `xoxp-*` or `xoxb-*` token.""" + if not base_url.endswith("/"): + base_url += "/" + self.base_url = base_url + """A string representing the Slack API base URL. + Default is `'https://slack.com/api/'`.""" + self.timeout = timeout + """The maximum number of seconds the client will wait + to connect and receive a response from Slack. + Default is 30 seconds.""" + self.ssl = ssl + """An [`ssl.SSLContext`](https://docs.python.org/3/library/ssl.html#ssl.SSLContext) + instance, helpful for specifying your own custom + certificate chain.""" + self.proxy = proxy + """String representing a fully-qualified URL to a proxy through which + to route all requests to the Slack API. Even if this parameter + is not specified, if any of the following environment variables are + present, they will be loaded into this parameter: `HTTPS_PROXY`, + `https_proxy`, `HTTP_PROXY` or `http_proxy`.""" + self.session = session + """An [`aiohttp.ClientSession`](https://docs.aiohttp.org/en/stable/client_reference.html#client-session) + to attach to all outgoing requests.""" + # https://github.com/slackapi/python-slack-sdk/issues/738 + self.trust_env_in_session = trust_env_in_session + """Boolean setting whether aiohttp outgoing requests + are allowed to read environment variables. Commonly used in conjunction + with proxy support via the `HTTPS_PROXY`, `https_proxy`, `HTTP_PROXY` and + `http_proxy` environment variables.""" + self.headers = headers or {} + """`dict` representing additional request headers to attach to all requests.""" + self.headers["User-Agent"] = get_user_agent(user_agent_prefix, user_agent_suffix) + self.default_params = {} + if team_id is not None: + self.default_params["team_id"] = team_id + self._logger = logger if logger is not None else logging.getLogger(__name__) + self.retry_handlers = retry_handlers if retry_handlers is not None else async_default_handlers() + + if self.proxy is None or len(self.proxy.strip()) == 0: + env_variable = load_http_proxy_from_env(self._logger) + if env_variable is not None: + self.proxy = env_variable + + # ------------------------- + # accessors + + @property + def logger(self) -> logging.Logger: + """The logger this client uses.""" + return self._logger + + # ------------------------- + # api call + + async def api_call( + self, + api_method: str, + *, + http_verb: str = "POST", + files: Optional[dict] = None, + data: Optional[Union[dict, FormData]] = None, + params: Optional[dict] = None, + json: Optional[dict] = None, + headers: Optional[dict] = None, + auth: Optional[dict] = None, + ) -> AsyncSlackResponse: + """Create a request and execute the API call to Slack. + + Args: + api_method (str): The target Slack API method. + e.g. 'chat.postMessage' + http_verb (str): HTTP Verb. e.g. 'POST' + files (dict): Files to multipart upload. + e.g. {image OR file: file_object OR file_path} + data: The body to attach to the request. If a dictionary is + provided, form-encoding will take place. + e.g. {'key1': 'value1', 'key2': 'value2'} + params (dict): The URL parameters to append to the URL. + e.g. {'key1': 'value1', 'key2': 'value2'} + json (dict): JSON for the body to attach to the request + (if files or data is not specified). + e.g. {'key1': 'value1', 'key2': 'value2'} + headers (dict): Additional request headers + auth (dict): A dictionary that consists of client_id and client_secret + + Returns: + (AsyncSlackResponse) + The server's response to an HTTP request. Data + from the response can be accessed like a dict. + If the response included 'next_cursor' it can + be iterated on to execute subsequent requests. + + Raises: + SlackApiError: The following Slack API call failed: + 'chat.postMessage'. + SlackRequestError: Json data can only be submitted as + POST requests. + """ + + api_url = _get_url(self.base_url, api_method) + if auth is not None: + if isinstance(auth, Dict): + auth = BasicAuth(auth["client_id"], auth["client_secret"]) # type: ignore[assignment] + if isinstance(auth, BasicAuth): + if headers is None: + headers = {} + headers["Authorization"] = auth.encode() + auth = None + + headers = headers or {} + headers.update(self.headers) + req_args = _build_req_args( + token=self.token, + http_verb=http_verb, + files=files, # type: ignore[arg-type] + data=data, # type: ignore[arg-type] + default_params=self.default_params, + params=params, # type: ignore[arg-type] + json=json, # type: ignore[arg-type] + headers=headers, + auth=auth, # type: ignore[arg-type] + ssl=self.ssl, + proxy=self.proxy, + ) + + show_deprecation_warning_if_any(api_method) + + return await self._send( + http_verb=http_verb, + api_url=api_url, + req_args=req_args, + ) + + async def _send(self, http_verb: str, api_url: str, req_args: dict) -> AsyncSlackResponse: + """Sends the request out for transmission. + + Args: + http_verb (str): The HTTP verb. e.g. 'GET' or 'POST'. + api_url (str): The Slack API url. e.g. 'https://slack.com/api/chat.postMessage' + req_args (dict): The request arguments to be attached to the request. + e.g. + { + json: { + 'attachments': [{"pretext": "pre-hello", "text": "text-world"}], + 'channel': '#random' + } + } + Returns: + The response parsed into a AsyncSlackResponse object. + """ + open_files = _files_to_data(req_args) + try: + if "params" in req_args: + # True/False -> "1"/"0" + req_args["params"] = convert_bool_to_0_or_1(req_args["params"]) + + res = await self._request(http_verb=http_verb, api_url=api_url, req_args=req_args) + finally: + for f in open_files: + f.close() + + data = { + "client": self, + "http_verb": http_verb, + "api_url": api_url, + "req_args": req_args, + } + return AsyncSlackResponse(**{**data, **res}).validate() + + async def _request(self, *, http_verb, api_url, req_args) -> Dict[str, Any]: + """Submit the HTTP request with the running session or a new session. + Returns: + A dictionary of the response data. + """ + return await _request_with_session( + current_session=self.session, + timeout=self.timeout, + logger=self._logger, + http_verb=http_verb, + api_url=api_url, + req_args=req_args, + retry_handlers=self.retry_handlers, + ) + + async def _upload_file( + self, + *, + url: str, + data: bytes, + logger: logging.Logger, + timeout: int, + proxy: Optional[str], + ssl: Optional[SSLContext], + ) -> FileUploadV2Result: + """Upload a file using the issued upload URL""" + result = await _request_with_session( + current_session=self.session, + timeout=timeout, + logger=logger, + http_verb="POST", + api_url=url, + req_args={"data": data, "proxy": proxy, "ssl": ssl}, + retry_handlers=self.retry_handlers, + ) + return FileUploadV2Result( + status=result.get("status_code"), # type: ignore[arg-type] + body=result.get("body"), # type: ignore[arg-type] + ) diff --git a/slack_sdk/web/async_chat_stream.py b/slack_sdk/web/async_chat_stream.py new file mode 100644 index 000000000..4661f19dd --- /dev/null +++ b/slack_sdk/web/async_chat_stream.py @@ -0,0 +1,212 @@ +# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +# +# *** DO NOT EDIT THIS FILE *** +# +# 1) Modify slack_sdk/web/client.py +# 2) Run `python scripts/codegen.py` +# 3) Run `black slack_sdk/` +# +# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +import json +import logging +from typing import TYPE_CHECKING, Dict, Optional, Sequence, Union + +import slack_sdk.errors as e +from slack_sdk.models.blocks.blocks import Block +from slack_sdk.models.metadata import Metadata +from slack_sdk.web.async_slack_response import AsyncSlackResponse + +if TYPE_CHECKING: + from slack_sdk.web.async_client import AsyncWebClient + + +class AsyncChatStream: + """A helper class for streaming markdown text into a conversation using the chat streaming APIs. + + This class provides a convenient interface for the chat.startStream, chat.appendStream, and chat.stopStream API + methods, with automatic buffering and state management. + """ + + def __init__( + self, + client: "AsyncWebClient", + *, + channel: str, + logger: logging.Logger, + thread_ts: str, + buffer_size: int, + recipient_team_id: Optional[str] = None, + recipient_user_id: Optional[str] = None, + **kwargs, + ): + """Initialize a new ChatStream instance. + + The __init__ method creates a unique ChatStream instance that keeps track of one chat stream. + + Args: + client: The WebClient instance to use for API calls. + channel: An encoded ID that represents a channel, private group, or DM. + logger: A logging channel for outputs. + thread_ts: Provide another message's ts value to reply to. Streamed messages should always be replies to a user + request. + recipient_team_id: The encoded ID of the team the user receiving the streaming text belongs to. Required when + streaming to channels. + recipient_user_id: The encoded ID of the user to receive the streaming text. Required when streaming to channels. + buffer_size: The length of markdown_text to buffer in-memory before calling a method. Increasing this value + decreases the number of method calls made for the same amount of text, which is useful to avoid rate limits. + **kwargs: Additional arguments passed to the underlying API calls. + """ + self._client = client + self._logger = logger + self._token: Optional[str] = kwargs.pop("token", None) + self._stream_args = { + "channel": channel, + "thread_ts": thread_ts, + "recipient_team_id": recipient_team_id, + "recipient_user_id": recipient_user_id, + **kwargs, + } + self._buffer = "" + self._state = "starting" + self._stream_ts: Optional[str] = None + self._buffer_size = buffer_size + + async def append( + self, + *, + markdown_text: str, + **kwargs, + ) -> Optional[AsyncSlackResponse]: + """Append to the stream. + + The "append" method appends to the chat stream being used. This method can be called multiple times. After the stream + is stopped this method cannot be called. + + Args: + markdown_text: Accepts message text formatted in markdown. Limit this field to 12,000 characters. This text is + what will be appended to the message received so far. + **kwargs: Additional arguments passed to the underlying API calls. + + Returns: + AsyncSlackResponse if the buffer was flushed, None if buffering. + + Raises: + SlackRequestError: If the stream is already completed. + + Example: + ```python + streamer = client.chat_stream( + channel="C0123456789", + thread_ts="1700000001.123456", + recipient_team_id="T0123456789", + recipient_user_id="U0123456789", + ) + streamer.append(markdown_text="**hello wo") + streamer.append(markdown_text="rld!**") + streamer.stop() + ``` + """ + if self._state == "completed": + raise e.SlackRequestError(f"Cannot append to stream: stream state is {self._state}") + if kwargs.get("token"): + self._token = kwargs.pop("token") + self._buffer += markdown_text + if len(self._buffer) >= self._buffer_size: + return await self._flush_buffer(**kwargs) + details = { + "buffer_length": len(self._buffer), + "buffer_size": self._buffer_size, + "channel": self._stream_args.get("channel"), + "recipient_team_id": self._stream_args.get("recipient_team_id"), + "recipient_user_id": self._stream_args.get("recipient_user_id"), + "thread_ts": self._stream_args.get("thread_ts"), + } + self._logger.debug(f"ChatStream appended to buffer: {json.dumps(details)}") + return None + + async def stop( + self, + *, + markdown_text: Optional[str] = None, + blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None, + metadata: Optional[Union[Dict, Metadata]] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Stop the stream and finalize the message. + + Args: + blocks: A list of blocks that will be rendered at the bottom of the finalized message. + markdown_text: Accepts message text formatted in markdown. Limit this field to 12,000 characters. This text is + what will be appended to the message received so far. + metadata: JSON object with event_type and event_payload fields, presented as a URL-encoded string. Metadata you + post to Slack is accessible to any app or user who is a member of that workspace. + **kwargs: Additional arguments passed to the underlying API calls. + + Returns: + AsyncSlackResponse from the chat.stopStream API call. + + Raises: + SlackRequestError: If the stream is already completed. + + Example: + ```python + streamer = client.chat_stream( + channel="C0123456789", + thread_ts="1700000001.123456", + recipient_team_id="T0123456789", + recipient_user_id="U0123456789", + ) + streamer.append(markdown_text="**hello wo") + streamer.append(markdown_text="rld!**") + streamer.stop() + ``` + """ + if self._state == "completed": + raise e.SlackRequestError(f"Cannot stop stream: stream state is {self._state}") + if kwargs.get("token"): + self._token = kwargs.pop("token") + if markdown_text: + self._buffer += markdown_text + if not self._stream_ts: + response = await self._client.chat_startStream( + **self._stream_args, + token=self._token, + ) + if not response.get("ts"): + raise e.SlackRequestError("Failed to stop stream: stream not started") + self._stream_ts = str(response["ts"]) + self._state = "in_progress" + response = await self._client.chat_stopStream( + token=self._token, + channel=self._stream_args["channel"], + ts=self._stream_ts, + blocks=blocks, + markdown_text=self._buffer, + metadata=metadata, + **kwargs, + ) + self._state = "completed" + return response + + async def _flush_buffer(self, **kwargs) -> AsyncSlackResponse: + """Flush the internal buffer by making appropriate API calls.""" + if not self._stream_ts: + response = await self._client.chat_startStream( + **self._stream_args, + token=self._token, + **kwargs, + markdown_text=self._buffer, + ) + self._stream_ts = response.get("ts") + self._state = "in_progress" + else: + response = await self._client.chat_appendStream( + token=self._token, + channel=self._stream_args["channel"], + ts=self._stream_ts, + **kwargs, + markdown_text=self._buffer, + ) + self._buffer = "" + return response diff --git a/slack_sdk/web/async_client.py b/slack_sdk/web/async_client.py new file mode 100644 index 000000000..ca163da98 --- /dev/null +++ b/slack_sdk/web/async_client.py @@ -0,0 +1,5957 @@ +# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +# +# *** DO NOT EDIT THIS FILE *** +# +# 1) Modify slack_sdk/web/client.py +# 2) Run `python scripts/codegen.py` +# 3) Run `black slack_sdk/` +# +# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +"""A Python module for interacting with Slack's Web API.""" + +import json +import os +import warnings +from io import IOBase +from typing import Any, Dict, List, Optional, Sequence, Union + +import slack_sdk.errors as e +from slack_sdk.models.views import View +from slack_sdk.web.async_chat_stream import AsyncChatStream + +from ..models.attachments import Attachment +from ..models.blocks import Block, RichTextBlock +from ..models.metadata import Metadata, EntityMetadata, EventAndEntityMetadata +from .async_base_client import AsyncBaseClient, AsyncSlackResponse +from .internal_utils import ( + _parse_web_class_objects, + _print_files_upload_v2_suggestion, + _remove_none_values, + _to_v2_file_upload_item, + _update_call_participants, + _validate_for_legacy_client, + _warn_if_message_text_content_is_missing, +) + + +class AsyncWebClient(AsyncBaseClient): + """A WebClient allows apps to communicate with the Slack Platform's Web API. + + https://docs.slack.dev/reference/methods + + The Slack Web API is an interface for querying information from + and enacting change in a Slack workspace. + + This client handles constructing and sending HTTP requests to Slack + as well as parsing any responses received into a `SlackResponse`. + + Attributes: + token (str): A string specifying an `xoxp-*` or `xoxb-*` token. + base_url (str): A string representing the Slack API base URL. + Default is `'https://slack.com/api/'` + timeout (int): The maximum number of seconds the client will wait + to connect and receive a response from Slack. + Default is 30 seconds. + ssl (SSLContext): An [`ssl.SSLContext`][1] instance, helpful for specifying + your own custom certificate chain. + proxy (str): String representing a fully-qualified URL to a proxy through + which to route all requests to the Slack API. Even if this parameter + is not specified, if any of the following environment variables are + present, they will be loaded into this parameter: `HTTPS_PROXY`, + `https_proxy`, `HTTP_PROXY` or `http_proxy`. + headers (dict): Additional request headers to attach to all requests. + + Methods: + `api_call`: Constructs a request and executes the API call to Slack. + + Example of recommended usage: + ```python + import os + from slack_sdk.web.async_client import AsyncWebClient + + client = AsyncWebClient(token=os.environ['SLACK_API_TOKEN']) + response = client.chat_postMessage( + channel='#random', + text="Hello world!") + assert response["ok"] + assert response["message"]["text"] == "Hello world!" + ``` + + Example manually creating an API request: + ```python + import os + from slack_sdk.web.async_client import AsyncWebClient + + client = AsyncWebClient(token=os.environ['SLACK_API_TOKEN']) + response = client.api_call( + api_method='chat.postMessage', + json={'channel': '#random','text': "Hello world!"} + ) + assert response["ok"] + assert response["message"]["text"] == "Hello world!" + ``` + + Note: + Any attributes or methods prefixed with _underscores are + intended to be "private" internal use only. They may be changed or + removed at anytime. + + [1]: https://docs.python.org/3/library/ssl.html#ssl.SSLContext + """ + + async def admin_analytics_getFile( + self, + *, + type: str, + date: Optional[str] = None, + metadata_only: Optional[bool] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Retrieve analytics data for a given date, presented as a compressed JSON file + https://docs.slack.dev/reference/methods/admin.analytics.getFile + """ + kwargs.update({"type": type}) + if date is not None: + kwargs.update({"date": date}) + if metadata_only is not None: + kwargs.update({"metadata_only": metadata_only}) + return await self.api_call("admin.analytics.getFile", params=kwargs) + + async def admin_apps_approve( + self, + *, + app_id: Optional[str] = None, + request_id: Optional[str] = None, + enterprise_id: Optional[str] = None, + team_id: Optional[str] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Approve an app for installation on a workspace. + Either app_id or request_id is required. + These IDs can be obtained either directly via the app_requested event, + or by the admin.apps.requests.list method. + https://docs.slack.dev/reference/methods/admin.apps.approve + """ + if app_id: + kwargs.update({"app_id": app_id}) + elif request_id: + kwargs.update({"request_id": request_id}) + else: + raise e.SlackRequestError("The app_id or request_id argument must be specified.") + + kwargs.update( + { + "enterprise_id": enterprise_id, + "team_id": team_id, + } + ) + return await self.api_call("admin.apps.approve", params=kwargs) + + async def admin_apps_approved_list( + self, + *, + cursor: Optional[str] = None, + limit: Optional[int] = None, + enterprise_id: Optional[str] = None, + team_id: Optional[str] = None, + **kwargs, + ) -> AsyncSlackResponse: + """List approved apps for an org or workspace. + https://docs.slack.dev/reference/methods/admin.apps.approved.list + """ + kwargs.update( + { + "cursor": cursor, + "limit": limit, + "enterprise_id": enterprise_id, + "team_id": team_id, + } + ) + return await self.api_call("admin.apps.approved.list", http_verb="GET", params=kwargs) + + async def admin_apps_clearResolution( + self, + *, + app_id: str, + enterprise_id: Optional[str] = None, + team_id: Optional[str] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Clear an app resolution + https://docs.slack.dev/reference/methods/admin.apps.clearResolution + """ + kwargs.update( + { + "app_id": app_id, + "enterprise_id": enterprise_id, + "team_id": team_id, + } + ) + return await self.api_call("admin.apps.clearResolution", http_verb="POST", params=kwargs) + + async def admin_apps_requests_cancel( + self, + *, + request_id: str, + enterprise_id: Optional[str] = None, + team_id: Optional[str] = None, + **kwargs, + ) -> AsyncSlackResponse: + """List app requests for a team/workspace. + https://docs.slack.dev/reference/methods/admin.apps.requests.cancel + """ + kwargs.update( + { + "request_id": request_id, + "enterprise_id": enterprise_id, + "team_id": team_id, + } + ) + return await self.api_call("admin.apps.requests.cancel", http_verb="POST", params=kwargs) + + async def admin_apps_requests_list( + self, + *, + cursor: Optional[str] = None, + limit: Optional[int] = None, + team_id: Optional[str] = None, + **kwargs, + ) -> AsyncSlackResponse: + """List app requests for a team/workspace. + https://docs.slack.dev/reference/methods/admin.apps.requests.list + """ + kwargs.update( + { + "cursor": cursor, + "limit": limit, + "team_id": team_id, + } + ) + return await self.api_call("admin.apps.requests.list", http_verb="GET", params=kwargs) + + async def admin_apps_restrict( + self, + *, + app_id: Optional[str] = None, + request_id: Optional[str] = None, + enterprise_id: Optional[str] = None, + team_id: Optional[str] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Restrict an app for installation on a workspace. + Exactly one of the team_id or enterprise_id arguments is required, not both. + Either app_id or request_id is required. These IDs can be obtained either directly + via the app_requested event, or by the admin.apps.requests.list method. + https://docs.slack.dev/reference/methods/admin.apps.restrict + """ + if app_id: + kwargs.update({"app_id": app_id}) + elif request_id: + kwargs.update({"request_id": request_id}) + else: + raise e.SlackRequestError("The app_id or request_id argument must be specified.") + + kwargs.update( + { + "enterprise_id": enterprise_id, + "team_id": team_id, + } + ) + return await self.api_call("admin.apps.restrict", params=kwargs) + + async def admin_apps_restricted_list( + self, + *, + cursor: Optional[str] = None, + limit: Optional[int] = None, + enterprise_id: Optional[str] = None, + team_id: Optional[str] = None, + **kwargs, + ) -> AsyncSlackResponse: + """List restricted apps for an org or workspace. + https://docs.slack.dev/reference/methods/admin.apps.restricted.list + """ + kwargs.update( + { + "cursor": cursor, + "limit": limit, + "enterprise_id": enterprise_id, + "team_id": team_id, + } + ) + return await self.api_call("admin.apps.restricted.list", http_verb="GET", params=kwargs) + + async def admin_apps_uninstall( + self, + *, + app_id: str, + enterprise_id: Optional[str] = None, + team_ids: Optional[Union[str, Sequence[str]]] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Uninstall an app from one or many workspaces, or an entire enterprise organization. + With an org-level token, enterprise_id or team_ids is required. + https://docs.slack.dev/reference/methods/admin.apps.uninstall + """ + kwargs.update({"app_id": app_id}) + if enterprise_id is not None: + kwargs.update({"enterprise_id": enterprise_id}) + if team_ids is not None: + if isinstance(team_ids, (list, tuple)): + kwargs.update({"team_ids": ",".join(team_ids)}) + else: + kwargs.update({"team_ids": team_ids}) + return await self.api_call("admin.apps.uninstall", http_verb="POST", params=kwargs) + + async def admin_apps_activities_list( + self, + *, + app_id: Optional[str] = None, + component_id: Optional[str] = None, + component_type: Optional[str] = None, + log_event_type: Optional[str] = None, + max_date_created: Optional[int] = None, + min_date_created: Optional[int] = None, + min_log_level: Optional[str] = None, + sort_direction: Optional[str] = None, + source: Optional[str] = None, + team_id: Optional[str] = None, + trace_id: Optional[str] = None, + cursor: Optional[str] = None, + limit: Optional[int] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Get logs for a specified team/org + https://docs.slack.dev/reference/methods/admin.apps.activities.list + """ + kwargs.update( + { + "app_id": app_id, + "component_id": component_id, + "component_type": component_type, + "log_event_type": log_event_type, + "max_date_created": max_date_created, + "min_date_created": min_date_created, + "min_log_level": min_log_level, + "sort_direction": sort_direction, + "source": source, + "team_id": team_id, + "trace_id": trace_id, + "cursor": cursor, + "limit": limit, + } + ) + return await self.api_call("admin.apps.activities.list", params=kwargs) + + async def admin_apps_config_lookup( + self, + *, + app_ids: Union[str, Sequence[str]], + **kwargs, + ) -> AsyncSlackResponse: + """Look up the app config for connectors by their IDs + https://docs.slack.dev/reference/methods/admin.apps.config.lookup + """ + if isinstance(app_ids, (list, tuple)): + kwargs.update({"app_ids": ",".join(app_ids)}) + else: + kwargs.update({"app_ids": app_ids}) + return await self.api_call("admin.apps.config.lookup", params=kwargs) + + async def admin_apps_config_set( + self, + *, + app_id: str, + domain_restrictions: Optional[Dict[str, Any]] = None, + workflow_auth_strategy: Optional[str] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Set the app config for a connector + https://docs.slack.dev/reference/methods/admin.apps.config.set + """ + kwargs.update( + { + "app_id": app_id, + "workflow_auth_strategy": workflow_auth_strategy, + } + ) + if domain_restrictions is not None: + kwargs.update({"domain_restrictions": json.dumps(domain_restrictions)}) + return await self.api_call("admin.apps.config.set", params=kwargs) + + async def admin_auth_policy_getEntities( + self, + *, + policy_name: str, + cursor: Optional[str] = None, + entity_type: Optional[str] = None, + limit: Optional[int] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Fetch all the entities assigned to a particular authentication policy by name. + https://docs.slack.dev/reference/methods/admin.auth.policy.getEntities + """ + kwargs.update({"policy_name": policy_name}) + if cursor is not None: + kwargs.update({"cursor": cursor}) + if entity_type is not None: + kwargs.update({"entity_type": entity_type}) + if limit is not None: + kwargs.update({"limit": limit}) + return await self.api_call("admin.auth.policy.getEntities", http_verb="POST", params=kwargs) + + async def admin_auth_policy_assignEntities( + self, + *, + entity_ids: Union[str, Sequence[str]], + policy_name: str, + entity_type: str, + **kwargs, + ) -> AsyncSlackResponse: + """Assign entities to a particular authentication policy. + https://docs.slack.dev/reference/methods/admin.auth.policy.assignEntities + """ + if isinstance(entity_ids, (list, tuple)): + kwargs.update({"entity_ids": ",".join(entity_ids)}) + else: + kwargs.update({"entity_ids": entity_ids}) + kwargs.update({"policy_name": policy_name}) + kwargs.update({"entity_type": entity_type}) + return await self.api_call("admin.auth.policy.assignEntities", http_verb="POST", params=kwargs) + + async def admin_auth_policy_removeEntities( + self, + *, + entity_ids: Union[str, Sequence[str]], + policy_name: str, + entity_type: str, + **kwargs, + ) -> AsyncSlackResponse: + """Remove specified entities from a specified authentication policy. + https://docs.slack.dev/reference/methods/admin.auth.policy.removeEntities + """ + if isinstance(entity_ids, (list, tuple)): + kwargs.update({"entity_ids": ",".join(entity_ids)}) + else: + kwargs.update({"entity_ids": entity_ids}) + kwargs.update({"policy_name": policy_name}) + kwargs.update({"entity_type": entity_type}) + return await self.api_call("admin.auth.policy.removeEntities", http_verb="POST", params=kwargs) + + async def admin_conversations_createForObjects( + self, + *, + object_id: str, + salesforce_org_id: str, + invite_object_team: Optional[bool] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Create a Salesforce channel for the corresponding object provided. + https://docs.slack.dev/reference/methods/admin.conversations.createForObjects + """ + kwargs.update( + {"object_id": object_id, "salesforce_org_id": salesforce_org_id, "invite_object_team": invite_object_team} + ) + return await self.api_call("admin.conversations.createForObjects", params=kwargs) + + async def admin_conversations_linkObjects( + self, + *, + channel: str, + record_id: str, + salesforce_org_id: str, + **kwargs, + ) -> AsyncSlackResponse: + """Link a Salesforce record to a channel. + https://docs.slack.dev/reference/methods/admin.conversations.linkObjects + """ + kwargs.update( + { + "channel": channel, + "record_id": record_id, + "salesforce_org_id": salesforce_org_id, + } + ) + return await self.api_call("admin.conversations.linkObjects", params=kwargs) + + async def admin_conversations_unlinkObjects( + self, + *, + channel: str, + new_name: str, + **kwargs, + ) -> AsyncSlackResponse: + """Unlink a Salesforce record from a channel. + https://docs.slack.dev/reference/methods/admin.conversations.unlinkObjects + """ + kwargs.update( + { + "channel": channel, + "new_name": new_name, + } + ) + return await self.api_call("admin.conversations.unlinkObjects", params=kwargs) + + async def admin_barriers_create( + self, + *, + barriered_from_usergroup_ids: Union[str, Sequence[str]], + primary_usergroup_id: str, + restricted_subjects: Union[str, Sequence[str]], + **kwargs, + ) -> AsyncSlackResponse: + """Create an Information Barrier + https://docs.slack.dev/reference/methods/admin.barriers.create + """ + kwargs.update({"primary_usergroup_id": primary_usergroup_id}) + if isinstance(barriered_from_usergroup_ids, (list, tuple)): + kwargs.update({"barriered_from_usergroup_ids": ",".join(barriered_from_usergroup_ids)}) + else: + kwargs.update({"barriered_from_usergroup_ids": barriered_from_usergroup_ids}) + if isinstance(restricted_subjects, (list, tuple)): + kwargs.update({"restricted_subjects": ",".join(restricted_subjects)}) + else: + kwargs.update({"restricted_subjects": restricted_subjects}) + return await self.api_call("admin.barriers.create", http_verb="POST", params=kwargs) + + async def admin_barriers_delete( + self, + *, + barrier_id: str, + **kwargs, + ) -> AsyncSlackResponse: + """Delete an existing Information Barrier + https://docs.slack.dev/reference/methods/admin.barriers.delete + """ + kwargs.update({"barrier_id": barrier_id}) + return await self.api_call("admin.barriers.delete", http_verb="POST", params=kwargs) + + async def admin_barriers_update( + self, + *, + barrier_id: str, + barriered_from_usergroup_ids: Union[str, Sequence[str]], + primary_usergroup_id: str, + restricted_subjects: Union[str, Sequence[str]], + **kwargs, + ) -> AsyncSlackResponse: + """Update an existing Information Barrier + https://docs.slack.dev/reference/methods/admin.barriers.update + """ + kwargs.update({"barrier_id": barrier_id, "primary_usergroup_id": primary_usergroup_id}) + if isinstance(barriered_from_usergroup_ids, (list, tuple)): + kwargs.update({"barriered_from_usergroup_ids": ",".join(barriered_from_usergroup_ids)}) + else: + kwargs.update({"barriered_from_usergroup_ids": barriered_from_usergroup_ids}) + if isinstance(restricted_subjects, (list, tuple)): + kwargs.update({"restricted_subjects": ",".join(restricted_subjects)}) + else: + kwargs.update({"restricted_subjects": restricted_subjects}) + return await self.api_call("admin.barriers.update", http_verb="POST", params=kwargs) + + async def admin_barriers_list( + self, + *, + cursor: Optional[str] = None, + limit: Optional[int] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Get all Information Barriers for your organization + https://docs.slack.dev/reference/methods/admin.barriers.list""" + kwargs.update( + { + "cursor": cursor, + "limit": limit, + } + ) + return await self.api_call("admin.barriers.list", http_verb="GET", params=kwargs) + + async def admin_conversations_create( + self, + *, + is_private: bool, + name: str, + description: Optional[str] = None, + org_wide: Optional[bool] = None, + team_id: Optional[str] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Create a public or private channel-based conversation. + https://docs.slack.dev/reference/methods/admin.conversations.create + """ + kwargs.update( + { + "is_private": is_private, + "name": name, + "description": description, + "org_wide": org_wide, + "team_id": team_id, + } + ) + return await self.api_call("admin.conversations.create", params=kwargs) + + async def admin_conversations_delete( + self, + *, + channel_id: str, + **kwargs, + ) -> AsyncSlackResponse: + """Delete a public or private channel. + https://docs.slack.dev/reference/methods/admin.conversations.delete + """ + kwargs.update({"channel_id": channel_id}) + return await self.api_call("admin.conversations.delete", params=kwargs) + + async def admin_conversations_invite( + self, + *, + channel_id: str, + user_ids: Union[str, Sequence[str]], + **kwargs, + ) -> AsyncSlackResponse: + """Invite a user to a public or private channel. + https://docs.slack.dev/reference/methods/admin.conversations.invite + """ + kwargs.update({"channel_id": channel_id}) + if isinstance(user_ids, (list, tuple)): + kwargs.update({"user_ids": ",".join(user_ids)}) + else: + kwargs.update({"user_ids": user_ids}) + # NOTE: the endpoint is unable to handle Content-Type: application/json as of Sep 3, 2020. + return await self.api_call("admin.conversations.invite", params=kwargs) + + async def admin_conversations_archive( + self, + *, + channel_id: str, + **kwargs, + ) -> AsyncSlackResponse: + """Archive a public or private channel. + https://docs.slack.dev/reference/methods/admin.conversations.archive + """ + kwargs.update({"channel_id": channel_id}) + return await self.api_call("admin.conversations.archive", params=kwargs) + + async def admin_conversations_unarchive( + self, + *, + channel_id: str, + **kwargs, + ) -> AsyncSlackResponse: + """Unarchive a public or private channel. + https://docs.slack.dev/reference/methods/admin.conversations.archive + """ + kwargs.update({"channel_id": channel_id}) + return await self.api_call("admin.conversations.unarchive", params=kwargs) + + async def admin_conversations_rename( + self, + *, + channel_id: str, + name: str, + **kwargs, + ) -> AsyncSlackResponse: + """Rename a public or private channel. + https://docs.slack.dev/reference/methods/admin.conversations.rename + """ + kwargs.update({"channel_id": channel_id, "name": name}) + return await self.api_call("admin.conversations.rename", params=kwargs) + + async def admin_conversations_search( + self, + *, + cursor: Optional[str] = None, + limit: Optional[int] = None, + query: Optional[str] = None, + search_channel_types: Optional[Union[str, Sequence[str]]] = None, + sort: Optional[str] = None, + sort_dir: Optional[str] = None, + team_ids: Optional[Union[str, Sequence[str]]] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Search for public or private channels in an Enterprise organization. + https://docs.slack.dev/reference/methods/admin.conversations.search + """ + kwargs.update( + { + "cursor": cursor, + "limit": limit, + "query": query, + "sort": sort, + "sort_dir": sort_dir, + } + ) + + if isinstance(search_channel_types, (list, tuple)): + kwargs.update({"search_channel_types": ",".join(search_channel_types)}) + else: + kwargs.update({"search_channel_types": search_channel_types}) + + if isinstance(team_ids, (list, tuple)): + kwargs.update({"team_ids": ",".join(team_ids)}) + else: + kwargs.update({"team_ids": team_ids}) + + return await self.api_call("admin.conversations.search", params=kwargs) + + async def admin_conversations_convertToPrivate( + self, + *, + channel_id: str, + **kwargs, + ) -> AsyncSlackResponse: + """Convert a public channel to a private channel. + https://docs.slack.dev/reference/methods/admin.conversations.convertToPrivate + """ + kwargs.update({"channel_id": channel_id}) + return await self.api_call("admin.conversations.convertToPrivate", params=kwargs) + + async def admin_conversations_convertToPublic( + self, + *, + channel_id: str, + **kwargs, + ) -> AsyncSlackResponse: + """Convert a privte channel to a public channel. + https://docs.slack.dev/reference/methods/admin.conversations.convertToPublic + """ + kwargs.update({"channel_id": channel_id}) + return await self.api_call("admin.conversations.convertToPublic", params=kwargs) + + async def admin_conversations_setConversationPrefs( + self, + *, + channel_id: str, + prefs: Union[str, Dict[str, str]], + **kwargs, + ) -> AsyncSlackResponse: + """Set the posting permissions for a public or private channel. + https://docs.slack.dev/reference/methods/admin.conversations.setConversationPrefs + """ + kwargs.update({"channel_id": channel_id}) + if isinstance(prefs, dict): + kwargs.update({"prefs": json.dumps(prefs)}) + else: + kwargs.update({"prefs": prefs}) + return await self.api_call("admin.conversations.setConversationPrefs", params=kwargs) + + async def admin_conversations_getConversationPrefs( + self, + *, + channel_id: str, + **kwargs, + ) -> AsyncSlackResponse: + """Get conversation preferences for a public or private channel. + https://docs.slack.dev/reference/methods/admin.conversations.getConversationPrefs + """ + kwargs.update({"channel_id": channel_id}) + return await self.api_call("admin.conversations.getConversationPrefs", params=kwargs) + + async def admin_conversations_disconnectShared( + self, + *, + channel_id: str, + leaving_team_ids: Optional[Union[str, Sequence[str]]] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Disconnect a connected channel from one or more workspaces. + https://docs.slack.dev/reference/methods/admin.conversations.disconnectShared + """ + kwargs.update({"channel_id": channel_id}) + if isinstance(leaving_team_ids, (list, tuple)): + kwargs.update({"leaving_team_ids": ",".join(leaving_team_ids)}) + else: + kwargs.update({"leaving_team_ids": leaving_team_ids}) + return await self.api_call("admin.conversations.disconnectShared", params=kwargs) + + async def admin_conversations_lookup( + self, + *, + last_message_activity_before: int, + team_ids: Union[str, Sequence[str]], + cursor: Optional[str] = None, + limit: Optional[int] = None, + max_member_count: Optional[int] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Returns channels on the given team using the filters. + https://docs.slack.dev/reference/methods/admin.conversations.lookup + """ + kwargs.update( + { + "last_message_activity_before": last_message_activity_before, + "cursor": cursor, + "limit": limit, + "max_member_count": max_member_count, + } + ) + if isinstance(team_ids, (list, tuple)): + kwargs.update({"team_ids": ",".join(team_ids)}) + else: + kwargs.update({"team_ids": team_ids}) + return await self.api_call("admin.conversations.lookup", params=kwargs) + + async def admin_conversations_ekm_listOriginalConnectedChannelInfo( + self, + *, + channel_ids: Optional[Union[str, Sequence[str]]] = None, + cursor: Optional[str] = None, + limit: Optional[int] = None, + team_ids: Optional[Union[str, Sequence[str]]] = None, + **kwargs, + ) -> AsyncSlackResponse: + """List all disconnected channels—i.e., + channels that were once connected to other workspaces and then disconnected—and + the corresponding original channel IDs for key revocation with EKM. + https://docs.slack.dev/reference/methods/admin.conversations.ekm.listOriginalConnectedChannelInfo + """ + kwargs.update( + { + "cursor": cursor, + "limit": limit, + } + ) + if isinstance(channel_ids, (list, tuple)): + kwargs.update({"channel_ids": ",".join(channel_ids)}) + else: + kwargs.update({"channel_ids": channel_ids}) + if isinstance(team_ids, (list, tuple)): + kwargs.update({"team_ids": ",".join(team_ids)}) + else: + kwargs.update({"team_ids": team_ids}) + return await self.api_call("admin.conversations.ekm.listOriginalConnectedChannelInfo", params=kwargs) + + async def admin_conversations_restrictAccess_addGroup( + self, + *, + channel_id: str, + group_id: str, + team_id: Optional[str] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Add an allowlist of IDP groups for accessing a channel. + https://docs.slack.dev/reference/methods/admin.conversations.restrictAccess.addGroup + """ + kwargs.update( + { + "channel_id": channel_id, + "group_id": group_id, + "team_id": team_id, + } + ) + return await self.api_call( + "admin.conversations.restrictAccess.addGroup", + http_verb="GET", + params=kwargs, + ) + + async def admin_conversations_restrictAccess_listGroups( + self, + *, + channel_id: str, + team_id: Optional[str] = None, + **kwargs, + ) -> AsyncSlackResponse: + """List all IDP Groups linked to a channel. + https://docs.slack.dev/reference/methods/admin.conversations.restrictAccess.listGroups + """ + kwargs.update( + { + "channel_id": channel_id, + "team_id": team_id, + } + ) + return await self.api_call( + "admin.conversations.restrictAccess.listGroups", + http_verb="GET", + params=kwargs, + ) + + async def admin_conversations_restrictAccess_removeGroup( + self, + *, + channel_id: str, + group_id: str, + team_id: str, + **kwargs, + ) -> AsyncSlackResponse: + """Remove a linked IDP group linked from a private channel. + https://docs.slack.dev/reference/methods/admin.conversations.restrictAccess.removeGroup + """ + kwargs.update( + { + "channel_id": channel_id, + "group_id": group_id, + "team_id": team_id, + } + ) + return await self.api_call( + "admin.conversations.restrictAccess.removeGroup", + http_verb="GET", + params=kwargs, + ) + + async def admin_conversations_setTeams( + self, + *, + channel_id: str, + org_channel: Optional[bool] = None, + target_team_ids: Optional[Union[str, Sequence[str]]] = None, + team_id: Optional[str] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Set the workspaces in an Enterprise grid org that connect to a public or private channel. + https://docs.slack.dev/reference/methods/admin.conversations.setTeams + """ + kwargs.update( + { + "channel_id": channel_id, + "org_channel": org_channel, + "team_id": team_id, + } + ) + if isinstance(target_team_ids, (list, tuple)): + kwargs.update({"target_team_ids": ",".join(target_team_ids)}) + else: + kwargs.update({"target_team_ids": target_team_ids}) + return await self.api_call("admin.conversations.setTeams", params=kwargs) + + async def admin_conversations_getTeams( + self, + *, + channel_id: str, + cursor: Optional[str] = None, + limit: Optional[int] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Set the workspaces in an Enterprise grid org that connect to a channel. + https://docs.slack.dev/reference/methods/admin.conversations.getTeams + """ + kwargs.update( + { + "channel_id": channel_id, + "cursor": cursor, + "limit": limit, + } + ) + return await self.api_call("admin.conversations.getTeams", params=kwargs) + + async def admin_conversations_getCustomRetention( + self, + *, + channel_id: str, + **kwargs, + ) -> AsyncSlackResponse: + """Get a channel's retention policy + https://docs.slack.dev/reference/methods/admin.conversations.getCustomRetention + """ + kwargs.update({"channel_id": channel_id}) + return await self.api_call("admin.conversations.getCustomRetention", params=kwargs) + + async def admin_conversations_removeCustomRetention( + self, + *, + channel_id: str, + **kwargs, + ) -> AsyncSlackResponse: + """Remove a channel's retention policy + https://docs.slack.dev/reference/methods/admin.conversations.removeCustomRetention + """ + kwargs.update({"channel_id": channel_id}) + return await self.api_call("admin.conversations.removeCustomRetention", params=kwargs) + + async def admin_conversations_setCustomRetention( + self, + *, + channel_id: str, + duration_days: int, + **kwargs, + ) -> AsyncSlackResponse: + """Set a channel's retention policy + https://docs.slack.dev/reference/methods/admin.conversations.setCustomRetention + """ + kwargs.update({"channel_id": channel_id, "duration_days": duration_days}) + return await self.api_call("admin.conversations.setCustomRetention", params=kwargs) + + async def admin_conversations_bulkArchive( + self, + *, + channel_ids: Union[Sequence[str], str], + **kwargs, + ) -> AsyncSlackResponse: + """Archive public or private channels in bulk. + https://docs.slack.dev/reference/methods/admin.conversations.bulkArchive + """ + kwargs.update({"channel_ids": ",".join(channel_ids) if isinstance(channel_ids, (list, tuple)) else channel_ids}) + return await self.api_call("admin.conversations.bulkArchive", params=kwargs) + + async def admin_conversations_bulkDelete( + self, + *, + channel_ids: Union[Sequence[str], str], + **kwargs, + ) -> AsyncSlackResponse: + """Delete public or private channels in bulk. + https://slack.com/api/admin.conversations.bulkDelete + """ + kwargs.update({"channel_ids": ",".join(channel_ids) if isinstance(channel_ids, (list, tuple)) else channel_ids}) + return await self.api_call("admin.conversations.bulkDelete", params=kwargs) + + async def admin_conversations_bulkMove( + self, + *, + channel_ids: Union[Sequence[str], str], + target_team_id: str, + **kwargs, + ) -> AsyncSlackResponse: + """Move public or private channels in bulk. + https://docs.slack.dev/reference/methods/admin.conversations.bulkMove + """ + kwargs.update( + { + "target_team_id": target_team_id, + "channel_ids": ",".join(channel_ids) if isinstance(channel_ids, (list, tuple)) else channel_ids, + } + ) + return await self.api_call("admin.conversations.bulkMove", params=kwargs) + + async def admin_emoji_add( + self, + *, + name: str, + url: str, + **kwargs, + ) -> AsyncSlackResponse: + """Add an emoji. + https://docs.slack.dev/reference/methods/admin.emoji.add + """ + kwargs.update({"name": name, "url": url}) + return await self.api_call("admin.emoji.add", http_verb="GET", params=kwargs) + + async def admin_emoji_addAlias( + self, + *, + alias_for: str, + name: str, + **kwargs, + ) -> AsyncSlackResponse: + """Add an emoji alias. + https://docs.slack.dev/reference/methods/admin.emoji.addAlias + """ + kwargs.update({"alias_for": alias_for, "name": name}) + return await self.api_call("admin.emoji.addAlias", http_verb="GET", params=kwargs) + + async def admin_emoji_list( + self, + *, + cursor: Optional[str] = None, + limit: Optional[int] = None, + **kwargs, + ) -> AsyncSlackResponse: + """List emoji for an Enterprise Grid organization. + https://docs.slack.dev/reference/methods/admin.emoji.list + """ + kwargs.update({"cursor": cursor, "limit": limit}) + return await self.api_call("admin.emoji.list", http_verb="GET", params=kwargs) + + async def admin_emoji_remove( + self, + *, + name: str, + **kwargs, + ) -> AsyncSlackResponse: + """Remove an emoji across an Enterprise Grid organization. + https://docs.slack.dev/reference/methods/admin.emoji.remove + """ + kwargs.update({"name": name}) + return await self.api_call("admin.emoji.remove", http_verb="GET", params=kwargs) + + async def admin_emoji_rename( + self, + *, + name: str, + new_name: str, + **kwargs, + ) -> AsyncSlackResponse: + """Rename an emoji. + https://docs.slack.dev/reference/methods/admin.emoji.rename + """ + kwargs.update({"name": name, "new_name": new_name}) + return await self.api_call("admin.emoji.rename", http_verb="GET", params=kwargs) + + async def admin_functions_list( + self, + *, + app_ids: Union[str, Sequence[str]], + team_id: Optional[str] = None, + cursor: Optional[str] = None, + limit: Optional[int] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Look up functions by a set of apps + https://docs.slack.dev/reference/methods/admin.functions.list + """ + if isinstance(app_ids, (list, tuple)): + kwargs.update({"app_ids": ",".join(app_ids)}) + else: + kwargs.update({"app_ids": app_ids}) + kwargs.update( + { + "team_id": team_id, + "cursor": cursor, + "limit": limit, + } + ) + return await self.api_call("admin.functions.list", params=kwargs) + + async def admin_functions_permissions_lookup( + self, + *, + function_ids: Union[str, Sequence[str]], + **kwargs, + ) -> AsyncSlackResponse: + """Lookup the visibility of multiple Slack functions + and include the users if it is limited to particular named entities. + https://docs.slack.dev/reference/methods/admin.functions.permissions.lookup + """ + if isinstance(function_ids, (list, tuple)): + kwargs.update({"function_ids": ",".join(function_ids)}) + else: + kwargs.update({"function_ids": function_ids}) + return await self.api_call("admin.functions.permissions.lookup", params=kwargs) + + async def admin_functions_permissions_set( + self, + *, + function_id: str, + visibility: str, + user_ids: Optional[Union[str, Sequence[str]]] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Set the visibility of a Slack function + and define the users or workspaces if it is set to named_entities + https://docs.slack.dev/reference/methods/admin.functions.permissions.set + """ + kwargs.update( + { + "function_id": function_id, + "visibility": visibility, + } + ) + if user_ids is not None: + if isinstance(user_ids, (list, tuple)): + kwargs.update({"user_ids": ",".join(user_ids)}) + else: + kwargs.update({"user_ids": user_ids}) + return await self.api_call("admin.functions.permissions.set", params=kwargs) + + async def admin_roles_addAssignments( + self, + *, + role_id: str, + entity_ids: Union[str, Sequence[str]], + user_ids: Union[str, Sequence[str]], + **kwargs, + ) -> AsyncSlackResponse: + """Adds members to the specified role with the specified scopes + https://docs.slack.dev/reference/methods/admin.roles.addAssignments + """ + kwargs.update({"role_id": role_id}) + if isinstance(entity_ids, (list, tuple)): + kwargs.update({"entity_ids": ",".join(entity_ids)}) + else: + kwargs.update({"entity_ids": entity_ids}) + if isinstance(user_ids, (list, tuple)): + kwargs.update({"user_ids": ",".join(user_ids)}) + else: + kwargs.update({"user_ids": user_ids}) + return await self.api_call("admin.roles.addAssignments", params=kwargs) + + async def admin_roles_listAssignments( + self, + *, + role_ids: Optional[Union[str, Sequence[str]]] = None, + entity_ids: Optional[Union[str, Sequence[str]]] = None, + cursor: Optional[str] = None, + limit: Optional[Union[str, int]] = None, + sort_dir: Optional[str] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Lists assignments for all roles across entities. + Options to scope results by any combination of roles or entities + https://docs.slack.dev/reference/methods/admin.roles.listAssignments + """ + kwargs.update({"cursor": cursor, "limit": limit, "sort_dir": sort_dir}) + if isinstance(entity_ids, (list, tuple)): + kwargs.update({"entity_ids": ",".join(entity_ids)}) + else: + kwargs.update({"entity_ids": entity_ids}) + if isinstance(role_ids, (list, tuple)): + kwargs.update({"role_ids": ",".join(role_ids)}) + else: + kwargs.update({"role_ids": role_ids}) + return await self.api_call("admin.roles.listAssignments", params=kwargs) + + async def admin_roles_removeAssignments( + self, + *, + role_id: str, + entity_ids: Union[str, Sequence[str]], + user_ids: Union[str, Sequence[str]], + **kwargs, + ) -> AsyncSlackResponse: + """Removes a set of users from a role for the given scopes and entities + https://docs.slack.dev/reference/methods/admin.roles.removeAssignments + """ + kwargs.update({"role_id": role_id}) + if isinstance(entity_ids, (list, tuple)): + kwargs.update({"entity_ids": ",".join(entity_ids)}) + else: + kwargs.update({"entity_ids": entity_ids}) + if isinstance(user_ids, (list, tuple)): + kwargs.update({"user_ids": ",".join(user_ids)}) + else: + kwargs.update({"user_ids": user_ids}) + return await self.api_call("admin.roles.removeAssignments", params=kwargs) + + async def admin_users_session_reset( + self, + *, + user_id: str, + mobile_only: Optional[bool] = None, + web_only: Optional[bool] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Wipes all valid sessions on all devices for a given user. + https://docs.slack.dev/reference/methods/admin.users.session.reset + """ + kwargs.update( + { + "user_id": user_id, + "mobile_only": mobile_only, + "web_only": web_only, + } + ) + return await self.api_call("admin.users.session.reset", params=kwargs) + + async def admin_users_session_resetBulk( + self, + *, + user_ids: Union[str, Sequence[str]], + mobile_only: Optional[bool] = None, + web_only: Optional[bool] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Enqueues an asynchronous job to wipe all valid sessions on all devices for a given list of users + https://docs.slack.dev/reference/methods/admin.users.session.resetBulk + """ + if isinstance(user_ids, (list, tuple)): + kwargs.update({"user_ids": ",".join(user_ids)}) + else: + kwargs.update({"user_ids": user_ids}) + kwargs.update( + { + "mobile_only": mobile_only, + "web_only": web_only, + } + ) + return await self.api_call("admin.users.session.resetBulk", params=kwargs) + + async def admin_users_session_invalidate( + self, + *, + session_id: str, + team_id: str, + **kwargs, + ) -> AsyncSlackResponse: + """Invalidate a single session for a user by session_id. + https://docs.slack.dev/reference/methods/admin.users.session.invalidate + """ + kwargs.update({"session_id": session_id, "team_id": team_id}) + return await self.api_call("admin.users.session.invalidate", params=kwargs) + + async def admin_users_session_list( + self, + *, + cursor: Optional[str] = None, + limit: Optional[int] = None, + team_id: Optional[str] = None, + user_id: Optional[str] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Lists all active user sessions for an organization + https://docs.slack.dev/reference/methods/admin.users.session.list + """ + kwargs.update( + { + "cursor": cursor, + "limit": limit, + "team_id": team_id, + "user_id": user_id, + } + ) + return await self.api_call("admin.users.session.list", params=kwargs) + + async def admin_teams_settings_setDefaultChannels( + self, + *, + team_id: str, + channel_ids: Union[str, Sequence[str]], + **kwargs, + ) -> AsyncSlackResponse: + """Set the default channels of a workspace. + https://docs.slack.dev/reference/methods/admin.teams.settings.setDefaultChannels + """ + kwargs.update({"team_id": team_id}) + if isinstance(channel_ids, (list, tuple)): + kwargs.update({"channel_ids": ",".join(channel_ids)}) + else: + kwargs.update({"channel_ids": channel_ids}) + return await self.api_call("admin.teams.settings.setDefaultChannels", http_verb="GET", params=kwargs) + + async def admin_users_session_getSettings( + self, + *, + user_ids: Union[str, Sequence[str]], + **kwargs, + ) -> AsyncSlackResponse: + """Get user-specific session settings—the session duration + and what happens when the client closes—given a list of users. + https://docs.slack.dev/reference/methods/admin.users.session.getSettings + """ + if isinstance(user_ids, (list, tuple)): + kwargs.update({"user_ids": ",".join(user_ids)}) + else: + kwargs.update({"user_ids": user_ids}) + return await self.api_call("admin.users.session.getSettings", params=kwargs) + + async def admin_users_session_setSettings( + self, + *, + user_ids: Union[str, Sequence[str]], + desktop_app_browser_quit: Optional[bool] = None, + duration: Optional[int] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Configure the user-level session settings—the session duration + and what happens when the client closes—for one or more users. + https://docs.slack.dev/reference/methods/admin.users.session.setSettings + """ + if isinstance(user_ids, (list, tuple)): + kwargs.update({"user_ids": ",".join(user_ids)}) + else: + kwargs.update({"user_ids": user_ids}) + kwargs.update( + { + "desktop_app_browser_quit": desktop_app_browser_quit, + "duration": duration, + } + ) + return await self.api_call("admin.users.session.setSettings", params=kwargs) + + async def admin_users_session_clearSettings( + self, + *, + user_ids: Union[str, Sequence[str]], + **kwargs, + ) -> AsyncSlackResponse: + """Clear user-specific session settings—the session duration + and what happens when the client closes—for a list of users. + https://docs.slack.dev/reference/methods/admin.users.session.clearSettings + """ + if isinstance(user_ids, (list, tuple)): + kwargs.update({"user_ids": ",".join(user_ids)}) + else: + kwargs.update({"user_ids": user_ids}) + return await self.api_call("admin.users.session.clearSettings", params=kwargs) + + async def admin_users_unsupportedVersions_export( + self, + *, + date_end_of_support: Optional[Union[str, int]] = None, + date_sessions_started: Optional[Union[str, int]] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Ask Slackbot to send you an export listing all workspace members using unsupported software, + presented as a zipped CSV file. + https://docs.slack.dev/reference/methods/admin.users.unsupportedVersions.export + """ + kwargs.update( + { + "date_end_of_support": date_end_of_support, + "date_sessions_started": date_sessions_started, + } + ) + return await self.api_call("admin.users.unsupportedVersions.export", params=kwargs) + + async def admin_inviteRequests_approve( + self, + *, + invite_request_id: str, + team_id: Optional[str] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Approve a workspace invite request. + https://docs.slack.dev/reference/methods/admin.inviteRequests.approve + """ + kwargs.update({"invite_request_id": invite_request_id, "team_id": team_id}) + return await self.api_call("admin.inviteRequests.approve", params=kwargs) + + async def admin_inviteRequests_approved_list( + self, + *, + cursor: Optional[str] = None, + limit: Optional[int] = None, + team_id: Optional[str] = None, + **kwargs, + ) -> AsyncSlackResponse: + """List all approved workspace invite requests. + https://docs.slack.dev/reference/methods/admin.inviteRequests.approved.list + """ + kwargs.update( + { + "cursor": cursor, + "limit": limit, + "team_id": team_id, + } + ) + return await self.api_call("admin.inviteRequests.approved.list", params=kwargs) + + async def admin_inviteRequests_denied_list( + self, + *, + cursor: Optional[str] = None, + limit: Optional[int] = None, + team_id: Optional[str] = None, + **kwargs, + ) -> AsyncSlackResponse: + """List all denied workspace invite requests. + https://docs.slack.dev/reference/methods/admin.inviteRequests.denied.list + """ + kwargs.update( + { + "cursor": cursor, + "limit": limit, + "team_id": team_id, + } + ) + return await self.api_call("admin.inviteRequests.denied.list", params=kwargs) + + async def admin_inviteRequests_deny( + self, + *, + invite_request_id: str, + team_id: Optional[str] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Deny a workspace invite request. + https://docs.slack.dev/reference/methods/admin.inviteRequests.deny + """ + kwargs.update({"invite_request_id": invite_request_id, "team_id": team_id}) + return await self.api_call("admin.inviteRequests.deny", params=kwargs) + + async def admin_inviteRequests_list( + self, + **kwargs, + ) -> AsyncSlackResponse: + """List all pending workspace invite requests.""" + return await self.api_call("admin.inviteRequests.list", params=kwargs) + + async def admin_teams_admins_list( + self, + *, + team_id: str, + cursor: Optional[str] = None, + limit: Optional[int] = None, + **kwargs, + ) -> AsyncSlackResponse: + """List all of the admins on a given workspace. + https://docs.slack.dev/reference/methods/admin.inviteRequests.list + """ + kwargs.update( + { + "cursor": cursor, + "limit": limit, + "team_id": team_id, + } + ) + return await self.api_call("admin.teams.admins.list", http_verb="GET", params=kwargs) + + async def admin_teams_create( + self, + *, + team_domain: str, + team_name: str, + team_description: Optional[str] = None, + team_discoverability: Optional[str] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Create an Enterprise team. + https://docs.slack.dev/reference/methods/admin.teams.create + """ + kwargs.update( + { + "team_domain": team_domain, + "team_name": team_name, + "team_description": team_description, + "team_discoverability": team_discoverability, + } + ) + return await self.api_call("admin.teams.create", params=kwargs) + + async def admin_teams_list( + self, + *, + cursor: Optional[str] = None, + limit: Optional[int] = None, + **kwargs, + ) -> AsyncSlackResponse: + """List all teams on an Enterprise organization. + https://docs.slack.dev/reference/methods/admin.teams.list + """ + kwargs.update({"cursor": cursor, "limit": limit}) + return await self.api_call("admin.teams.list", params=kwargs) + + async def admin_teams_owners_list( + self, + *, + team_id: str, + cursor: Optional[str] = None, + limit: Optional[int] = None, + **kwargs, + ) -> AsyncSlackResponse: + """List all of the admins on a given workspace. + https://docs.slack.dev/reference/methods/admin.teams.owners.list + """ + kwargs.update({"team_id": team_id, "cursor": cursor, "limit": limit}) + return await self.api_call("admin.teams.owners.list", http_verb="GET", params=kwargs) + + async def admin_teams_settings_info( + self, + *, + team_id: str, + **kwargs, + ) -> AsyncSlackResponse: + """Fetch information about settings in a workspace + https://docs.slack.dev/reference/methods/admin.teams.settings.info + """ + kwargs.update({"team_id": team_id}) + return await self.api_call("admin.teams.settings.info", params=kwargs) + + async def admin_teams_settings_setDescription( + self, + *, + team_id: str, + description: str, + **kwargs, + ) -> AsyncSlackResponse: + """Set the description of a given workspace. + https://docs.slack.dev/reference/methods/admin.teams.settings.setDescription + """ + kwargs.update({"team_id": team_id, "description": description}) + return await self.api_call("admin.teams.settings.setDescription", params=kwargs) + + async def admin_teams_settings_setDiscoverability( + self, + *, + team_id: str, + discoverability: str, + **kwargs, + ) -> AsyncSlackResponse: + """Sets the icon of a workspace. + https://docs.slack.dev/reference/methods/admin.teams.settings.setDiscoverability + """ + kwargs.update({"team_id": team_id, "discoverability": discoverability}) + return await self.api_call("admin.teams.settings.setDiscoverability", params=kwargs) + + async def admin_teams_settings_setIcon( + self, + *, + team_id: str, + image_url: str, + **kwargs, + ) -> AsyncSlackResponse: + """Sets the icon of a workspace. + https://docs.slack.dev/reference/methods/admin.teams.settings.setIcon + """ + kwargs.update({"team_id": team_id, "image_url": image_url}) + return await self.api_call("admin.teams.settings.setIcon", http_verb="GET", params=kwargs) + + async def admin_teams_settings_setName( + self, + *, + team_id: str, + name: str, + **kwargs, + ) -> AsyncSlackResponse: + """Sets the icon of a workspace. + https://docs.slack.dev/reference/methods/admin.teams.settings.setName + """ + kwargs.update({"team_id": team_id, "name": name}) + return await self.api_call("admin.teams.settings.setName", params=kwargs) + + async def admin_usergroups_addChannels( + self, + *, + channel_ids: Union[str, Sequence[str]], + usergroup_id: str, + team_id: Optional[str] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Add one or more default channels to an IDP group. + https://docs.slack.dev/reference/methods/admin.usergroups.addChannels + """ + kwargs.update({"team_id": team_id, "usergroup_id": usergroup_id}) + if isinstance(channel_ids, (list, tuple)): + kwargs.update({"channel_ids": ",".join(channel_ids)}) + else: + kwargs.update({"channel_ids": channel_ids}) + return await self.api_call("admin.usergroups.addChannels", params=kwargs) + + async def admin_usergroups_addTeams( + self, + *, + usergroup_id: str, + team_ids: Union[str, Sequence[str]], + auto_provision: Optional[bool] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Associate one or more default workspaces with an organization-wide IDP group. + https://docs.slack.dev/reference/methods/admin.usergroups.addTeams + """ + kwargs.update({"usergroup_id": usergroup_id, "auto_provision": auto_provision}) + if isinstance(team_ids, (list, tuple)): + kwargs.update({"team_ids": ",".join(team_ids)}) + else: + kwargs.update({"team_ids": team_ids}) + return await self.api_call("admin.usergroups.addTeams", params=kwargs) + + async def admin_usergroups_listChannels( + self, + *, + usergroup_id: str, + include_num_members: Optional[bool] = None, + team_id: Optional[bool] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Add one or more default channels to an IDP group. + https://docs.slack.dev/reference/methods/admin.usergroups.listChannels + """ + kwargs.update( + { + "usergroup_id": usergroup_id, + "include_num_members": include_num_members, + "team_id": team_id, + } + ) + return await self.api_call("admin.usergroups.listChannels", params=kwargs) + + async def admin_usergroups_removeChannels( + self, + *, + usergroup_id: str, + channel_ids: Union[str, Sequence[str]], + **kwargs, + ) -> AsyncSlackResponse: + """Add one or more default channels to an IDP group. + https://docs.slack.dev/reference/methods/admin.usergroups.removeChannels + """ + kwargs.update({"usergroup_id": usergroup_id}) + if isinstance(channel_ids, (list, tuple)): + kwargs.update({"channel_ids": ",".join(channel_ids)}) + else: + kwargs.update({"channel_ids": channel_ids}) + return await self.api_call("admin.usergroups.removeChannels", params=kwargs) + + async def admin_users_assign( + self, + *, + team_id: str, + user_id: str, + channel_ids: Optional[Union[str, Sequence[str]]] = None, + is_restricted: Optional[bool] = None, + is_ultra_restricted: Optional[bool] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Add an Enterprise user to a workspace. + https://docs.slack.dev/reference/methods/admin.users.assign + """ + kwargs.update( + { + "team_id": team_id, + "user_id": user_id, + "is_restricted": is_restricted, + "is_ultra_restricted": is_ultra_restricted, + } + ) + if isinstance(channel_ids, (list, tuple)): + kwargs.update({"channel_ids": ",".join(channel_ids)}) + else: + kwargs.update({"channel_ids": channel_ids}) + return await self.api_call("admin.users.assign", params=kwargs) + + async def admin_users_invite( + self, + *, + team_id: str, + email: str, + channel_ids: Union[str, Sequence[str]], + custom_message: Optional[str] = None, + email_password_policy_enabled: Optional[bool] = None, + guest_expiration_ts: Optional[Union[str, float]] = None, + is_restricted: Optional[bool] = None, + is_ultra_restricted: Optional[bool] = None, + real_name: Optional[str] = None, + resend: Optional[bool] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Invite a user to a workspace. + https://docs.slack.dev/reference/methods/admin.users.invite + """ + kwargs.update( + { + "team_id": team_id, + "email": email, + "custom_message": custom_message, + "email_password_policy_enabled": email_password_policy_enabled, + "guest_expiration_ts": str(guest_expiration_ts) if guest_expiration_ts is not None else None, + "is_restricted": is_restricted, + "is_ultra_restricted": is_ultra_restricted, + "real_name": real_name, + "resend": resend, + } + ) + if isinstance(channel_ids, (list, tuple)): + kwargs.update({"channel_ids": ",".join(channel_ids)}) + else: + kwargs.update({"channel_ids": channel_ids}) + return await self.api_call("admin.users.invite", params=kwargs) + + async def admin_users_list( + self, + *, + team_id: Optional[str] = None, + include_deactivated_user_workspaces: Optional[bool] = None, + is_active: Optional[bool] = None, + cursor: Optional[str] = None, + limit: Optional[int] = None, + **kwargs, + ) -> AsyncSlackResponse: + """List users on a workspace + https://docs.slack.dev/reference/methods/admin.users.list + """ + kwargs.update( + { + "team_id": team_id, + "include_deactivated_user_workspaces": include_deactivated_user_workspaces, + "is_active": is_active, + "cursor": cursor, + "limit": limit, + } + ) + return await self.api_call("admin.users.list", params=kwargs) + + async def admin_users_remove( + self, + *, + team_id: str, + user_id: str, + **kwargs, + ) -> AsyncSlackResponse: + """Remove a user from a workspace. + https://docs.slack.dev/reference/methods/admin.users.remove + """ + kwargs.update({"team_id": team_id, "user_id": user_id}) + return await self.api_call("admin.users.remove", params=kwargs) + + async def admin_users_setAdmin( + self, + *, + team_id: str, + user_id: str, + **kwargs, + ) -> AsyncSlackResponse: + """Set an existing guest, regular user, or owner to be an admin user. + https://docs.slack.dev/reference/methods/admin.users.setAdmin + """ + kwargs.update({"team_id": team_id, "user_id": user_id}) + return await self.api_call("admin.users.setAdmin", params=kwargs) + + async def admin_users_setExpiration( + self, + *, + expiration_ts: int, + user_id: str, + team_id: Optional[str] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Set an expiration for a guest user. + https://docs.slack.dev/reference/methods/admin.users.setExpiration + """ + kwargs.update({"expiration_ts": expiration_ts, "team_id": team_id, "user_id": user_id}) + return await self.api_call("admin.users.setExpiration", params=kwargs) + + async def admin_users_setOwner( + self, + *, + team_id: str, + user_id: str, + **kwargs, + ) -> AsyncSlackResponse: + """Set an existing guest, regular user, or admin user to be a workspace owner. + https://docs.slack.dev/reference/methods/admin.users.setOwner + """ + kwargs.update({"team_id": team_id, "user_id": user_id}) + return await self.api_call("admin.users.setOwner", params=kwargs) + + async def admin_users_setRegular( + self, + *, + team_id: str, + user_id: str, + **kwargs, + ) -> AsyncSlackResponse: + """Set an existing guest user, admin user, or owner to be a regular user. + https://docs.slack.dev/reference/methods/admin.users.setRegular + """ + kwargs.update({"team_id": team_id, "user_id": user_id}) + return await self.api_call("admin.users.setRegular", params=kwargs) + + async def admin_workflows_search( + self, + *, + app_id: Optional[str] = None, + collaborator_ids: Optional[Union[str, Sequence[str]]] = None, + cursor: Optional[str] = None, + limit: Optional[int] = None, + no_collaborators: Optional[bool] = None, + num_trigger_ids: Optional[int] = None, + query: Optional[str] = None, + sort: Optional[str] = None, + sort_dir: Optional[str] = None, + source: Optional[str] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Search workflows within the team or enterprise + https://docs.slack.dev/reference/methods/admin.workflows.search + """ + if collaborator_ids is not None: + if isinstance(collaborator_ids, (list, tuple)): + kwargs.update({"collaborator_ids": ",".join(collaborator_ids)}) + else: + kwargs.update({"collaborator_ids": collaborator_ids}) + kwargs.update( + { + "app_id": app_id, + "cursor": cursor, + "limit": limit, + "no_collaborators": no_collaborators, + "num_trigger_ids": num_trigger_ids, + "query": query, + "sort": sort, + "sort_dir": sort_dir, + "source": source, + } + ) + return await self.api_call("admin.workflows.search", params=kwargs) + + async def admin_workflows_permissions_lookup( + self, + *, + workflow_ids: Union[str, Sequence[str]], + max_workflow_triggers: Optional[int] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Look up the permissions for a set of workflows + https://docs.slack.dev/reference/methods/admin.workflows.permissions.lookup + """ + if isinstance(workflow_ids, (list, tuple)): + kwargs.update({"workflow_ids": ",".join(workflow_ids)}) + else: + kwargs.update({"workflow_ids": workflow_ids}) + kwargs.update( + { + "max_workflow_triggers": max_workflow_triggers, + } + ) + return await self.api_call("admin.workflows.permissions.lookup", params=kwargs) + + async def admin_workflows_collaborators_add( + self, + *, + collaborator_ids: Union[str, Sequence[str]], + workflow_ids: Union[str, Sequence[str]], + **kwargs, + ) -> AsyncSlackResponse: + """Add collaborators to workflows within the team or enterprise + https://docs.slack.dev/reference/methods/admin.workflows.collaborators.add + """ + if isinstance(collaborator_ids, (list, tuple)): + kwargs.update({"collaborator_ids": ",".join(collaborator_ids)}) + else: + kwargs.update({"collaborator_ids": collaborator_ids}) + if isinstance(workflow_ids, (list, tuple)): + kwargs.update({"workflow_ids": ",".join(workflow_ids)}) + else: + kwargs.update({"workflow_ids": workflow_ids}) + return await self.api_call("admin.workflows.collaborators.add", params=kwargs) + + async def admin_workflows_collaborators_remove( + self, + *, + collaborator_ids: Union[str, Sequence[str]], + workflow_ids: Union[str, Sequence[str]], + **kwargs, + ) -> AsyncSlackResponse: + """Remove collaborators from workflows within the team or enterprise + https://docs.slack.dev/reference/methods/admin.workflows.collaborators.remove + """ + if isinstance(collaborator_ids, (list, tuple)): + kwargs.update({"collaborator_ids": ",".join(collaborator_ids)}) + else: + kwargs.update({"collaborator_ids": collaborator_ids}) + if isinstance(workflow_ids, (list, tuple)): + kwargs.update({"workflow_ids": ",".join(workflow_ids)}) + else: + kwargs.update({"workflow_ids": workflow_ids}) + return await self.api_call("admin.workflows.collaborators.remove", params=kwargs) + + async def admin_workflows_unpublish( + self, + *, + workflow_ids: Union[str, Sequence[str]], + **kwargs, + ) -> AsyncSlackResponse: + """Unpublish workflows within the team or enterprise + https://docs.slack.dev/reference/methods/admin.workflows.unpublish + """ + if isinstance(workflow_ids, (list, tuple)): + kwargs.update({"workflow_ids": ",".join(workflow_ids)}) + else: + kwargs.update({"workflow_ids": workflow_ids}) + return await self.api_call("admin.workflows.unpublish", params=kwargs) + + async def api_test( + self, + *, + error: Optional[str] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Checks API calling code. + https://docs.slack.dev/reference/methods/api.test + """ + kwargs.update({"error": error}) + return await self.api_call("api.test", params=kwargs) + + async def apps_connections_open( + self, + *, + app_token: str, + **kwargs, + ) -> AsyncSlackResponse: + """Generate a temporary Socket Mode WebSocket URL that your app can connect to + in order to receive events and interactive payloads + https://docs.slack.dev/reference/methods/apps.connections.open + """ + kwargs.update({"token": app_token}) + return await self.api_call("apps.connections.open", http_verb="POST", params=kwargs) + + async def apps_event_authorizations_list( + self, + *, + event_context: str, + cursor: Optional[str] = None, + limit: Optional[int] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Get a list of authorizations for the given event context. + Each authorization represents an app installation that the event is visible to. + https://docs.slack.dev/reference/methods/apps.event.authorizations.list + """ + kwargs.update({"event_context": event_context, "cursor": cursor, "limit": limit}) + return await self.api_call("apps.event.authorizations.list", params=kwargs) + + async def apps_uninstall( + self, + *, + client_id: str, + client_secret: str, + **kwargs, + ) -> AsyncSlackResponse: + """Uninstalls your app from a workspace. + https://docs.slack.dev/reference/methods/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://docs.slack.dev/reference/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://docs.slack.dev/reference/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://docs.slack.dev/reference/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://docs.slack.dev/reference/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://docs.slack.dev/reference/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://docs.slack.dev/reference/methods/tooling.tokens.rotate + """ + kwargs.update({"refresh_token": refresh_token}) + return await self.api_call("tooling.tokens.rotate", params=kwargs) + + async def assistant_threads_setStatus( + self, + *, + channel_id: str, + thread_ts: str, + status: str, + loading_messages: Optional[List[str]] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Set the status for an AI assistant thread. + https://docs.slack.dev/reference/methods/assistant.threads.setStatus + """ + kwargs.update( + {"channel_id": channel_id, "thread_ts": thread_ts, "status": status, "loading_messages": loading_messages} + ) + kwargs = _remove_none_values(kwargs) + return await self.api_call("assistant.threads.setStatus", json=kwargs) + + async def assistant_threads_setTitle( + self, + *, + channel_id: str, + thread_ts: str, + title: str, + **kwargs, + ) -> AsyncSlackResponse: + """Set the title for the given assistant thread. + https://docs.slack.dev/reference/methods/assistant.threads.setTitle + """ + kwargs.update({"channel_id": channel_id, "thread_ts": thread_ts, "title": title}) + return await self.api_call("assistant.threads.setTitle", params=kwargs) + + async def assistant_threads_setSuggestedPrompts( + self, + *, + channel_id: str, + thread_ts: str, + title: Optional[str] = None, + prompts: List[Dict[str, str]], + **kwargs, + ) -> AsyncSlackResponse: + """Set suggested prompts for the given assistant thread. + https://docs.slack.dev/reference/methods/assistant.threads.setSuggestedPrompts + """ + kwargs.update({"channel_id": channel_id, "thread_ts": thread_ts, "prompts": prompts}) + if title is not None: + kwargs.update({"title": title}) + return await self.api_call("assistant.threads.setSuggestedPrompts", json=kwargs) + + async def auth_revoke( + self, + *, + test: Optional[bool] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Revokes a token. + https://docs.slack.dev/reference/methods/auth.revoke + """ + kwargs.update({"test": test}) + return await self.api_call("auth.revoke", http_verb="GET", params=kwargs) + + async def auth_test( + self, + **kwargs, + ) -> AsyncSlackResponse: + """Checks authentication & identity. + https://docs.slack.dev/reference/methods/auth.test + """ + return await self.api_call("auth.test", params=kwargs) + + async def auth_teams_list( + self, + cursor: Optional[str] = None, + limit: Optional[int] = None, + include_icon: Optional[bool] = None, + **kwargs, + ) -> AsyncSlackResponse: + """List the workspaces a token can access. + https://docs.slack.dev/reference/methods/auth.teams.list + """ + kwargs.update({"cursor": cursor, "limit": limit, "include_icon": include_icon}) + return await self.api_call("auth.teams.list", params=kwargs) + + async def bookmarks_add( + self, + *, + channel_id: str, + title: str, + type: str, + emoji: Optional[str] = None, + entity_id: Optional[str] = None, + link: Optional[str] = None, # include when type is 'link' + parent_id: Optional[str] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Add bookmark to a channel. + https://docs.slack.dev/reference/methods/bookmarks.add + """ + kwargs.update( + { + "channel_id": channel_id, + "title": title, + "type": type, + "emoji": emoji, + "entity_id": entity_id, + "link": link, + "parent_id": parent_id, + } + ) + return await self.api_call("bookmarks.add", http_verb="POST", params=kwargs) + + async def bookmarks_edit( + self, + *, + bookmark_id: str, + channel_id: str, + emoji: Optional[str] = None, + link: Optional[str] = None, + title: Optional[str] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Edit bookmark. + https://docs.slack.dev/reference/methods/bookmarks.edit + """ + kwargs.update( + { + "bookmark_id": bookmark_id, + "channel_id": channel_id, + "emoji": emoji, + "link": link, + "title": title, + } + ) + return await self.api_call("bookmarks.edit", http_verb="POST", params=kwargs) + + async def bookmarks_list( + self, + *, + channel_id: str, + **kwargs, + ) -> AsyncSlackResponse: + """List bookmark for the channel. + https://docs.slack.dev/reference/methods/bookmarks.list + """ + kwargs.update({"channel_id": channel_id}) + return await self.api_call("bookmarks.list", http_verb="POST", params=kwargs) + + async def bookmarks_remove( + self, + *, + bookmark_id: str, + channel_id: str, + **kwargs, + ) -> AsyncSlackResponse: + """Remove bookmark from the channel. + https://docs.slack.dev/reference/methods/bookmarks.remove + """ + kwargs.update({"bookmark_id": bookmark_id, "channel_id": channel_id}) + return await self.api_call("bookmarks.remove", http_verb="POST", params=kwargs) + + async def bots_info( + self, + *, + bot: Optional[str] = None, + team_id: Optional[str] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Gets information about a bot user. + https://docs.slack.dev/reference/methods/bots.info + """ + kwargs.update({"bot": bot, "team_id": team_id}) + return await self.api_call("bots.info", http_verb="GET", params=kwargs) + + async def calls_add( + self, + *, + external_unique_id: str, + join_url: str, + created_by: Optional[str] = None, + date_start: Optional[int] = None, + desktop_app_join_url: Optional[str] = None, + external_display_id: Optional[str] = None, + title: Optional[str] = None, + users: Optional[Union[str, Sequence[Dict[str, str]]]] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Registers a new Call. + https://docs.slack.dev/reference/methods/calls.add + """ + kwargs.update( + { + "external_unique_id": external_unique_id, + "join_url": join_url, + "created_by": created_by, + "date_start": date_start, + "desktop_app_join_url": desktop_app_join_url, + "external_display_id": external_display_id, + "title": title, + } + ) + _update_call_participants( + kwargs, + users if users is not None else kwargs.get("users"), # type: ignore[arg-type] + ) + return await self.api_call("calls.add", http_verb="POST", params=kwargs) + + async def calls_end( + self, + *, + id: str, + duration: Optional[int] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Ends a Call. + https://docs.slack.dev/reference/methods/calls.end + """ + kwargs.update({"id": id, "duration": duration}) + return await self.api_call("calls.end", http_verb="POST", params=kwargs) + + async def calls_info( + self, + *, + id: str, + **kwargs, + ) -> AsyncSlackResponse: + """Returns information about a Call. + https://docs.slack.dev/reference/methods/calls.info + """ + kwargs.update({"id": id}) + return await self.api_call("calls.info", http_verb="POST", params=kwargs) + + async def calls_participants_add( + self, + *, + id: str, + users: Union[str, Sequence[Dict[str, str]]], + **kwargs, + ) -> AsyncSlackResponse: + """Registers new participants added to a Call. + https://docs.slack.dev/reference/methods/calls.participants.add + """ + kwargs.update({"id": id}) + _update_call_participants(kwargs, users) + return await self.api_call("calls.participants.add", http_verb="POST", params=kwargs) + + async def calls_participants_remove( + self, + *, + id: str, + users: Union[str, Sequence[Dict[str, str]]], + **kwargs, + ) -> AsyncSlackResponse: + """Registers participants removed from a Call. + https://docs.slack.dev/reference/methods/calls.participants.remove + """ + kwargs.update({"id": id}) + _update_call_participants(kwargs, users) + return await self.api_call("calls.participants.remove", http_verb="POST", params=kwargs) + + async def calls_update( + self, + *, + id: str, + desktop_app_join_url: Optional[str] = None, + join_url: Optional[str] = None, + title: Optional[str] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Updates information about a Call. + https://docs.slack.dev/reference/methods/calls.update + """ + kwargs.update( + { + "id": id, + "desktop_app_join_url": desktop_app_join_url, + "join_url": join_url, + "title": title, + } + ) + return await self.api_call("calls.update", http_verb="POST", params=kwargs) + + async def canvases_create( + self, + *, + title: Optional[str] = None, + document_content: Dict[str, str], + **kwargs, + ) -> AsyncSlackResponse: + """Create Canvas for a user + https://docs.slack.dev/reference/methods/canvases.create + """ + kwargs.update({"title": title, "document_content": document_content}) + return await self.api_call("canvases.create", json=kwargs) + + async def canvases_edit( + self, + *, + canvas_id: str, + changes: Sequence[Dict[str, Any]], + **kwargs, + ) -> AsyncSlackResponse: + """Update an existing canvas + https://docs.slack.dev/reference/methods/canvases.edit + """ + kwargs.update({"canvas_id": canvas_id, "changes": changes}) + return await self.api_call("canvases.edit", json=kwargs) + + async def canvases_delete( + self, + *, + canvas_id: str, + **kwargs, + ) -> AsyncSlackResponse: + """Deletes a canvas + https://docs.slack.dev/reference/methods/canvases.delete + """ + kwargs.update({"canvas_id": canvas_id}) + return await self.api_call("canvases.delete", params=kwargs) + + async def canvases_access_set( + self, + *, + canvas_id: str, + access_level: str, + channel_ids: Optional[Union[Sequence[str], str]] = None, + user_ids: Optional[Union[Sequence[str], str]] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Sets the access level to a canvas for specified entities + https://docs.slack.dev/reference/methods/canvases.access.set + """ + kwargs.update({"canvas_id": canvas_id, "access_level": access_level}) + if channel_ids is not None: + if isinstance(channel_ids, (list, tuple)): + kwargs.update({"channel_ids": ",".join(channel_ids)}) + else: + kwargs.update({"channel_ids": channel_ids}) + if user_ids is not None: + if isinstance(user_ids, (list, tuple)): + kwargs.update({"user_ids": ",".join(user_ids)}) + else: + kwargs.update({"user_ids": user_ids}) + + return await self.api_call("canvases.access.set", params=kwargs) + + async def canvases_access_delete( + self, + *, + canvas_id: str, + channel_ids: Optional[Union[Sequence[str], str]] = None, + user_ids: Optional[Union[Sequence[str], str]] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Create a Channel Canvas for a channel + https://docs.slack.dev/reference/methods/canvases.access.delete + """ + kwargs.update({"canvas_id": canvas_id}) + if channel_ids is not None: + if isinstance(channel_ids, (list, tuple)): + kwargs.update({"channel_ids": ",".join(channel_ids)}) + else: + kwargs.update({"channel_ids": channel_ids}) + if user_ids is not None: + if isinstance(user_ids, (list, tuple)): + kwargs.update({"user_ids": ",".join(user_ids)}) + else: + kwargs.update({"user_ids": user_ids}) + return await self.api_call("canvases.access.delete", params=kwargs) + + async def canvases_sections_lookup( + self, + *, + canvas_id: str, + criteria: Dict[str, Any], + **kwargs, + ) -> AsyncSlackResponse: + """Find sections matching the provided criteria + https://docs.slack.dev/reference/methods/canvases.sections.lookup + """ + kwargs.update({"canvas_id": canvas_id, "criteria": json.dumps(criteria)}) + return await self.api_call("canvases.sections.lookup", params=kwargs) + + # -------------------------- + # Deprecated: channels.* + # You can use conversations.* APIs instead. + # https://docs.slack.dev/changelog/2020-01-deprecating-antecedents-to-the-conversations-api/ + # -------------------------- + + async def channels_archive( + self, + *, + channel: str, + **kwargs, + ) -> AsyncSlackResponse: + """Archives a channel.""" + kwargs.update({"channel": channel}) + kwargs = _remove_none_values(kwargs) + return await self.api_call("channels.archive", json=kwargs) + + async def channels_create( + self, + *, + name: str, + **kwargs, + ) -> AsyncSlackResponse: + """Creates a channel.""" + kwargs.update({"name": name}) + kwargs = _remove_none_values(kwargs) + return await self.api_call("channels.create", json=kwargs) + + async def channels_history( + self, + *, + channel: str, + **kwargs, + ) -> AsyncSlackResponse: + """Fetches history of messages and events from a channel.""" + kwargs.update({"channel": channel}) + return await self.api_call("channels.history", http_verb="GET", params=kwargs) + + async def channels_info( + self, + *, + channel: str, + **kwargs, + ) -> AsyncSlackResponse: + """Gets information about a channel.""" + kwargs.update({"channel": channel}) + return await self.api_call("channels.info", http_verb="GET", params=kwargs) + + async def channels_invite( + self, + *, + channel: str, + user: str, + **kwargs, + ) -> AsyncSlackResponse: + """Invites a user to a channel.""" + kwargs.update({"channel": channel, "user": user}) + kwargs = _remove_none_values(kwargs) + return await self.api_call("channels.invite", json=kwargs) + + async def channels_join( + self, + *, + name: str, + **kwargs, + ) -> AsyncSlackResponse: + """Joins a channel, creating it if needed.""" + kwargs.update({"name": name}) + kwargs = _remove_none_values(kwargs) + return await self.api_call("channels.join", json=kwargs) + + async def channels_kick( + self, + *, + channel: str, + user: str, + **kwargs, + ) -> AsyncSlackResponse: + """Removes a user from a channel.""" + kwargs.update({"channel": channel, "user": user}) + kwargs = _remove_none_values(kwargs) + return await self.api_call("channels.kick", json=kwargs) + + async def channels_leave( + self, + *, + channel: str, + **kwargs, + ) -> AsyncSlackResponse: + """Leaves a channel.""" + kwargs.update({"channel": channel}) + kwargs = _remove_none_values(kwargs) + return await self.api_call("channels.leave", json=kwargs) + + async def channels_list( + self, + **kwargs, + ) -> AsyncSlackResponse: + """Lists all channels in a Slack team.""" + return await self.api_call("channels.list", http_verb="GET", params=kwargs) + + async def channels_mark( + self, + *, + channel: str, + ts: str, + **kwargs, + ) -> AsyncSlackResponse: + """Sets the read cursor in a channel.""" + kwargs.update({"channel": channel, "ts": ts}) + kwargs = _remove_none_values(kwargs) + return await self.api_call("channels.mark", json=kwargs) + + async def channels_rename( + self, + *, + channel: str, + name: str, + **kwargs, + ) -> AsyncSlackResponse: + """Renames a channel.""" + kwargs.update({"channel": channel, "name": name}) + kwargs = _remove_none_values(kwargs) + return await self.api_call("channels.rename", json=kwargs) + + async def channels_replies( + self, + *, + channel: str, + thread_ts: str, + **kwargs, + ) -> AsyncSlackResponse: + """Retrieve a thread of messages posted to a channel""" + kwargs.update({"channel": channel, "thread_ts": thread_ts}) + return await self.api_call("channels.replies", http_verb="GET", params=kwargs) + + async def channels_setPurpose( + self, + *, + channel: str, + purpose: str, + **kwargs, + ) -> AsyncSlackResponse: + """Sets the purpose for a channel.""" + kwargs.update({"channel": channel, "purpose": purpose}) + kwargs = _remove_none_values(kwargs) + return await self.api_call("channels.setPurpose", json=kwargs) + + async def channels_setTopic( + self, + *, + channel: str, + topic: str, + **kwargs, + ) -> AsyncSlackResponse: + """Sets the topic for a channel.""" + kwargs.update({"channel": channel, "topic": topic}) + kwargs = _remove_none_values(kwargs) + return await self.api_call("channels.setTopic", json=kwargs) + + async def channels_unarchive( + self, + *, + channel: str, + **kwargs, + ) -> AsyncSlackResponse: + """Unarchives a channel.""" + kwargs.update({"channel": channel}) + kwargs = _remove_none_values(kwargs) + return await self.api_call("channels.unarchive", json=kwargs) + + # -------------------------- + + async def chat_appendStream( + self, + *, + channel: str, + ts: str, + markdown_text: str, + **kwargs, + ) -> AsyncSlackResponse: + """Appends text to an existing streaming conversation. + https://docs.slack.dev/reference/methods/chat.appendStream + """ + kwargs.update( + { + "channel": channel, + "ts": ts, + "markdown_text": markdown_text, + } + ) + kwargs = _remove_none_values(kwargs) + return await self.api_call("chat.appendStream", json=kwargs) + + async def chat_delete( + self, + *, + channel: str, + ts: str, + as_user: Optional[bool] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Deletes a message. + https://docs.slack.dev/reference/methods/chat.delete + """ + kwargs.update({"channel": channel, "ts": ts, "as_user": as_user}) + return await self.api_call("chat.delete", params=kwargs) + + async def chat_deleteScheduledMessage( + self, + *, + channel: str, + scheduled_message_id: str, + as_user: Optional[bool] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Deletes a scheduled message. + https://docs.slack.dev/reference/methods/chat.deleteScheduledMessage + """ + kwargs.update( + { + "channel": channel, + "scheduled_message_id": scheduled_message_id, + "as_user": as_user, + } + ) + return await self.api_call("chat.deleteScheduledMessage", params=kwargs) + + async def chat_getPermalink( + self, + *, + channel: str, + message_ts: str, + **kwargs, + ) -> AsyncSlackResponse: + """Retrieve a permalink URL for a specific extant message + https://docs.slack.dev/reference/methods/chat.getPermalink + """ + kwargs.update({"channel": channel, "message_ts": message_ts}) + return await self.api_call("chat.getPermalink", http_verb="GET", params=kwargs) + + async def chat_meMessage( + self, + *, + channel: str, + text: str, + **kwargs, + ) -> AsyncSlackResponse: + """Share a me message into a channel. + https://docs.slack.dev/reference/methods/chat.meMessage + """ + kwargs.update({"channel": channel, "text": text}) + return await self.api_call("chat.meMessage", params=kwargs) + + async def chat_postEphemeral( + self, + *, + channel: str, + user: str, + text: Optional[str] = None, + as_user: Optional[bool] = None, + attachments: Optional[Union[str, Sequence[Union[Dict, Attachment]]]] = None, + blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None, + thread_ts: Optional[str] = None, + icon_emoji: Optional[str] = None, + icon_url: Optional[str] = None, + link_names: Optional[bool] = None, + username: Optional[str] = None, + parse: Optional[str] = None, + markdown_text: Optional[str] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Sends an ephemeral message to a user in a channel. + https://docs.slack.dev/reference/methods/chat.postEphemeral + """ + kwargs.update( + { + "channel": channel, + "user": user, + "text": text, + "as_user": as_user, + "attachments": attachments, + "blocks": blocks, + "thread_ts": thread_ts, + "icon_emoji": icon_emoji, + "icon_url": icon_url, + "link_names": link_names, + "username": username, + "parse": parse, + "markdown_text": markdown_text, + } + ) + _parse_web_class_objects(kwargs) + kwargs = _remove_none_values(kwargs) + _warn_if_message_text_content_is_missing("chat.postEphemeral", kwargs) + # NOTE: intentionally using json over params for the API methods using blocks/attachments + return await self.api_call("chat.postEphemeral", json=kwargs) + + async def chat_postMessage( + self, + *, + channel: str, + text: Optional[str] = None, + as_user: Optional[bool] = None, + attachments: Optional[Union[str, Sequence[Union[Dict, Attachment]]]] = None, + blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None, + thread_ts: Optional[str] = None, + reply_broadcast: Optional[bool] = None, + unfurl_links: Optional[bool] = None, + unfurl_media: Optional[bool] = None, + container_id: Optional[str] = None, + icon_emoji: Optional[str] = None, + icon_url: Optional[str] = None, + mrkdwn: Optional[bool] = None, + link_names: Optional[bool] = None, + username: Optional[str] = None, + parse: Optional[str] = None, # none, full + metadata: Optional[Union[Dict, Metadata, EventAndEntityMetadata]] = None, + markdown_text: Optional[str] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Sends a message to a channel. + https://docs.slack.dev/reference/methods/chat.postMessage + """ + kwargs.update( + { + "channel": channel, + "text": text, + "as_user": as_user, + "attachments": attachments, + "blocks": blocks, + "thread_ts": thread_ts, + "reply_broadcast": reply_broadcast, + "unfurl_links": unfurl_links, + "unfurl_media": unfurl_media, + "container_id": container_id, + "icon_emoji": icon_emoji, + "icon_url": icon_url, + "mrkdwn": mrkdwn, + "link_names": link_names, + "username": username, + "parse": parse, + "metadata": metadata, + "markdown_text": markdown_text, + } + ) + _parse_web_class_objects(kwargs) + kwargs = _remove_none_values(kwargs) + _warn_if_message_text_content_is_missing("chat.postMessage", kwargs) + # NOTE: intentionally using json over params for the API methods using blocks/attachments + return await self.api_call("chat.postMessage", json=kwargs) + + async def chat_scheduleMessage( + self, + *, + channel: str, + post_at: Union[str, int], + text: Optional[str] = None, + as_user: Optional[bool] = None, + attachments: Optional[Union[str, Sequence[Union[Dict, Attachment]]]] = None, + blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None, + thread_ts: Optional[str] = None, + parse: Optional[str] = None, + reply_broadcast: Optional[bool] = None, + unfurl_links: Optional[bool] = None, + unfurl_media: Optional[bool] = None, + link_names: Optional[bool] = None, + metadata: Optional[Union[Dict, Metadata]] = None, + markdown_text: Optional[str] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Schedules a message. + https://docs.slack.dev/reference/methods/chat.scheduleMessage + """ + kwargs.update( + { + "channel": channel, + "post_at": post_at, + "text": text, + "as_user": as_user, + "attachments": attachments, + "blocks": blocks, + "thread_ts": thread_ts, + "reply_broadcast": reply_broadcast, + "parse": parse, + "unfurl_links": unfurl_links, + "unfurl_media": unfurl_media, + "link_names": link_names, + "metadata": metadata, + "markdown_text": markdown_text, + } + ) + _parse_web_class_objects(kwargs) + kwargs = _remove_none_values(kwargs) + _warn_if_message_text_content_is_missing("chat.scheduleMessage", kwargs) + # NOTE: intentionally using json over params for the API methods using blocks/attachments + return await self.api_call("chat.scheduleMessage", json=kwargs) + + async def chat_scheduledMessages_list( + self, + *, + channel: Optional[str] = None, + cursor: Optional[str] = None, + latest: Optional[str] = None, + limit: Optional[int] = None, + oldest: Optional[str] = None, + team_id: Optional[str] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Lists all scheduled messages. + https://docs.slack.dev/reference/methods/chat.scheduledMessages.list + """ + kwargs.update( + { + "channel": channel, + "cursor": cursor, + "latest": latest, + "limit": limit, + "oldest": oldest, + "team_id": team_id, + } + ) + return await self.api_call("chat.scheduledMessages.list", params=kwargs) + + async def chat_startStream( + self, + *, + channel: str, + thread_ts: str, + markdown_text: Optional[str] = None, + recipient_team_id: Optional[str] = None, + recipient_user_id: Optional[str] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Starts a new streaming conversation. + https://docs.slack.dev/reference/methods/chat.startStream + """ + kwargs.update( + { + "channel": channel, + "thread_ts": thread_ts, + "markdown_text": markdown_text, + "recipient_team_id": recipient_team_id, + "recipient_user_id": recipient_user_id, + } + ) + kwargs = _remove_none_values(kwargs) + return await self.api_call("chat.startStream", json=kwargs) + + async def chat_stopStream( + self, + *, + channel: str, + ts: str, + markdown_text: Optional[str] = None, + blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None, + metadata: Optional[Union[Dict, Metadata]] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Stops a streaming conversation. + https://docs.slack.dev/reference/methods/chat.stopStream + """ + kwargs.update( + { + "channel": channel, + "ts": ts, + "markdown_text": markdown_text, + "blocks": blocks, + "metadata": metadata, + } + ) + _parse_web_class_objects(kwargs) + kwargs = _remove_none_values(kwargs) + return await self.api_call("chat.stopStream", json=kwargs) + + async def chat_stream( + self, + *, + buffer_size: int = 256, + channel: str, + thread_ts: str, + recipient_team_id: Optional[str] = None, + recipient_user_id: Optional[str] = None, + **kwargs, + ) -> AsyncChatStream: + """Stream markdown text into a conversation. + + This method starts a new chat stream in a conversation that can be appended to. After appending an entire message, + the stream can be stopped with concluding arguments such as "blocks" for gathering feedback. + + The following methods are used: + + - chat.startStream: Starts a new streaming conversation. + [Reference](https://docs.slack.dev/reference/methods/chat.startStream). + - chat.appendStream: Appends text to an existing streaming conversation. + [Reference](https://docs.slack.dev/reference/methods/chat.appendStream). + - chat.stopStream: Stops a streaming conversation. + [Reference](https://docs.slack.dev/reference/methods/chat.stopStream). + + Args: + buffer_size: The length of markdown_text to buffer in-memory before calling a stream method. Increasing this + value decreases the number of method calls made for the same amount of text, which is useful to avoid rate + limits. Default: 256. + channel: An encoded ID that represents a channel, private group, or DM. + thread_ts: Provide another message's ts value to reply to. Streamed messages should always be replies to a user + request. + recipient_team_id: The encoded ID of the team the user receiving the streaming text belongs to. Required when + streaming to channels. + recipient_user_id: The encoded ID of the user to receive the streaming text. Required when streaming to channels. + **kwargs: Additional arguments passed to the underlying API calls. + + Returns: + ChatStream instance for managing the stream + + Example: + ```python + streamer = await client.chat_stream( + channel="C0123456789", + thread_ts="1700000001.123456", + recipient_team_id="T0123456789", + recipient_user_id="U0123456789", + ) + await streamer.append(markdown_text="**hello wo") + await streamer.append(markdown_text="rld!**") + await streamer.stop() + ``` + """ + return AsyncChatStream( + self, + logger=self._logger, + channel=channel, + thread_ts=thread_ts, + recipient_team_id=recipient_team_id, + recipient_user_id=recipient_user_id, + buffer_size=buffer_size, + **kwargs, + ) + + async def chat_unfurl( + self, + *, + channel: Optional[str] = None, + ts: Optional[str] = None, + source: Optional[str] = None, + unfurl_id: Optional[str] = None, + unfurls: Optional[Dict[str, Dict]] = None, # or user_auth_* + metadata: Optional[Union[Dict, EventAndEntityMetadata]] = None, + user_auth_blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None, + user_auth_message: Optional[str] = None, + user_auth_required: Optional[bool] = None, + user_auth_url: Optional[str] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Provide custom unfurl behavior for user-posted URLs. + https://docs.slack.dev/reference/methods/chat.unfurl + """ + kwargs.update( + { + "channel": channel, + "ts": ts, + "source": source, + "unfurl_id": unfurl_id, + "unfurls": unfurls, + "metadata": metadata, + "user_auth_blocks": user_auth_blocks, + "user_auth_message": user_auth_message, + "user_auth_required": user_auth_required, + "user_auth_url": user_auth_url, + } + ) + _parse_web_class_objects(kwargs) # for user_auth_blocks + kwargs = _remove_none_values(kwargs) + # NOTE: intentionally using json over params for API methods using blocks/attachments + return await self.api_call("chat.unfurl", json=kwargs) + + async def chat_update( + self, + *, + channel: str, + ts: str, + text: Optional[str] = None, + attachments: Optional[Union[str, Sequence[Union[Dict, Attachment]]]] = None, + blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None, + as_user: Optional[bool] = None, + file_ids: Optional[Union[str, Sequence[str]]] = None, + link_names: Optional[bool] = None, + parse: Optional[str] = None, # none, full + reply_broadcast: Optional[bool] = None, + metadata: Optional[Union[Dict, Metadata]] = None, + markdown_text: Optional[str] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Updates a message in a channel. + https://docs.slack.dev/reference/methods/chat.update + """ + kwargs.update( + { + "channel": channel, + "ts": ts, + "text": text, + "attachments": attachments, + "blocks": blocks, + "as_user": as_user, + "link_names": link_names, + "parse": parse, + "reply_broadcast": reply_broadcast, + "metadata": metadata, + "markdown_text": markdown_text, + } + ) + if isinstance(file_ids, (list, tuple)): + kwargs.update({"file_ids": ",".join(file_ids)}) + else: + kwargs.update({"file_ids": file_ids}) + _parse_web_class_objects(kwargs) + kwargs = _remove_none_values(kwargs) + _warn_if_message_text_content_is_missing("chat.update", kwargs) + # NOTE: intentionally using json over params for API methods using blocks/attachments + return await self.api_call("chat.update", json=kwargs) + + async def conversations_acceptSharedInvite( + self, + *, + channel_name: str, + channel_id: Optional[str] = None, + invite_id: Optional[str] = None, + free_trial_accepted: Optional[bool] = None, + is_private: Optional[bool] = None, + team_id: Optional[str] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Accepts an invitation to a Slack Connect channel. + https://docs.slack.dev/reference/methods/conversations.acceptSharedInvite + """ + if channel_id is None and invite_id is None: + raise e.SlackRequestError("Either channel_id or invite_id must be provided.") + kwargs.update( + { + "channel_name": channel_name, + "channel_id": channel_id, + "invite_id": invite_id, + "free_trial_accepted": free_trial_accepted, + "is_private": is_private, + "team_id": team_id, + } + ) + return await self.api_call("conversations.acceptSharedInvite", http_verb="POST", params=kwargs) + + async def conversations_approveSharedInvite( + self, + *, + invite_id: str, + target_team: Optional[str] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Approves an invitation to a Slack Connect channel. + https://docs.slack.dev/reference/methods/conversations.approveSharedInvite + """ + kwargs.update({"invite_id": invite_id, "target_team": target_team}) + return await self.api_call("conversations.approveSharedInvite", http_verb="POST", params=kwargs) + + async def conversations_archive( + self, + *, + channel: str, + **kwargs, + ) -> AsyncSlackResponse: + """Archives a conversation. + https://docs.slack.dev/reference/methods/conversations.archive + """ + kwargs.update({"channel": channel}) + return await self.api_call("conversations.archive", params=kwargs) + + async def conversations_close( + self, + *, + channel: str, + **kwargs, + ) -> AsyncSlackResponse: + """Closes a direct message or multi-person direct message. + https://docs.slack.dev/reference/methods/conversations.close + """ + kwargs.update({"channel": channel}) + return await self.api_call("conversations.close", params=kwargs) + + async def conversations_create( + self, + *, + name: str, + is_private: Optional[bool] = None, + team_id: Optional[str] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Initiates a public or private channel-based conversation + https://docs.slack.dev/reference/methods/conversations.create + """ + kwargs.update({"name": name, "is_private": is_private, "team_id": team_id}) + return await self.api_call("conversations.create", params=kwargs) + + async def conversations_declineSharedInvite( + self, + *, + invite_id: str, + target_team: Optional[str] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Declines a Slack Connect channel invite. + https://docs.slack.dev/reference/methods/conversations.declineSharedInvite + """ + kwargs.update({"invite_id": invite_id, "target_team": target_team}) + return await self.api_call("conversations.declineSharedInvite", http_verb="GET", params=kwargs) + + async def conversations_externalInvitePermissions_set( + self, *, action: str, channel: str, target_team: str, **kwargs + ) -> AsyncSlackResponse: + """Sets a team in a shared External Limited channel to a shared Slack Connect channel or vice versa. + https://docs.slack.dev/reference/methods/conversations.externalInvitePermissions.set + """ + kwargs.update( + { + "action": action, + "channel": channel, + "target_team": target_team, + } + ) + return await self.api_call("conversations.externalInvitePermissions.set", params=kwargs) + + async def conversations_history( + self, + *, + channel: str, + cursor: Optional[str] = None, + inclusive: Optional[bool] = None, + include_all_metadata: Optional[bool] = None, + latest: Optional[str] = None, + limit: Optional[int] = None, + oldest: Optional[str] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Fetches a conversation's history of messages and events. + https://docs.slack.dev/reference/methods/conversations.history + """ + kwargs.update( + { + "channel": channel, + "cursor": cursor, + "inclusive": inclusive, + "include_all_metadata": include_all_metadata, + "limit": limit, + "latest": latest, + "oldest": oldest, + } + ) + return await self.api_call("conversations.history", http_verb="GET", params=kwargs) + + async def conversations_info( + self, + *, + channel: str, + include_locale: Optional[bool] = None, + include_num_members: Optional[bool] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Retrieve information about a conversation. + https://docs.slack.dev/reference/methods/conversations.info + """ + kwargs.update( + { + "channel": channel, + "include_locale": include_locale, + "include_num_members": include_num_members, + } + ) + return await self.api_call("conversations.info", http_verb="GET", params=kwargs) + + async def conversations_invite( + self, + *, + channel: str, + users: Union[str, Sequence[str]], + force: Optional[bool] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Invites users to a channel. + https://docs.slack.dev/reference/methods/conversations.invite + """ + kwargs.update( + { + "channel": channel, + "force": force, + } + ) + if isinstance(users, (list, tuple)): + kwargs.update({"users": ",".join(users)}) + else: + kwargs.update({"users": users}) + return await self.api_call("conversations.invite", params=kwargs) + + async def conversations_inviteShared( + self, + *, + channel: str, + emails: Optional[Union[str, Sequence[str]]] = None, + user_ids: Optional[Union[str, Sequence[str]]] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Sends an invitation to a Slack Connect channel. + https://docs.slack.dev/reference/methods/conversations.inviteShared + """ + if emails is None and user_ids is None: + raise e.SlackRequestError("Either emails or user ids must be provided.") + kwargs.update({"channel": channel}) + if isinstance(emails, (list, tuple)): + kwargs.update({"emails": ",".join(emails)}) + else: + kwargs.update({"emails": emails}) + if isinstance(user_ids, (list, tuple)): + kwargs.update({"user_ids": ",".join(user_ids)}) + else: + kwargs.update({"user_ids": user_ids}) + return await self.api_call("conversations.inviteShared", http_verb="GET", params=kwargs) + + async def conversations_join( + self, + *, + channel: str, + **kwargs, + ) -> AsyncSlackResponse: + """Joins an existing conversation. + https://docs.slack.dev/reference/methods/conversations.join + """ + kwargs.update({"channel": channel}) + return await self.api_call("conversations.join", params=kwargs) + + async def conversations_kick( + self, + *, + channel: str, + user: str, + **kwargs, + ) -> AsyncSlackResponse: + """Removes a user from a conversation. + https://docs.slack.dev/reference/methods/conversations.kick + """ + kwargs.update({"channel": channel, "user": user}) + return await self.api_call("conversations.kick", params=kwargs) + + async def conversations_leave( + self, + *, + channel: str, + **kwargs, + ) -> AsyncSlackResponse: + """Leaves a conversation. + https://docs.slack.dev/reference/methods/conversations.leave + """ + kwargs.update({"channel": channel}) + return await self.api_call("conversations.leave", params=kwargs) + + async def conversations_list( + self, + *, + cursor: Optional[str] = None, + exclude_archived: Optional[bool] = None, + limit: Optional[int] = None, + team_id: Optional[str] = None, + types: Optional[Union[str, Sequence[str]]] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Lists all channels in a Slack team. + https://docs.slack.dev/reference/methods/conversations.list + """ + kwargs.update( + { + "cursor": cursor, + "exclude_archived": exclude_archived, + "limit": limit, + "team_id": team_id, + } + ) + if isinstance(types, (list, tuple)): + kwargs.update({"types": ",".join(types)}) + else: + kwargs.update({"types": types}) + return await self.api_call("conversations.list", http_verb="GET", params=kwargs) + + async def conversations_listConnectInvites( + self, + *, + count: Optional[int] = None, + cursor: Optional[str] = None, + team_id: Optional[str] = None, + **kwargs, + ) -> AsyncSlackResponse: + """List shared channel invites that have been generated + or received but have not yet been approved by all parties. + https://docs.slack.dev/reference/methods/conversations.listConnectInvites + """ + kwargs.update({"count": count, "cursor": cursor, "team_id": team_id}) + return await self.api_call("conversations.listConnectInvites", params=kwargs) + + async def conversations_mark( + self, + *, + channel: str, + ts: str, + **kwargs, + ) -> AsyncSlackResponse: + """Sets the read cursor in a channel. + https://docs.slack.dev/reference/methods/conversations.mark + """ + kwargs.update({"channel": channel, "ts": ts}) + return await self.api_call("conversations.mark", params=kwargs) + + async def conversations_members( + self, + *, + channel: str, + cursor: Optional[str] = None, + limit: Optional[int] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Retrieve members of a conversation. + https://docs.slack.dev/reference/methods/conversations.members + """ + kwargs.update({"channel": channel, "cursor": cursor, "limit": limit}) + return await self.api_call("conversations.members", http_verb="GET", params=kwargs) + + async def conversations_open( + self, + *, + channel: Optional[str] = None, + return_im: Optional[bool] = None, + users: Optional[Union[str, Sequence[str]]] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Opens or resumes a direct message or multi-person direct message. + https://docs.slack.dev/reference/methods/conversations.open + """ + if channel is None and users is None: + raise e.SlackRequestError("Either channel or users must be provided.") + kwargs.update({"channel": channel, "return_im": return_im}) + if isinstance(users, (list, tuple)): + kwargs.update({"users": ",".join(users)}) + else: + kwargs.update({"users": users}) + return await self.api_call("conversations.open", params=kwargs) + + async def conversations_rename( + self, + *, + channel: str, + name: str, + **kwargs, + ) -> AsyncSlackResponse: + """Renames a conversation. + https://docs.slack.dev/reference/methods/conversations.rename + """ + kwargs.update({"channel": channel, "name": name}) + return await self.api_call("conversations.rename", params=kwargs) + + async def conversations_replies( + self, + *, + channel: str, + ts: str, + cursor: Optional[str] = None, + inclusive: Optional[bool] = None, + include_all_metadata: Optional[bool] = None, + latest: Optional[str] = None, + limit: Optional[int] = None, + oldest: Optional[str] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Retrieve a thread of messages posted to a conversation + https://docs.slack.dev/reference/methods/conversations.replies + """ + kwargs.update( + { + "channel": channel, + "ts": ts, + "cursor": cursor, + "inclusive": inclusive, + "include_all_metadata": include_all_metadata, + "limit": limit, + "latest": latest, + "oldest": oldest, + } + ) + return await self.api_call("conversations.replies", http_verb="GET", params=kwargs) + + async def conversations_requestSharedInvite_approve( + self, + *, + invite_id: str, + channel_id: Optional[str] = None, + is_external_limited: Optional[str] = None, + message: Optional[Dict[str, Any]] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Approve a request to add an external user to a channel. This also sends them a Slack Connect invite. + https://docs.slack.dev/reference/methods/conversations.requestSharedInvite.approve + """ + kwargs.update( + { + "invite_id": invite_id, + "channel_id": channel_id, + "is_external_limited": is_external_limited, + } + ) + if message is not None: + kwargs.update({"message": json.dumps(message)}) + return await self.api_call("conversations.requestSharedInvite.approve", params=kwargs) + + async def conversations_requestSharedInvite_deny( + self, + *, + invite_id: str, + message: Optional[str] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Deny a request to invite an external user to a channel. + https://docs.slack.dev/reference/methods/conversations.requestSharedInvite.deny + """ + kwargs.update({"invite_id": invite_id, "message": message}) + return await self.api_call("conversations.requestSharedInvite.deny", params=kwargs) + + async def conversations_requestSharedInvite_list( + self, + *, + cursor: Optional[str] = None, + include_approved: Optional[bool] = None, + include_denied: Optional[bool] = None, + include_expired: Optional[bool] = None, + invite_ids: Optional[Union[str, Sequence[str]]] = None, + limit: Optional[int] = None, + user_id: Optional[str] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Lists requests to add external users to channels with ability to filter. + https://docs.slack.dev/reference/methods/conversations.requestSharedInvite.list + """ + kwargs.update( + { + "cursor": cursor, + "include_approved": include_approved, + "include_denied": include_denied, + "include_expired": include_expired, + "limit": limit, + "user_id": user_id, + } + ) + if invite_ids is not None: + if isinstance(invite_ids, (list, tuple)): + kwargs.update({"invite_ids": ",".join(invite_ids)}) + else: + kwargs.update({"invite_ids": invite_ids}) + return await self.api_call("conversations.requestSharedInvite.list", params=kwargs) + + async def conversations_setPurpose( + self, + *, + channel: str, + purpose: str, + **kwargs, + ) -> AsyncSlackResponse: + """Sets the purpose for a conversation. + https://docs.slack.dev/reference/methods/conversations.setPurpose + """ + kwargs.update({"channel": channel, "purpose": purpose}) + return await self.api_call("conversations.setPurpose", params=kwargs) + + async def conversations_setTopic( + self, + *, + channel: str, + topic: str, + **kwargs, + ) -> AsyncSlackResponse: + """Sets the topic for a conversation. + https://docs.slack.dev/reference/methods/conversations.setTopic + """ + kwargs.update({"channel": channel, "topic": topic}) + return await self.api_call("conversations.setTopic", params=kwargs) + + async def conversations_unarchive( + self, + *, + channel: str, + **kwargs, + ) -> AsyncSlackResponse: + """Reverses conversation archival. + https://docs.slack.dev/reference/methods/conversations.unarchive + """ + kwargs.update({"channel": channel}) + return await self.api_call("conversations.unarchive", params=kwargs) + + async def conversations_canvases_create( + self, + *, + channel_id: str, + document_content: Dict[str, str], + **kwargs, + ) -> AsyncSlackResponse: + """Create a Channel Canvas for a channel + https://docs.slack.dev/reference/methods/conversations.canvases.create + """ + kwargs.update({"channel_id": channel_id, "document_content": document_content}) + return await self.api_call("conversations.canvases.create", json=kwargs) + + async def dialog_open( + self, + *, + dialog: Dict[str, Any], + trigger_id: str, + **kwargs, + ) -> AsyncSlackResponse: + """Open a dialog with a user. + https://docs.slack.dev/reference/methods/dialog.open + """ + kwargs.update({"dialog": dialog, "trigger_id": trigger_id}) + kwargs = _remove_none_values(kwargs) + # NOTE: As the dialog can be a dict, this API call works only with json format. + return await self.api_call("dialog.open", json=kwargs) + + async def dnd_endDnd( + self, + **kwargs, + ) -> AsyncSlackResponse: + """Ends the current user's Do Not Disturb session immediately. + https://docs.slack.dev/reference/methods/dnd.endDnd + """ + return await self.api_call("dnd.endDnd", params=kwargs) + + async def dnd_endSnooze( + self, + **kwargs, + ) -> AsyncSlackResponse: + """Ends the current user's snooze mode immediately. + https://docs.slack.dev/reference/methods/dnd.endSnooze + """ + return await self.api_call("dnd.endSnooze", params=kwargs) + + async def dnd_info( + self, + *, + team_id: Optional[str] = None, + user: Optional[str] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Retrieves a user's current Do Not Disturb status. + https://docs.slack.dev/reference/methods/dnd.info + """ + kwargs.update({"team_id": team_id, "user": user}) + return await self.api_call("dnd.info", http_verb="GET", params=kwargs) + + async def dnd_setSnooze( + self, + *, + num_minutes: Union[int, str], + **kwargs, + ) -> AsyncSlackResponse: + """Turns on Do Not Disturb mode for the current user, or changes its duration. + https://docs.slack.dev/reference/methods/dnd.setSnooze + """ + kwargs.update({"num_minutes": num_minutes}) + return await self.api_call("dnd.setSnooze", http_verb="GET", params=kwargs) + + async def dnd_teamInfo( + self, + users: Union[str, Sequence[str]], + team_id: Optional[str] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Retrieves the Do Not Disturb status for users on a team. + https://docs.slack.dev/reference/methods/dnd.teamInfo + """ + if isinstance(users, (list, tuple)): + kwargs.update({"users": ",".join(users)}) + else: + kwargs.update({"users": users}) + kwargs.update({"team_id": team_id}) + return await self.api_call("dnd.teamInfo", http_verb="GET", params=kwargs) + + async def emoji_list( + self, + include_categories: Optional[bool] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Lists custom emoji for a team. + https://docs.slack.dev/reference/methods/emoji.list + """ + kwargs.update({"include_categories": include_categories}) + return await self.api_call("emoji.list", http_verb="GET", params=kwargs) + + async def entity_presentDetails( + self, + trigger_id: str, + metadata: Optional[Union[Dict, EntityMetadata]] = None, + user_auth_required: Optional[bool] = None, + user_auth_url: Optional[str] = None, + error: Optional[Dict[str, Any]] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Provides entity details for the flexpane. + https://docs.slack.dev/reference/methods/entity.presentDetails/ + """ + kwargs.update({"trigger_id": trigger_id}) + if metadata is not None: + kwargs.update({"metadata": metadata}) + if user_auth_required is not None: + kwargs.update({"user_auth_required": user_auth_required}) + if user_auth_url is not None: + kwargs.update({"user_auth_url": user_auth_url}) + if error is not None: + kwargs.update({"error": error}) + _parse_web_class_objects(kwargs) + return await self.api_call("entity.presentDetails", json=kwargs) + + async def files_comments_delete( + self, + *, + file: str, + id: str, + **kwargs, + ) -> AsyncSlackResponse: + """Deletes an existing comment on a file. + https://docs.slack.dev/reference/methods/files.comments.delete + """ + kwargs.update({"file": file, "id": id}) + return await self.api_call("files.comments.delete", params=kwargs) + + async def files_delete( + self, + *, + file: str, + **kwargs, + ) -> AsyncSlackResponse: + """Deletes a file. + https://docs.slack.dev/reference/methods/files.delete + """ + kwargs.update({"file": file}) + return await self.api_call("files.delete", params=kwargs) + + async def files_info( + self, + *, + file: str, + count: Optional[int] = None, + cursor: Optional[str] = None, + limit: Optional[int] = None, + page: Optional[int] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Gets information about a team file. + https://docs.slack.dev/reference/methods/files.info + """ + kwargs.update( + { + "file": file, + "count": count, + "cursor": cursor, + "limit": limit, + "page": page, + } + ) + return await self.api_call("files.info", http_verb="GET", params=kwargs) + + async def files_list( + self, + *, + channel: Optional[str] = None, + count: Optional[int] = None, + page: Optional[int] = None, + show_files_hidden_by_limit: Optional[bool] = None, + team_id: Optional[str] = None, + ts_from: Optional[str] = None, + ts_to: Optional[str] = None, + types: Optional[Union[str, Sequence[str]]] = None, + user: Optional[str] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Lists & filters team files. + https://docs.slack.dev/reference/methods/files.list + """ + kwargs.update( + { + "channel": channel, + "count": count, + "page": page, + "show_files_hidden_by_limit": show_files_hidden_by_limit, + "team_id": team_id, + "ts_from": ts_from, + "ts_to": ts_to, + "user": user, + } + ) + if isinstance(types, (list, tuple)): + kwargs.update({"types": ",".join(types)}) + else: + kwargs.update({"types": types}) + return await self.api_call("files.list", http_verb="GET", params=kwargs) + + async def files_remote_info( + self, + *, + external_id: Optional[str] = None, + file: Optional[str] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Retrieve information about a remote file added to Slack. + https://docs.slack.dev/reference/methods/files.remote.info + """ + kwargs.update({"external_id": external_id, "file": file}) + return await self.api_call("files.remote.info", http_verb="GET", params=kwargs) + + async def files_remote_list( + self, + *, + channel: Optional[str] = None, + cursor: Optional[str] = None, + limit: Optional[int] = None, + ts_from: Optional[str] = None, + ts_to: Optional[str] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Retrieve information about a remote file added to Slack. + https://docs.slack.dev/reference/methods/files.remote.list + """ + kwargs.update( + { + "channel": channel, + "cursor": cursor, + "limit": limit, + "ts_from": ts_from, + "ts_to": ts_to, + } + ) + return await self.api_call("files.remote.list", http_verb="GET", params=kwargs) + + async def files_remote_add( + self, + *, + external_id: str, + external_url: str, + title: str, + filetype: Optional[str] = None, + indexable_file_contents: Optional[Union[str, bytes, IOBase]] = None, + preview_image: Optional[Union[str, bytes, IOBase]] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Adds a file from a remote service. + https://docs.slack.dev/reference/methods/files.remote.add + """ + kwargs.update( + { + "external_id": external_id, + "external_url": external_url, + "title": title, + "filetype": filetype, + } + ) + files = None + # preview_image (file): Preview of the document via multipart/form-data. + if preview_image is not None or indexable_file_contents is not None: + files = { + "preview_image": preview_image, + "indexable_file_contents": indexable_file_contents, + } + + return await self.api_call( + # Intentionally using "POST" method over "GET" here + "files.remote.add", + http_verb="POST", + data=kwargs, + files=files, + ) + + async def files_remote_update( + self, + *, + external_id: Optional[str] = None, + external_url: Optional[str] = None, + file: Optional[str] = None, + title: Optional[str] = None, + filetype: Optional[str] = None, + indexable_file_contents: Optional[str] = None, + preview_image: Optional[str] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Updates an existing remote file. + https://docs.slack.dev/reference/methods/files.remote.update + """ + kwargs.update( + { + "external_id": external_id, + "external_url": external_url, + "file": file, + "title": title, + "filetype": filetype, + } + ) + files = None + # preview_image (file): Preview of the document via multipart/form-data. + if preview_image is not None or indexable_file_contents is not None: + files = { + "preview_image": preview_image, + "indexable_file_contents": indexable_file_contents, + } + + return await self.api_call( + # Intentionally using "POST" method over "GET" here + "files.remote.update", + http_verb="POST", + data=kwargs, + files=files, + ) + + async def files_remote_remove( + self, + *, + external_id: Optional[str] = None, + file: Optional[str] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Remove a remote file. + https://docs.slack.dev/reference/methods/files.remote.remove + """ + kwargs.update({"external_id": external_id, "file": file}) + return await self.api_call("files.remote.remove", http_verb="POST", params=kwargs) + + async def files_remote_share( + self, + *, + channels: Union[str, Sequence[str]], + external_id: Optional[str] = None, + file: Optional[str] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Share a remote file into a channel. + https://docs.slack.dev/reference/methods/files.remote.share + """ + if external_id is None and file is None: + raise e.SlackRequestError("Either external_id or file must be provided.") + if isinstance(channels, (list, tuple)): + kwargs.update({"channels": ",".join(channels)}) + else: + kwargs.update({"channels": channels}) + kwargs.update({"external_id": external_id, "file": file}) + return await self.api_call("files.remote.share", http_verb="GET", params=kwargs) + + async def files_revokePublicURL( + self, + *, + file: str, + **kwargs, + ) -> AsyncSlackResponse: + """Revokes public/external sharing access for a file + https://docs.slack.dev/reference/methods/files.revokePublicURL + """ + kwargs.update({"file": file}) + return await self.api_call("files.revokePublicURL", params=kwargs) + + async def files_sharedPublicURL( + self, + *, + file: str, + **kwargs, + ) -> AsyncSlackResponse: + """Enables a file for public/external sharing. + https://docs.slack.dev/reference/methods/files.sharedPublicURL + """ + kwargs.update({"file": file}) + return await self.api_call("files.sharedPublicURL", params=kwargs) + + async def files_upload( + self, + *, + file: Optional[Union[str, bytes, IOBase]] = None, + content: Optional[Union[str, bytes]] = None, + filename: Optional[str] = None, + filetype: Optional[str] = None, + initial_comment: Optional[str] = None, + thread_ts: Optional[str] = None, + title: Optional[str] = None, + channels: Optional[Union[str, Sequence[str]]] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Uploads or creates a file. + https://docs.slack.dev/reference/methods/files.upload + """ + _print_files_upload_v2_suggestion() + + if file is None and content is None: + raise e.SlackRequestError("The file or content argument must be specified.") + if file is not None and content is not None: + raise e.SlackRequestError("You cannot specify both the file and the content argument.") + + if isinstance(channels, (list, tuple)): + kwargs.update({"channels": ",".join(channels)}) + else: + kwargs.update({"channels": channels}) + kwargs.update( + { + "filename": filename, + "filetype": filetype, + "initial_comment": initial_comment, + "thread_ts": thread_ts, + "title": title, + } + ) + if file: + if kwargs.get("filename") is None and isinstance(file, str): + # use the local filename if filename is missing + if kwargs.get("filename") is None: + kwargs["filename"] = file.split(os.path.sep)[-1] + return await self.api_call("files.upload", files={"file": file}, data=kwargs) + else: + kwargs["content"] = content + return await self.api_call("files.upload", data=kwargs) + + async def files_upload_v2( + self, + *, + # for sending a single file + filename: Optional[str] = None, # you can skip this only when sending along with content parameter + file: Optional[Union[str, bytes, IOBase, os.PathLike]] = None, + content: Optional[Union[str, bytes]] = None, + title: Optional[str] = None, + alt_txt: Optional[str] = None, + snippet_type: Optional[str] = None, + # To upload multiple files at a time + file_uploads: Optional[List[Dict[str, Any]]] = None, + channel: Optional[str] = None, + channels: Optional[List[str]] = None, + initial_comment: Optional[str] = None, + thread_ts: Optional[str] = None, + request_file_info: bool = True, # since v3.23, this flag is no longer necessary + **kwargs, + ) -> AsyncSlackResponse: + """This wrapper method provides an easy way to upload files using the following endpoints: + + - step1: https://docs.slack.dev/reference/methods/files.getUploadURLExternal + + - step2: "https://files.slack.com/upload/v1/..." URLs returned from files.getUploadURLExternal API + + - step3: https://docs.slack.dev/reference/methods/files.completeUploadExternal + and https://docs.slack.dev/reference/methods/files.info + + """ + if file is None and content is None and file_uploads is None: + raise e.SlackRequestError("Any of file, content, and file_uploads must be specified.") + if file is not None and content is not None: + raise e.SlackRequestError("You cannot specify both the file and the content argument.") + + # deprecated arguments: + filetype = kwargs.get("filetype") + + if filetype is not None: + warnings.warn("The filetype parameter is no longer supported. Please remove it from the arguments.") + + # step1: files.getUploadURLExternal per file + files: List[Dict[str, Any]] = [] + if file_uploads is not None: + for f in file_uploads: + files.append(_to_v2_file_upload_item(f)) + else: + f = _to_v2_file_upload_item( + { + "filename": filename, + "file": file, + "content": content, + "title": title, + "alt_txt": alt_txt, + "snippet_type": snippet_type, + } + ) + files.append(f) + + for f in files: + url_response = await self.files_getUploadURLExternal( + filename=f.get("filename"), # type: ignore[arg-type] + length=f.get("length"), # type: ignore[arg-type] + alt_txt=f.get("alt_txt"), + snippet_type=f.get("snippet_type"), + token=kwargs.get("token"), + ) + _validate_for_legacy_client(url_response) + f["file_id"] = url_response.get("file_id") # type: ignore[union-attr, unused-ignore] + f["upload_url"] = url_response.get("upload_url") # type: ignore[union-attr, unused-ignore] + + # step2: "https://files.slack.com/upload/v1/..." per file + for f in files: + upload_result = await self._upload_file( + url=f["upload_url"], + data=f["data"], + logger=self._logger, + timeout=self.timeout, + proxy=self.proxy, + ssl=self.ssl, + ) + if upload_result.status != 200: + status = upload_result.status + body = upload_result.body + message = ( + "Failed to upload a file " + f"(status: {status}, body: {body}, filename: {f.get('filename')}, title: {f.get('title')})" + ) + raise e.SlackRequestError(message) + + # step3: files.completeUploadExternal with all the sets of (file_id + title) + completion = await self.files_completeUploadExternal( + files=[{"id": f["file_id"], "title": f["title"]} for f in files], + channel_id=channel, + channels=channels, + initial_comment=initial_comment, + thread_ts=thread_ts, + **kwargs, + ) + if len(completion.get("files")) == 1: # type: ignore[arg-type, union-attr, unused-ignore] + completion.data["file"] = completion.get("files")[0] # type: ignore[index, union-attr, unused-ignore] + return completion + + async def files_getUploadURLExternal( + self, + *, + filename: str, + length: int, + alt_txt: Optional[str] = None, + snippet_type: Optional[str] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Gets a URL for an edge external upload. + https://docs.slack.dev/reference/methods/files.getUploadURLExternal + """ + kwargs.update( + { + "filename": filename, + "length": length, + "alt_txt": alt_txt, + "snippet_type": snippet_type, + } + ) + return await self.api_call("files.getUploadURLExternal", params=kwargs) + + async def files_completeUploadExternal( + self, + *, + files: List[Dict[str, str]], + channel_id: Optional[str] = None, + channels: Optional[List[str]] = None, + initial_comment: Optional[str] = None, + thread_ts: Optional[str] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Finishes an upload started with files.getUploadURLExternal. + https://docs.slack.dev/reference/methods/files.completeUploadExternal + """ + _files = [{k: v for k, v in f.items() if v is not None} for f in files] + kwargs.update( + { + "files": json.dumps(_files), + "channel_id": channel_id, + "initial_comment": initial_comment, + "thread_ts": thread_ts, + } + ) + if channels: + kwargs["channels"] = ",".join(channels) + return await self.api_call("files.completeUploadExternal", params=kwargs) + + async def functions_completeSuccess( + self, + *, + function_execution_id: str, + outputs: Dict[str, Any], + **kwargs, + ) -> AsyncSlackResponse: + """Signal the successful completion of a function + https://docs.slack.dev/reference/methods/functions.completeSuccess + """ + kwargs.update({"function_execution_id": function_execution_id, "outputs": json.dumps(outputs)}) + return await self.api_call("functions.completeSuccess", params=kwargs) + + async def functions_completeError( + self, + *, + function_execution_id: str, + error: str, + **kwargs, + ) -> AsyncSlackResponse: + """Signal the failure to execute a function + https://docs.slack.dev/reference/methods/functions.completeError + """ + kwargs.update({"function_execution_id": function_execution_id, "error": error}) + return await self.api_call("functions.completeError", params=kwargs) + + # -------------------------- + # Deprecated: groups.* + # You can use conversations.* APIs instead. + # https://docs.slack.dev/changelog/2020-01-deprecating-antecedents-to-the-conversations-api/ + # -------------------------- + + async def groups_archive( + self, + *, + channel: str, + **kwargs, + ) -> AsyncSlackResponse: + """Archives a private channel.""" + kwargs.update({"channel": channel}) + kwargs = _remove_none_values(kwargs) + return await self.api_call("groups.archive", json=kwargs) + + async def groups_create( + self, + *, + name: str, + **kwargs, + ) -> AsyncSlackResponse: + """Creates a private channel.""" + kwargs.update({"name": name}) + kwargs = _remove_none_values(kwargs) + return await self.api_call("groups.create", json=kwargs) + + async def groups_createChild( + self, + *, + channel: str, + **kwargs, + ) -> AsyncSlackResponse: + """Clones and archives a private channel.""" + kwargs.update({"channel": channel}) + return await self.api_call("groups.createChild", http_verb="GET", params=kwargs) + + async def groups_history( + self, + *, + channel: str, + **kwargs, + ) -> AsyncSlackResponse: + """Fetches history of messages and events from a private channel.""" + kwargs.update({"channel": channel}) + return await self.api_call("groups.history", http_verb="GET", params=kwargs) + + async def groups_info( + self, + *, + channel: str, + **kwargs, + ) -> AsyncSlackResponse: + """Gets information about a private channel.""" + kwargs.update({"channel": channel}) + return await self.api_call("groups.info", http_verb="GET", params=kwargs) + + async def groups_invite( + self, + *, + channel: str, + user: str, + **kwargs, + ) -> AsyncSlackResponse: + """Invites a user to a private channel.""" + kwargs.update({"channel": channel, "user": user}) + kwargs = _remove_none_values(kwargs) + return await self.api_call("groups.invite", json=kwargs) + + async def groups_kick( + self, + *, + channel: str, + user: str, + **kwargs, + ) -> AsyncSlackResponse: + """Removes a user from a private channel.""" + kwargs.update({"channel": channel, "user": user}) + kwargs = _remove_none_values(kwargs) + return await self.api_call("groups.kick", json=kwargs) + + async def groups_leave( + self, + *, + channel: str, + **kwargs, + ) -> AsyncSlackResponse: + """Leaves a private channel.""" + kwargs.update({"channel": channel}) + kwargs = _remove_none_values(kwargs) + return await self.api_call("groups.leave", json=kwargs) + + async def groups_list( + self, + **kwargs, + ) -> AsyncSlackResponse: + """Lists private channels that the calling user has access to.""" + return await self.api_call("groups.list", http_verb="GET", params=kwargs) + + async def groups_mark( + self, + *, + channel: str, + ts: str, + **kwargs, + ) -> AsyncSlackResponse: + """Sets the read cursor in a private channel.""" + kwargs.update({"channel": channel, "ts": ts}) + kwargs = _remove_none_values(kwargs) + return await self.api_call("groups.mark", json=kwargs) + + async def groups_open( + self, + *, + channel: str, + **kwargs, + ) -> AsyncSlackResponse: + """Opens a private channel.""" + kwargs.update({"channel": channel}) + kwargs = _remove_none_values(kwargs) + return await self.api_call("groups.open", json=kwargs) + + async def groups_rename( + self, + *, + channel: str, + name: str, + **kwargs, + ) -> AsyncSlackResponse: + """Renames a private channel.""" + kwargs.update({"channel": channel, "name": name}) + kwargs = _remove_none_values(kwargs) + return await self.api_call("groups.rename", json=kwargs) + + async def groups_replies( + self, + *, + channel: str, + thread_ts: str, + **kwargs, + ) -> AsyncSlackResponse: + """Retrieve a thread of messages posted to a private channel""" + kwargs.update({"channel": channel, "thread_ts": thread_ts}) + return await self.api_call("groups.replies", http_verb="GET", params=kwargs) + + async def groups_setPurpose( + self, + *, + channel: str, + purpose: str, + **kwargs, + ) -> AsyncSlackResponse: + """Sets the purpose for a private channel.""" + kwargs.update({"channel": channel, "purpose": purpose}) + kwargs = _remove_none_values(kwargs) + return await self.api_call("groups.setPurpose", json=kwargs) + + async def groups_setTopic( + self, + *, + channel: str, + topic: str, + **kwargs, + ) -> AsyncSlackResponse: + """Sets the topic for a private channel.""" + kwargs.update({"channel": channel, "topic": topic}) + kwargs = _remove_none_values(kwargs) + return await self.api_call("groups.setTopic", json=kwargs) + + async def groups_unarchive( + self, + *, + channel: str, + **kwargs, + ) -> AsyncSlackResponse: + """Unarchives a private channel.""" + kwargs.update({"channel": channel}) + kwargs = _remove_none_values(kwargs) + return await self.api_call("groups.unarchive", json=kwargs) + + # -------------------------- + # Deprecated: im.* + # You can use conversations.* APIs instead. + # https://docs.slack.dev/changelog/2020-01-deprecating-antecedents-to-the-conversations-api/ + # -------------------------- + + async def im_close( + self, + *, + channel: str, + **kwargs, + ) -> AsyncSlackResponse: + """Close a direct message channel.""" + kwargs.update({"channel": channel}) + kwargs = _remove_none_values(kwargs) + return await self.api_call("im.close", json=kwargs) + + async def im_history( + self, + *, + channel: str, + **kwargs, + ) -> AsyncSlackResponse: + """Fetches history of messages and events from direct message channel.""" + kwargs.update({"channel": channel}) + return await self.api_call("im.history", http_verb="GET", params=kwargs) + + async def im_list( + self, + **kwargs, + ) -> AsyncSlackResponse: + """Lists direct message channels for the calling user.""" + return await self.api_call("im.list", http_verb="GET", params=kwargs) + + async def im_mark( + self, + *, + channel: str, + ts: str, + **kwargs, + ) -> AsyncSlackResponse: + """Sets the read cursor in a direct message channel.""" + kwargs.update({"channel": channel, "ts": ts}) + kwargs = _remove_none_values(kwargs) + return await self.api_call("im.mark", json=kwargs) + + async def im_open( + self, + *, + user: str, + **kwargs, + ) -> AsyncSlackResponse: + """Opens a direct message channel.""" + kwargs.update({"user": user}) + kwargs = _remove_none_values(kwargs) + return await self.api_call("im.open", json=kwargs) + + async def im_replies( + self, + *, + channel: str, + thread_ts: str, + **kwargs, + ) -> AsyncSlackResponse: + """Retrieve a thread of messages posted to a direct message conversation""" + kwargs.update({"channel": channel, "thread_ts": thread_ts}) + return await self.api_call("im.replies", http_verb="GET", params=kwargs) + + # -------------------------- + + async def migration_exchange( + self, + *, + users: Union[str, Sequence[str]], + team_id: Optional[str] = None, + to_old: Optional[bool] = None, + **kwargs, + ) -> AsyncSlackResponse: + """For Enterprise Grid workspaces, map local user IDs to global user IDs + https://docs.slack.dev/reference/methods/migration.exchange + """ + if isinstance(users, (list, tuple)): + kwargs.update({"users": ",".join(users)}) + else: + kwargs.update({"users": users}) + kwargs.update({"team_id": team_id, "to_old": to_old}) + return await self.api_call("migration.exchange", http_verb="GET", params=kwargs) + + # -------------------------- + # Deprecated: mpim.* + # You can use conversations.* APIs instead. + # https://docs.slack.dev/changelog/2020-01-deprecating-antecedents-to-the-conversations-api/ + # -------------------------- + + async def mpim_close( + self, + *, + channel: str, + **kwargs, + ) -> AsyncSlackResponse: + """Closes a multiparty direct message channel.""" + kwargs.update({"channel": channel}) + kwargs = _remove_none_values(kwargs) + return await self.api_call("mpim.close", json=kwargs) + + async def mpim_history( + self, + *, + channel: str, + **kwargs, + ) -> AsyncSlackResponse: + """Fetches history of messages and events from a multiparty direct message.""" + kwargs.update({"channel": channel}) + return await self.api_call("mpim.history", http_verb="GET", params=kwargs) + + async def mpim_list( + self, + **kwargs, + ) -> AsyncSlackResponse: + """Lists multiparty direct message channels for the calling user.""" + return await self.api_call("mpim.list", http_verb="GET", params=kwargs) + + async def mpim_mark( + self, + *, + channel: str, + ts: str, + **kwargs, + ) -> AsyncSlackResponse: + """Sets the read cursor in a multiparty direct message channel.""" + kwargs.update({"channel": channel, "ts": ts}) + kwargs = _remove_none_values(kwargs) + return await self.api_call("mpim.mark", json=kwargs) + + async def mpim_open( + self, + *, + users: Union[str, Sequence[str]], + **kwargs, + ) -> AsyncSlackResponse: + """This method opens a multiparty direct message.""" + if isinstance(users, (list, tuple)): + kwargs.update({"users": ",".join(users)}) + else: + kwargs.update({"users": users}) + return await self.api_call("mpim.open", params=kwargs) + + async def mpim_replies( + self, + *, + channel: str, + thread_ts: str, + **kwargs, + ) -> AsyncSlackResponse: + """Retrieve a thread of messages posted to a direct message conversation from a + multiparty direct message. + """ + kwargs.update({"channel": channel, "thread_ts": thread_ts}) + return await self.api_call("mpim.replies", http_verb="GET", params=kwargs) + + # -------------------------- + + async def oauth_v2_access( + self, + *, + client_id: str, + client_secret: str, + # This field is required when processing the OAuth redirect URL requests + # while it's absent for token rotation + code: Optional[str] = None, + redirect_uri: Optional[str] = None, + # This field is required for token rotation + grant_type: Optional[str] = None, + # This field is required for token rotation + refresh_token: Optional[str] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Exchanges a temporary OAuth verifier code for an access token. + https://docs.slack.dev/reference/methods/oauth.v2.access + """ + if redirect_uri is not None: + kwargs.update({"redirect_uri": redirect_uri}) + if code is not None: + kwargs.update({"code": code}) + if grant_type is not None: + kwargs.update({"grant_type": grant_type}) + if refresh_token is not None: + kwargs.update({"refresh_token": refresh_token}) + return await self.api_call( + "oauth.v2.access", + data=kwargs, + auth={"client_id": client_id, "client_secret": client_secret}, + ) + + async def oauth_access( + self, + *, + client_id: str, + client_secret: str, + code: str, + redirect_uri: Optional[str] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Exchanges a temporary OAuth verifier code for an access token. + https://docs.slack.dev/reference/methods/oauth.access + """ + if redirect_uri is not None: + kwargs.update({"redirect_uri": redirect_uri}) + kwargs.update({"code": code}) + return await self.api_call( + "oauth.access", + data=kwargs, + auth={"client_id": client_id, "client_secret": client_secret}, + ) + + async def oauth_v2_exchange( + self, + *, + token: str, + client_id: str, + client_secret: str, + **kwargs, + ) -> AsyncSlackResponse: + """Exchanges a legacy access token for a new expiring access token and refresh token + https://docs.slack.dev/reference/methods/oauth.v2.exchange + """ + kwargs.update({"client_id": client_id, "client_secret": client_secret, "token": token}) + return await self.api_call("oauth.v2.exchange", params=kwargs) + + async def openid_connect_token( + self, + client_id: str, + client_secret: str, + code: Optional[str] = None, + redirect_uri: Optional[str] = None, + grant_type: Optional[str] = None, + refresh_token: Optional[str] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Exchanges a temporary OAuth verifier code for an access token for Sign in with Slack. + https://docs.slack.dev/reference/methods/openid.connect.token + """ + if redirect_uri is not None: + kwargs.update({"redirect_uri": redirect_uri}) + if code is not None: + kwargs.update({"code": code}) + if grant_type is not None: + kwargs.update({"grant_type": grant_type}) + if refresh_token is not None: + kwargs.update({"refresh_token": refresh_token}) + return await self.api_call( + "openid.connect.token", + data=kwargs, + auth={"client_id": client_id, "client_secret": client_secret}, + ) + + async def openid_connect_userInfo( + self, + **kwargs, + ) -> AsyncSlackResponse: + """Get the identity of a user who has authorized Sign in with Slack. + https://docs.slack.dev/reference/methods/openid.connect.userInfo + """ + return await self.api_call("openid.connect.userInfo", params=kwargs) + + async def pins_add( + self, + *, + channel: str, + timestamp: Optional[str] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Pins an item to a channel. + https://docs.slack.dev/reference/methods/pins.add + """ + kwargs.update({"channel": channel, "timestamp": timestamp}) + return await self.api_call("pins.add", params=kwargs) + + async def pins_list( + self, + *, + channel: str, + **kwargs, + ) -> AsyncSlackResponse: + """Lists items pinned to a channel. + https://docs.slack.dev/reference/methods/pins.list + """ + kwargs.update({"channel": channel}) + return await self.api_call("pins.list", http_verb="GET", params=kwargs) + + async def pins_remove( + self, + *, + channel: str, + timestamp: Optional[str] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Un-pins an item from a channel. + https://docs.slack.dev/reference/methods/pins.remove + """ + kwargs.update({"channel": channel, "timestamp": timestamp}) + return await self.api_call("pins.remove", params=kwargs) + + async def reactions_add( + self, + *, + channel: str, + name: str, + timestamp: str, + **kwargs, + ) -> AsyncSlackResponse: + """Adds a reaction to an item. + https://docs.slack.dev/reference/methods/reactions.add + """ + kwargs.update({"channel": channel, "name": name, "timestamp": timestamp}) + return await self.api_call("reactions.add", params=kwargs) + + async def reactions_get( + self, + *, + channel: Optional[str] = None, + file: Optional[str] = None, + file_comment: Optional[str] = None, + full: Optional[bool] = None, + timestamp: Optional[str] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Gets reactions for an item. + https://docs.slack.dev/reference/methods/reactions.get + """ + kwargs.update( + { + "channel": channel, + "file": file, + "file_comment": file_comment, + "full": full, + "timestamp": timestamp, + } + ) + return await self.api_call("reactions.get", http_verb="GET", params=kwargs) + + async def reactions_list( + self, + *, + count: Optional[int] = None, + cursor: Optional[str] = None, + full: Optional[bool] = None, + limit: Optional[int] = None, + page: Optional[int] = None, + team_id: Optional[str] = None, + user: Optional[str] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Lists reactions made by a user. + https://docs.slack.dev/reference/methods/reactions.list + """ + kwargs.update( + { + "count": count, + "cursor": cursor, + "full": full, + "limit": limit, + "page": page, + "team_id": team_id, + "user": user, + } + ) + return await self.api_call("reactions.list", http_verb="GET", params=kwargs) + + async def reactions_remove( + self, + *, + name: str, + channel: Optional[str] = None, + file: Optional[str] = None, + file_comment: Optional[str] = None, + timestamp: Optional[str] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Removes a reaction from an item. + https://docs.slack.dev/reference/methods/reactions.remove + """ + kwargs.update( + { + "name": name, + "channel": channel, + "file": file, + "file_comment": file_comment, + "timestamp": timestamp, + } + ) + return await self.api_call("reactions.remove", params=kwargs) + + async def reminders_add( + self, + *, + text: str, + time: str, + team_id: Optional[str] = None, + user: Optional[str] = None, + recurrence: Optional[str] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Creates a reminder. + https://docs.slack.dev/reference/methods/reminders.add + """ + kwargs.update( + { + "text": text, + "time": time, + "team_id": team_id, + "user": user, + "recurrence": recurrence, + } + ) + return await self.api_call("reminders.add", params=kwargs) + + async def reminders_complete( + self, + *, + reminder: str, + team_id: Optional[str] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Marks a reminder as complete. + https://docs.slack.dev/reference/methods/reminders.complete + """ + kwargs.update({"reminder": reminder, "team_id": team_id}) + return await self.api_call("reminders.complete", params=kwargs) + + async def reminders_delete( + self, + *, + reminder: str, + team_id: Optional[str] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Deletes a reminder. + https://docs.slack.dev/reference/methods/reminders.delete + """ + kwargs.update({"reminder": reminder, "team_id": team_id}) + return await self.api_call("reminders.delete", params=kwargs) + + async def reminders_info( + self, + *, + reminder: str, + team_id: Optional[str] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Gets information about a reminder. + https://docs.slack.dev/reference/methods/reminders.info + """ + kwargs.update({"reminder": reminder, "team_id": team_id}) + return await self.api_call("reminders.info", http_verb="GET", params=kwargs) + + async def reminders_list( + self, + *, + team_id: Optional[str] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Lists all reminders created by or for a given user. + https://docs.slack.dev/reference/methods/reminders.list + """ + kwargs.update({"team_id": team_id}) + return await self.api_call("reminders.list", http_verb="GET", params=kwargs) + + async def rtm_connect( + self, + *, + batch_presence_aware: Optional[bool] = None, + presence_sub: Optional[bool] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Starts a Real Time Messaging session. + https://docs.slack.dev/reference/methods/rtm.connect + """ + kwargs.update({"batch_presence_aware": batch_presence_aware, "presence_sub": presence_sub}) + return await self.api_call("rtm.connect", http_verb="GET", params=kwargs) + + async def rtm_start( + self, + *, + batch_presence_aware: Optional[bool] = None, + include_locale: Optional[bool] = None, + mpim_aware: Optional[bool] = None, + no_latest: Optional[bool] = None, + no_unreads: Optional[bool] = None, + presence_sub: Optional[bool] = None, + simple_latest: Optional[bool] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Starts a Real Time Messaging session. + https://docs.slack.dev/reference/methods/rtm.start + """ + kwargs.update( + { + "batch_presence_aware": batch_presence_aware, + "include_locale": include_locale, + "mpim_aware": mpim_aware, + "no_latest": no_latest, + "no_unreads": no_unreads, + "presence_sub": presence_sub, + "simple_latest": simple_latest, + } + ) + return await self.api_call("rtm.start", http_verb="GET", params=kwargs) + + async def search_all( + self, + *, + query: str, + count: Optional[int] = None, + highlight: Optional[bool] = None, + page: Optional[int] = None, + sort: Optional[str] = None, + sort_dir: Optional[str] = None, + team_id: Optional[str] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Searches for messages and files matching a query. + https://docs.slack.dev/reference/methods/search.all + """ + kwargs.update( + { + "query": query, + "count": count, + "highlight": highlight, + "page": page, + "sort": sort, + "sort_dir": sort_dir, + "team_id": team_id, + } + ) + return await self.api_call("search.all", http_verb="GET", params=kwargs) + + async def search_files( + self, + *, + query: str, + count: Optional[int] = None, + highlight: Optional[bool] = None, + page: Optional[int] = None, + sort: Optional[str] = None, + sort_dir: Optional[str] = None, + team_id: Optional[str] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Searches for files matching a query. + https://docs.slack.dev/reference/methods/search.files + """ + kwargs.update( + { + "query": query, + "count": count, + "highlight": highlight, + "page": page, + "sort": sort, + "sort_dir": sort_dir, + "team_id": team_id, + } + ) + return await self.api_call("search.files", http_verb="GET", params=kwargs) + + async def search_messages( + self, + *, + query: str, + count: Optional[int] = None, + cursor: Optional[str] = None, + highlight: Optional[bool] = None, + page: Optional[int] = None, + sort: Optional[str] = None, + sort_dir: Optional[str] = None, + team_id: Optional[str] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Searches for messages matching a query. + https://docs.slack.dev/reference/methods/search.messages + """ + kwargs.update( + { + "query": query, + "count": count, + "cursor": cursor, + "highlight": highlight, + "page": page, + "sort": sort, + "sort_dir": sort_dir, + "team_id": team_id, + } + ) + return await self.api_call("search.messages", http_verb="GET", params=kwargs) + + async def slackLists_access_delete( + self, + *, + list_id: str, + channel_ids: Optional[List[str]] = None, + user_ids: Optional[List[str]] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Revoke access to a List for specified entities. + https://docs.slack.dev/reference/methods/slackLists.access.delete + """ + kwargs.update({"list_id": list_id, "channel_ids": channel_ids, "user_ids": user_ids}) + kwargs = _remove_none_values(kwargs) + return await self.api_call("slackLists.access.delete", json=kwargs) + + async def slackLists_access_set( + self, + *, + list_id: str, + access_level: str, + channel_ids: Optional[List[str]] = None, + user_ids: Optional[List[str]] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Set the access level to a List for specified entities. + https://docs.slack.dev/reference/methods/slackLists.access.set + """ + kwargs.update({"list_id": list_id, "access_level": access_level, "channel_ids": channel_ids, "user_ids": user_ids}) + kwargs = _remove_none_values(kwargs) + return await self.api_call("slackLists.access.set", json=kwargs) + + async def slackLists_create( + self, + *, + name: str, + description_blocks: Optional[Union[str, Sequence[Union[Dict, RichTextBlock]]]] = None, + schema: Optional[List[Dict[str, Any]]] = None, + copy_from_list_id: Optional[str] = None, + include_copied_list_records: Optional[bool] = None, + todo_mode: Optional[bool] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Creates a List. + https://docs.slack.dev/reference/methods/slackLists.create + """ + kwargs.update( + { + "name": name, + "description_blocks": description_blocks, + "schema": schema, + "copy_from_list_id": copy_from_list_id, + "include_copied_list_records": include_copied_list_records, + "todo_mode": todo_mode, + } + ) + kwargs = _remove_none_values(kwargs) + return await self.api_call("slackLists.create", json=kwargs) + + async def slackLists_download_get( + self, + *, + list_id: str, + job_id: str, + **kwargs, + ) -> AsyncSlackResponse: + """Retrieve List download URL from an export job to download List contents. + https://docs.slack.dev/reference/methods/slackLists.download.get + """ + kwargs.update( + { + "list_id": list_id, + "job_id": job_id, + } + ) + kwargs = _remove_none_values(kwargs) + return await self.api_call("slackLists.download.get", json=kwargs) + + async def slackLists_download_start( + self, + *, + list_id: str, + include_archived: Optional[bool] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Initiate a job to export List contents. + https://docs.slack.dev/reference/methods/slackLists.download.start + """ + kwargs.update( + { + "list_id": list_id, + "include_archived": include_archived, + } + ) + kwargs = _remove_none_values(kwargs) + return await self.api_call("slackLists.download.start", json=kwargs) + + async def slackLists_items_create( + self, + *, + list_id: str, + duplicated_item_id: Optional[str] = None, + parent_item_id: Optional[str] = None, + initial_fields: Optional[List[Dict[str, Any]]] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Add a new item to an existing List. + https://docs.slack.dev/reference/methods/slackLists.items.create + """ + kwargs.update( + { + "list_id": list_id, + "duplicated_item_id": duplicated_item_id, + "parent_item_id": parent_item_id, + "initial_fields": initial_fields, + } + ) + kwargs = _remove_none_values(kwargs) + return await self.api_call("slackLists.items.create", json=kwargs) + + async def slackLists_items_delete( + self, + *, + list_id: str, + id: str, + **kwargs, + ) -> AsyncSlackResponse: + """Deletes an item from an existing List. + https://docs.slack.dev/reference/methods/slackLists.items.delete + """ + kwargs.update( + { + "list_id": list_id, + "id": id, + } + ) + kwargs = _remove_none_values(kwargs) + return await self.api_call("slackLists.items.delete", json=kwargs) + + async def slackLists_items_deleteMultiple( + self, + *, + list_id: str, + ids: List[str], + **kwargs, + ) -> AsyncSlackResponse: + """Deletes multiple items from an existing List. + https://docs.slack.dev/reference/methods/slackLists.items.deleteMultiple + """ + kwargs.update( + { + "list_id": list_id, + "ids": ids, + } + ) + kwargs = _remove_none_values(kwargs) + return await self.api_call("slackLists.items.deleteMultiple", json=kwargs) + + async def slackLists_items_info( + self, + *, + list_id: str, + id: str, + include_is_subscribed: Optional[bool] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Get a row from a List. + https://docs.slack.dev/reference/methods/slackLists.items.info + """ + kwargs.update( + { + "list_id": list_id, + "id": id, + "include_is_subscribed": include_is_subscribed, + } + ) + kwargs = _remove_none_values(kwargs) + return await self.api_call("slackLists.items.info", json=kwargs) + + async def slackLists_items_list( + self, + *, + list_id: str, + limit: Optional[int] = None, + cursor: Optional[str] = None, + archived: Optional[bool] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Get records from a List. + https://docs.slack.dev/reference/methods/slackLists.items.list + """ + kwargs.update( + { + "list_id": list_id, + "limit": limit, + "cursor": cursor, + "archived": archived, + } + ) + kwargs = _remove_none_values(kwargs) + return await self.api_call("slackLists.items.list", json=kwargs) + + async def slackLists_items_update( + self, + *, + list_id: str, + cells: List[Dict[str, Any]], + **kwargs, + ) -> AsyncSlackResponse: + """Updates cells in a List. + https://docs.slack.dev/reference/methods/slackLists.items.update + """ + kwargs.update( + { + "list_id": list_id, + "cells": cells, + } + ) + kwargs = _remove_none_values(kwargs) + return await self.api_call("slackLists.items.update", json=kwargs) + + async def slackLists_update( + self, + *, + id: str, + name: Optional[str] = None, + description_blocks: Optional[Union[str, Sequence[Union[Dict, RichTextBlock]]]] = None, + todo_mode: Optional[bool] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Update a List. + https://docs.slack.dev/reference/methods/slackLists.update + """ + kwargs.update( + { + "id": id, + "name": name, + "description_blocks": description_blocks, + "todo_mode": todo_mode, + } + ) + kwargs = _remove_none_values(kwargs) + return await self.api_call("slackLists.update", json=kwargs) + + async def stars_add( + self, + *, + channel: Optional[str] = None, + file: Optional[str] = None, + file_comment: Optional[str] = None, + timestamp: Optional[str] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Adds a star to an item. + https://docs.slack.dev/reference/methods/stars.add + """ + kwargs.update( + { + "channel": channel, + "file": file, + "file_comment": file_comment, + "timestamp": timestamp, + } + ) + return await self.api_call("stars.add", params=kwargs) + + async def stars_list( + self, + *, + count: Optional[int] = None, + cursor: Optional[str] = None, + limit: Optional[int] = None, + page: Optional[int] = None, + team_id: Optional[str] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Lists stars for a user. + https://docs.slack.dev/reference/methods/stars.list + """ + kwargs.update( + { + "count": count, + "cursor": cursor, + "limit": limit, + "page": page, + "team_id": team_id, + } + ) + return await self.api_call("stars.list", http_verb="GET", params=kwargs) + + async def stars_remove( + self, + *, + channel: Optional[str] = None, + file: Optional[str] = None, + file_comment: Optional[str] = None, + timestamp: Optional[str] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Removes a star from an item. + https://docs.slack.dev/reference/methods/stars.remove + """ + kwargs.update( + { + "channel": channel, + "file": file, + "file_comment": file_comment, + "timestamp": timestamp, + } + ) + return await self.api_call("stars.remove", params=kwargs) + + async def team_accessLogs( + self, + *, + before: Optional[Union[int, str]] = None, + count: Optional[Union[int, str]] = None, + page: Optional[Union[int, str]] = None, + team_id: Optional[str] = None, + cursor: Optional[str] = None, + limit: Optional[int] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Gets the access logs for the current team. + https://docs.slack.dev/reference/methods/team.accessLogs + """ + kwargs.update( + { + "before": before, + "count": count, + "page": page, + "team_id": team_id, + "cursor": cursor, + "limit": limit, + } + ) + return await self.api_call("team.accessLogs", http_verb="GET", params=kwargs) + + async def team_billableInfo( + self, + *, + team_id: Optional[str] = None, + user: Optional[str] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Gets billable users information for the current team. + https://docs.slack.dev/reference/methods/team.billableInfo + """ + kwargs.update({"team_id": team_id, "user": user}) + return await self.api_call("team.billableInfo", http_verb="GET", params=kwargs) + + async def team_billing_info( + self, + **kwargs, + ) -> AsyncSlackResponse: + """Reads a workspace's billing plan information. + https://docs.slack.dev/reference/methods/team.billing.info + """ + return await self.api_call("team.billing.info", params=kwargs) + + async def team_externalTeams_disconnect( + self, + *, + target_team: str, + **kwargs, + ) -> AsyncSlackResponse: + """Disconnects an external organization. + https://docs.slack.dev/reference/methods/team.externalTeams.disconnect + """ + kwargs.update( + { + "target_team": target_team, + } + ) + return await self.api_call("team.externalTeams.disconnect", params=kwargs) + + async def team_externalTeams_list( + self, + *, + connection_status_filter: Optional[str] = None, + slack_connect_pref_filter: Optional[Sequence[str]] = None, + sort_direction: Optional[str] = None, + sort_field: Optional[str] = None, + workspace_filter: Optional[Sequence[str]] = None, + cursor: Optional[str] = None, + limit: Optional[int] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Returns a list of all the external teams connected and details about the connection. + https://docs.slack.dev/reference/methods/team.externalTeams.list + """ + kwargs.update( + { + "connection_status_filter": connection_status_filter, + "sort_direction": sort_direction, + "sort_field": sort_field, + "cursor": cursor, + "limit": limit, + } + ) + if slack_connect_pref_filter is not None: + if isinstance(slack_connect_pref_filter, (list, tuple)): + kwargs.update({"slack_connect_pref_filter": ",".join(slack_connect_pref_filter)}) + else: + kwargs.update({"slack_connect_pref_filter": slack_connect_pref_filter}) + if workspace_filter is not None: + if isinstance(workspace_filter, (list, tuple)): + kwargs.update({"workspace_filter": ",".join(workspace_filter)}) + else: + kwargs.update({"workspace_filter": workspace_filter}) + return await self.api_call("team.externalTeams.list", http_verb="GET", params=kwargs) + + async def team_info( + self, + *, + team: Optional[str] = None, + domain: Optional[str] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Gets information about the current team. + https://docs.slack.dev/reference/methods/team.info + """ + kwargs.update({"team": team, "domain": domain}) + return await self.api_call("team.info", http_verb="GET", params=kwargs) + + async def team_integrationLogs( + self, + *, + app_id: Optional[str] = None, + change_type: Optional[str] = None, + count: Optional[Union[int, str]] = None, + page: Optional[Union[int, str]] = None, + service_id: Optional[str] = None, + team_id: Optional[str] = None, + user: Optional[str] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Gets the integration logs for the current team. + https://docs.slack.dev/reference/methods/team.integrationLogs + """ + kwargs.update( + { + "app_id": app_id, + "change_type": change_type, + "count": count, + "page": page, + "service_id": service_id, + "team_id": team_id, + "user": user, + } + ) + return await self.api_call("team.integrationLogs", http_verb="GET", params=kwargs) + + async def team_profile_get( + self, + *, + visibility: Optional[str] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Retrieve a team's profile. + https://docs.slack.dev/reference/methods/team.profile.get + """ + kwargs.update({"visibility": visibility}) + return await self.api_call("team.profile.get", http_verb="GET", params=kwargs) + + async def team_preferences_list( + self, + **kwargs, + ) -> AsyncSlackResponse: + """Retrieve a list of a workspace's team preferences. + https://docs.slack.dev/reference/methods/team.preferences.list + """ + return await self.api_call("team.preferences.list", params=kwargs) + + async def usergroups_create( + self, + *, + name: str, + channels: Optional[Union[str, Sequence[str]]] = None, + description: Optional[str] = None, + handle: Optional[str] = None, + include_count: Optional[bool] = None, + team_id: Optional[str] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Create a User Group + https://docs.slack.dev/reference/methods/usergroups.create + """ + kwargs.update( + { + "name": name, + "description": description, + "handle": handle, + "include_count": include_count, + "team_id": team_id, + } + ) + if isinstance(channels, (list, tuple)): + kwargs.update({"channels": ",".join(channels)}) + else: + kwargs.update({"channels": channels}) + return await self.api_call("usergroups.create", params=kwargs) + + async def usergroups_disable( + self, + *, + usergroup: str, + include_count: Optional[bool] = None, + team_id: Optional[str] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Disable an existing User Group + https://docs.slack.dev/reference/methods/usergroups.disable + """ + kwargs.update({"usergroup": usergroup, "include_count": include_count, "team_id": team_id}) + return await self.api_call("usergroups.disable", params=kwargs) + + async def usergroups_enable( + self, + *, + usergroup: str, + include_count: Optional[bool] = None, + team_id: Optional[str] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Enable a User Group + https://docs.slack.dev/reference/methods/usergroups.enable + """ + kwargs.update({"usergroup": usergroup, "include_count": include_count, "team_id": team_id}) + return await self.api_call("usergroups.enable", params=kwargs) + + async def usergroups_list( + self, + *, + include_count: Optional[bool] = None, + include_disabled: Optional[bool] = None, + include_users: Optional[bool] = None, + team_id: Optional[str] = None, + **kwargs, + ) -> AsyncSlackResponse: + """List all User Groups for a team + https://docs.slack.dev/reference/methods/usergroups.list + """ + kwargs.update( + { + "include_count": include_count, + "include_disabled": include_disabled, + "include_users": include_users, + "team_id": team_id, + } + ) + return await self.api_call("usergroups.list", http_verb="GET", params=kwargs) + + async def usergroups_update( + self, + *, + usergroup: str, + channels: Optional[Union[str, Sequence[str]]] = None, + description: Optional[str] = None, + handle: Optional[str] = None, + include_count: Optional[bool] = None, + name: Optional[str] = None, + team_id: Optional[str] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Update an existing User Group + https://docs.slack.dev/reference/methods/usergroups.update + """ + kwargs.update( + { + "usergroup": usergroup, + "description": description, + "handle": handle, + "include_count": include_count, + "name": name, + "team_id": team_id, + } + ) + if isinstance(channels, (list, tuple)): + kwargs.update({"channels": ",".join(channels)}) + else: + kwargs.update({"channels": channels}) + return await self.api_call("usergroups.update", params=kwargs) + + async def usergroups_users_list( + self, + *, + usergroup: str, + include_disabled: Optional[bool] = None, + team_id: Optional[str] = None, + **kwargs, + ) -> AsyncSlackResponse: + """List all users in a User Group + https://docs.slack.dev/reference/methods/usergroups.users.list + """ + kwargs.update( + { + "usergroup": usergroup, + "include_disabled": include_disabled, + "team_id": team_id, + } + ) + return await self.api_call("usergroups.users.list", http_verb="GET", params=kwargs) + + async def usergroups_users_update( + self, + *, + usergroup: str, + users: Union[str, Sequence[str]], + include_count: Optional[bool] = None, + team_id: Optional[str] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Update the list of users for a User Group + https://docs.slack.dev/reference/methods/usergroups.users.update + """ + kwargs.update( + { + "usergroup": usergroup, + "include_count": include_count, + "team_id": team_id, + } + ) + if isinstance(users, (list, tuple)): + kwargs.update({"users": ",".join(users)}) + else: + kwargs.update({"users": users}) + return await self.api_call("usergroups.users.update", params=kwargs) + + async def users_conversations( + self, + *, + cursor: Optional[str] = None, + exclude_archived: Optional[bool] = None, + limit: Optional[int] = None, + team_id: Optional[str] = None, + types: Optional[Union[str, Sequence[str]]] = None, + user: Optional[str] = None, + **kwargs, + ) -> AsyncSlackResponse: + """List conversations the calling user may access. + https://docs.slack.dev/reference/methods/users.conversations + """ + kwargs.update( + { + "cursor": cursor, + "exclude_archived": exclude_archived, + "limit": limit, + "team_id": team_id, + "user": user, + } + ) + if isinstance(types, (list, tuple)): + kwargs.update({"types": ",".join(types)}) + else: + kwargs.update({"types": types}) + return await self.api_call("users.conversations", http_verb="GET", params=kwargs) + + async def users_deletePhoto( + self, + **kwargs, + ) -> AsyncSlackResponse: + """Delete the user profile photo + https://docs.slack.dev/reference/methods/users.deletePhoto + """ + return await self.api_call("users.deletePhoto", http_verb="GET", params=kwargs) + + async def users_getPresence( + self, + *, + user: str, + **kwargs, + ) -> AsyncSlackResponse: + """Gets user presence information. + https://docs.slack.dev/reference/methods/users.getPresence + """ + kwargs.update({"user": user}) + return await self.api_call("users.getPresence", http_verb="GET", params=kwargs) + + async def users_identity( + self, + **kwargs, + ) -> AsyncSlackResponse: + """Get a user's identity. + https://docs.slack.dev/reference/methods/users.identity + """ + return await self.api_call("users.identity", http_verb="GET", params=kwargs) + + async def users_info( + self, + *, + user: str, + include_locale: Optional[bool] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Gets information about a user. + https://docs.slack.dev/reference/methods/users.info + """ + kwargs.update({"user": user, "include_locale": include_locale}) + return await self.api_call("users.info", http_verb="GET", params=kwargs) + + async def users_list( + self, + *, + cursor: Optional[str] = None, + include_locale: Optional[bool] = None, + limit: Optional[int] = None, + team_id: Optional[str] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Lists all users in a Slack team. + https://docs.slack.dev/reference/methods/users.list + """ + kwargs.update( + { + "cursor": cursor, + "include_locale": include_locale, + "limit": limit, + "team_id": team_id, + } + ) + return await self.api_call("users.list", http_verb="GET", params=kwargs) + + async def users_lookupByEmail( + self, + *, + email: str, + **kwargs, + ) -> AsyncSlackResponse: + """Find a user with an email address. + https://docs.slack.dev/reference/methods/users.lookupByEmail + """ + kwargs.update({"email": email}) + return await self.api_call("users.lookupByEmail", http_verb="GET", params=kwargs) + + async def users_setPhoto( + self, + *, + image: Union[str, IOBase], + crop_w: Optional[Union[int, str]] = None, + crop_x: Optional[Union[int, str]] = None, + crop_y: Optional[Union[int, str]] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Set the user profile photo + https://docs.slack.dev/reference/methods/users.setPhoto + """ + kwargs.update({"crop_w": crop_w, "crop_x": crop_x, "crop_y": crop_y}) + return await self.api_call("users.setPhoto", files={"image": image}, data=kwargs) + + async def users_setPresence( + self, + *, + presence: str, + **kwargs, + ) -> AsyncSlackResponse: + """Manually sets user presence. + https://docs.slack.dev/reference/methods/users.setPresence + """ + kwargs.update({"presence": presence}) + return await self.api_call("users.setPresence", params=kwargs) + + async def users_discoverableContacts_lookup( + self, + email: str, + **kwargs, + ) -> AsyncSlackResponse: + """Lookup an email address to see if someone is on Slack + https://docs.slack.dev/reference/methods/users.discoverableContacts.lookup + """ + kwargs.update({"email": email}) + return await self.api_call("users.discoverableContacts.lookup", params=kwargs) + + async def users_profile_get( + self, + *, + user: Optional[str] = None, + include_labels: Optional[bool] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Retrieves a user's profile information. + https://docs.slack.dev/reference/methods/users.profile.get + """ + kwargs.update({"user": user, "include_labels": include_labels}) + return await self.api_call("users.profile.get", http_verb="GET", params=kwargs) + + async def users_profile_set( + self, + *, + name: Optional[str] = None, + value: Optional[str] = None, + user: Optional[str] = None, + profile: Optional[Dict] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Set the profile information for a user. + https://docs.slack.dev/reference/methods/users.profile.set + """ + kwargs.update( + { + "name": name, + "profile": profile, + "user": user, + "value": value, + } + ) + kwargs = _remove_none_values(kwargs) + # NOTE: Intentionally using json for the "profile" parameter + return await self.api_call("users.profile.set", json=kwargs) + + async def views_open( + self, + *, + trigger_id: Optional[str] = None, + interactivity_pointer: Optional[str] = None, + view: Union[dict, View], + **kwargs, + ) -> AsyncSlackResponse: + """Open a view for a user. + https://docs.slack.dev/reference/methods/views.open + See https://docs.slack.dev/surfaces/modals/ for details. + """ + kwargs.update({"trigger_id": trigger_id, "interactivity_pointer": interactivity_pointer}) + if isinstance(view, View): + kwargs.update({"view": view.to_dict()}) + else: + kwargs.update({"view": view}) + kwargs = _remove_none_values(kwargs) + # NOTE: Intentionally using json for the "view" parameter + return await self.api_call("views.open", json=kwargs) + + async def views_push( + self, + *, + trigger_id: Optional[str] = None, + interactivity_pointer: Optional[str] = None, + view: Union[dict, View], + **kwargs, + ) -> AsyncSlackResponse: + """Push a view onto the stack of a root view. + Push a new view onto the existing view stack by passing a view + payload and a valid trigger_id generated from an interaction + within the existing modal. + Read the modals documentation (https://docs.slack.dev/surfaces/modals/) + to learn more about the lifecycle and intricacies of views. + https://docs.slack.dev/reference/methods/views.push + """ + kwargs.update({"trigger_id": trigger_id, "interactivity_pointer": interactivity_pointer}) + if isinstance(view, View): + kwargs.update({"view": view.to_dict()}) + else: + kwargs.update({"view": view}) + kwargs = _remove_none_values(kwargs) + # NOTE: Intentionally using json for the "view" parameter + return await self.api_call("views.push", json=kwargs) + + async def views_update( + self, + *, + view: Union[dict, View], + external_id: Optional[str] = None, + view_id: Optional[str] = None, + hash: Optional[str] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Update an existing view. + Update a view by passing a new view definition along with the + view_id returned in views.open or the external_id. + See the modals documentation (https://docs.slack.dev/surfaces/modals/#updating_views) + to learn more about updating views and avoiding race conditions with the hash argument. + https://docs.slack.dev/reference/methods/views.update + """ + if isinstance(view, View): + kwargs.update({"view": view.to_dict()}) + else: + kwargs.update({"view": view}) + if external_id: + kwargs.update({"external_id": external_id}) + elif view_id: + kwargs.update({"view_id": view_id}) + else: + raise e.SlackRequestError("Either view_id or external_id is required.") + kwargs.update({"hash": hash}) + kwargs = _remove_none_values(kwargs) + # NOTE: Intentionally using json for the "view" parameter + return await self.api_call("views.update", json=kwargs) + + async def views_publish( + self, + *, + user_id: str, + view: Union[dict, View], + hash: Optional[str] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Publish a static view for a User. + Create or update the view that comprises an + app's Home tab (https://docs.slack.dev/surfaces/app-home/) + https://docs.slack.dev/reference/methods/views.publish + """ + kwargs.update({"user_id": user_id, "hash": hash}) + if isinstance(view, View): + kwargs.update({"view": view.to_dict()}) + else: + kwargs.update({"view": view}) + kwargs = _remove_none_values(kwargs) + # NOTE: Intentionally using json for the "view" parameter + return await self.api_call("views.publish", json=kwargs) + + async def workflows_featured_add( + self, + *, + channel_id: str, + trigger_ids: Union[str, Sequence[str]], + **kwargs, + ) -> AsyncSlackResponse: + """Add featured workflows to a channel. + https://docs.slack.dev/reference/methods/workflows.featured.add + """ + kwargs.update({"channel_id": channel_id}) + if isinstance(trigger_ids, (list, tuple)): + kwargs.update({"trigger_ids": ",".join(trigger_ids)}) + else: + kwargs.update({"trigger_ids": trigger_ids}) + return await self.api_call("workflows.featured.add", params=kwargs) + + async def workflows_featured_list( + self, + *, + channel_ids: Union[str, Sequence[str]], + **kwargs, + ) -> AsyncSlackResponse: + """List the featured workflows for specified channels. + https://docs.slack.dev/reference/methods/workflows.featured.list + """ + if isinstance(channel_ids, (list, tuple)): + kwargs.update({"channel_ids": ",".join(channel_ids)}) + else: + kwargs.update({"channel_ids": channel_ids}) + return await self.api_call("workflows.featured.list", params=kwargs) + + async def workflows_featured_remove( + self, + *, + channel_id: str, + trigger_ids: Union[str, Sequence[str]], + **kwargs, + ) -> AsyncSlackResponse: + """Remove featured workflows from a channel. + https://docs.slack.dev/reference/methods/workflows.featured.remove + """ + kwargs.update({"channel_id": channel_id}) + if isinstance(trigger_ids, (list, tuple)): + kwargs.update({"trigger_ids": ",".join(trigger_ids)}) + else: + kwargs.update({"trigger_ids": trigger_ids}) + return await self.api_call("workflows.featured.remove", params=kwargs) + + async def workflows_featured_set( + self, + *, + channel_id: str, + trigger_ids: Union[str, Sequence[str]], + **kwargs, + ) -> AsyncSlackResponse: + """Set featured workflows for a channel. + https://docs.slack.dev/reference/methods/workflows.featured.set + """ + kwargs.update({"channel_id": channel_id}) + if isinstance(trigger_ids, (list, tuple)): + kwargs.update({"trigger_ids": ",".join(trigger_ids)}) + else: + kwargs.update({"trigger_ids": trigger_ids}) + return await self.api_call("workflows.featured.set", params=kwargs) + + async def workflows_stepCompleted( + self, + *, + workflow_step_execute_id: str, + outputs: Optional[dict] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Indicate a successful outcome of a workflow step's execution. + https://docs.slack.dev/reference/methods/workflows.stepCompleted + """ + kwargs.update({"workflow_step_execute_id": workflow_step_execute_id}) + if outputs is not None: + kwargs.update({"outputs": outputs}) + kwargs = _remove_none_values(kwargs) + # NOTE: Intentionally using json for the "outputs" parameter + return await self.api_call("workflows.stepCompleted", json=kwargs) + + async def workflows_stepFailed( + self, + *, + workflow_step_execute_id: str, + error: Dict[str, str], + **kwargs, + ) -> AsyncSlackResponse: + """Indicate an unsuccessful outcome of a workflow step's execution. + https://docs.slack.dev/reference/methods/workflows.stepFailed + """ + kwargs.update( + { + "workflow_step_execute_id": workflow_step_execute_id, + "error": error, + } + ) + kwargs = _remove_none_values(kwargs) + # NOTE: Intentionally using json for the "error" parameter + return await self.api_call("workflows.stepFailed", json=kwargs) + + async def workflows_updateStep( + self, + *, + workflow_step_edit_id: str, + inputs: Optional[Dict[str, Any]] = None, + outputs: Optional[List[Dict[str, str]]] = None, + **kwargs, + ) -> AsyncSlackResponse: + """Update the configuration for a workflow extension step. + https://docs.slack.dev/reference/methods/workflows.updateStep + """ + kwargs.update({"workflow_step_edit_id": workflow_step_edit_id}) + if inputs is not None: + kwargs.update({"inputs": inputs}) + if outputs is not None: + kwargs.update({"outputs": outputs}) + kwargs = _remove_none_values(kwargs) + # NOTE: Intentionally using json for the "inputs" / "outputs" parameters + return await self.api_call("workflows.updateStep", json=kwargs) diff --git a/slack_sdk/web/async_internal_utils.py b/slack_sdk/web/async_internal_utils.py new file mode 100644 index 000000000..01e214cb3 --- /dev/null +++ b/slack_sdk/web/async_internal_utils.py @@ -0,0 +1,219 @@ +import asyncio +import json +import logging +from asyncio import AbstractEventLoop +from logging import Logger +from typing import Optional, BinaryIO, Dict, Sequence, Union, List, Any + +import aiohttp +from aiohttp import ClientSession + +from slack_sdk.errors import SlackApiError +from slack_sdk.web.internal_utils import _build_unexpected_body_error_message + +from slack_sdk.http_retry.async_handler import AsyncRetryHandler +from slack_sdk.http_retry.request import HttpRequest as RetryHttpRequest +from slack_sdk.http_retry.response import HttpResponse as RetryHttpResponse +from slack_sdk.http_retry.state import RetryState + + +def _get_event_loop() -> AbstractEventLoop: + """Retrieves the event loop or creates a new one.""" + try: + return asyncio.get_event_loop() + except RuntimeError: + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + return loop + + +def _files_to_data(req_args: dict) -> Sequence[BinaryIO]: + open_files = [] + files = req_args.pop("files", None) + if files is not None: + for k, v in files.items(): + if isinstance(v, str): + f = open(v.encode("utf-8", "ignore"), "rb") + open_files.append(f) + req_args["data"].update({k: f}) + else: + req_args["data"].update({k: v}) + return open_files + + +async def _request_with_session( + *, + current_session: Optional[ClientSession], + timeout: int, + logger: Logger, + http_verb: str, + api_url: str, + req_args: dict, + # set the default to an empty array for legacy clients + retry_handlers: Optional[List[AsyncRetryHandler]] = None, +) -> Dict[str, Any]: + """Submit the HTTP request with the running session or a new session. + Returns: + A dictionary of the response data. + """ + retry_handlers = retry_handlers if retry_handlers is not None else [] + session = None + use_running_session = current_session and not current_session.closed + if use_running_session: + session = current_session + else: + session = aiohttp.ClientSession( + timeout=aiohttp.ClientTimeout(total=timeout), + auth=req_args.pop("auth", None), + ) + + last_error: Optional[Exception] = None + resp: Optional[Dict[str, Any]] = None + try: + retry_request = RetryHttpRequest( + method=http_verb, + url=api_url, + headers=req_args.get("headers", {}), + body_params=req_args.get("params"), + data=req_args.get("data"), + ) + + retry_state = RetryState() + counter_for_safety = 0 + while counter_for_safety < 100: + counter_for_safety += 1 + # If this is a retry, the next try started here. We can reset the flag. + retry_state.next_attempt_requested = False + retry_response: Optional[RetryHttpResponse] = None + + if logger.level <= logging.DEBUG: + + def convert_params(values: dict) -> dict: + if not values or not isinstance(values, dict): + return {} + return {k: ("(bytes)" if isinstance(v, bytes) else v) for k, v in values.items()} + + headers = { + k: "(redacted)" if k.lower() == "authorization" else v for k, v in req_args.get("headers", {}).items() + } + logger.debug( + f"Sending a request - url: {http_verb} {api_url}, " + f"params: {convert_params(req_args.get('params', 'n/a'))}, " + f"files: {convert_params(req_args.get('files', 'n/a'))}, " + f"data: {convert_params(req_args.get('data', 'n/a'))}, " + f"json: {convert_params(req_args.get('json', 'n/a'))}, " + f"proxy: {convert_params(req_args.get('proxy', 'n/a'))}, " + f"headers: {headers}" + ) + + try: + async with session.request(http_verb, api_url, **req_args) as res: # type: ignore[union-attr] + data: Union[dict, bytes, str] = {} + if res.content_type == "application/gzip": + # admin.analytics.getFile + data = await res.read() + retry_response = RetryHttpResponse( + status_code=res.status, + headers=res.headers, # type: ignore[arg-type] + data=data, + ) + elif res.content_type == "text/plain": + # https://files.slack.com/upload/v1/... + data = await res.text() + retry_response = RetryHttpResponse( + status_code=res.status, + headers=res.headers, # type: ignore[arg-type] + data=data, # type: ignore[arg-type] + ) + else: + try: + data = await res.json() + retry_response = RetryHttpResponse( + status_code=res.status, + headers=res.headers, # type: ignore[arg-type] + body=data, # type: ignore[arg-type] + ) + except aiohttp.ContentTypeError: + logger.debug(f"No response data returned from the following API call: {api_url}.") + retry_response = RetryHttpResponse( + status_code=res.status, + headers=res.headers, # type: ignore[arg-type] + ) + except json.decoder.JSONDecodeError: + try: + body: str = await res.text() + message = _build_unexpected_body_error_message(body) + raise SlackApiError(message, res) + except Exception as e: + raise SlackApiError( + f"Unexpectedly failed to read the response body: {str(e)}", + res, + ) + + if logger.level <= logging.DEBUG: + body = "(binary)" + if isinstance(data, dict) or isinstance(data, str): + body = data # type: ignore[assignment] + logger.debug( + "Received the following response - " + f"status: {res.status}, " + f"headers: {dict(res.headers)}, " + f"body: {body}" + ) + + for handler in retry_handlers: + if await handler.can_retry_async( + state=retry_state, + request=retry_request, + response=retry_response, + ): + if logger.level <= logging.DEBUG: + logger.info(f"A retry handler found: {type(handler).__name__} " f"for {http_verb} {api_url}") + await handler.prepare_for_next_attempt_async( + state=retry_state, + request=retry_request, + response=retry_response, + ) + break + + if retry_state.next_attempt_requested is False: + response = { + "data": data, + "headers": res.headers, + "status_code": res.status, + } + return response + + except Exception as e: + last_error = e + for handler in retry_handlers: + if await handler.can_retry_async( + state=retry_state, + request=retry_request, + response=retry_response, + error=e, + ): + if logger.level <= logging.DEBUG: + logger.info( + f"A retry handler found: {type(handler).__name__} " f"for {http_verb} {api_url} - {e}" + ) + await handler.prepare_for_next_attempt_async( + state=retry_state, + request=retry_request, + response=retry_response, + error=e, + ) + break + + if retry_state.next_attempt_requested is False: + raise last_error + + if resp is not None: + return resp + raise last_error # type: ignore[misc] + + finally: + if not use_running_session: + await session.close() # type: ignore[union-attr] + + return response diff --git a/slack_sdk/web/async_slack_response.py b/slack_sdk/web/async_slack_response.py new file mode 100644 index 000000000..2d2cbc680 --- /dev/null +++ b/slack_sdk/web/async_slack_response.py @@ -0,0 +1,203 @@ +"""A Python module for interacting and consuming responses from Slack.""" + +import logging +from typing import Any, Optional, TypeVar, Union, overload + +import slack_sdk.errors as e +from .internal_utils import _next_cursor_is_present + +T = TypeVar("T") + + +class AsyncSlackResponse: + """An iterable container of response data. + + Attributes: + data (dict): The json-encoded content of the response. Along + with the headers and status code information. + + Methods: + validate: Check if the response from Slack was successful. + get: Retrieves any key from the response data. + next: Retrieves the next portion of results, + if 'next_cursor' is present. + + Example: + ```python + import os + import slack + + client = slack.AsyncWebClient(token=os.environ['SLACK_API_TOKEN']) + + response1 = await client.auth_revoke(test='true') + assert not response1['revoked'] + + response2 = await client.auth_test() + assert response2.get('ok', False) + + users = [] + async for page in await client.users_list(limit=2): + users = users + page['members'] + ``` + + Note: + Some responses return collections of information + like channel and user lists. If they do it's likely + that you'll only receive a portion of results. This + object allows you to iterate over the response which + makes subsequent API requests until your code hits + 'break' or there are no more results to be found. + + Any attributes or methods prefixed with _underscores are + intended to be "private" internal use only. They may be changed or + removed at anytime. + """ + + def __init__( + self, + *, + client, # AsyncWebClient + http_verb: str, + api_url: str, + req_args: dict, + data: Union[dict, bytes], # data can be binary data + headers: dict, + status_code: int, + ): + self.http_verb = http_verb + self.api_url = api_url + self.req_args = req_args + self.data = data + self.headers = headers + self.status_code = status_code + self._initial_data = data + self._iteration = None # for __iter__ & __next__ + self._client = client + self._logger = logging.getLogger(__name__) + + def __str__(self): + """Return the Response data if object is converted to a string.""" + if isinstance(self.data, bytes): + raise ValueError("As the response.data is binary data, this operation is unsupported") + return f"{self.data}" + + def __contains__(self, key: str) -> bool: + return self.get(key) is not None + + def __getitem__(self, key): + """Retrieves any key from the data store. + + Note: + This is implemented so users can reference the + SlackResponse object like a dictionary. + e.g. response["ok"] + + Returns: + The value from data or None. + """ + if isinstance(self.data, bytes): + raise ValueError("As the response.data is binary data, this operation is unsupported") + if self.data is None: + raise ValueError("As the response.data is empty, this operation is unsupported") + return self.data.get(key, None) + + def __aiter__(self): + """Enables the ability to iterate over the response. + It's required async-for the iterator protocol. + + Note: + This enables Slack cursor-based pagination. + + Returns: + (AsyncSlackResponse) self + """ + if isinstance(self.data, bytes): + raise ValueError("As the response.data is binary data, this operation is unsupported") + self._iteration = 0 + self.data = self._initial_data + return self + + async def __anext__(self): + """Retrieves the next portion of results, if 'next_cursor' is present. + + Note: + Some responses return collections of information + like channel and user lists. If they do it's likely + that you'll only receive a portion of results. This + method allows you to iterate over the response until + your code hits 'break' or there are no more results + to be found. + + Returns: + (AsyncSlackResponse) self + With the new response data now attached to this object. + + Raises: + SlackApiError: If the request to the Slack API failed. + StopAsyncIteration: If 'next_cursor' is not present or empty. + """ + if isinstance(self.data, bytes): + raise ValueError("As the response.data is binary data, this operation is unsupported") + self._iteration += 1 + if self._iteration == 1: + return self + if _next_cursor_is_present(self.data): + params = self.req_args.get("params", {}) + if params is None: + params = {} + next_cursor = self.data.get("response_metadata", {}).get("next_cursor") or self.data.get("next_cursor") + params.update({"cursor": next_cursor}) + self.req_args.update({"params": params}) + + response = await self._client._request( + http_verb=self.http_verb, + api_url=self.api_url, + req_args=self.req_args, + ) + + self.data = response["data"] + self.headers = response["headers"] + self.status_code = response["status_code"] + return self.validate() + else: + raise StopAsyncIteration + + @overload + def get(self, key: str, default: None = None) -> Optional[Any]: + ... + + @overload + def get(self, key: str, default: T) -> T: + ... + + def get(self, key, default=None): + """Retrieves any key from the response data. + + Note: + This is implemented so users can reference the + SlackResponse object like a dictionary. + e.g. response.get("ok", False) + + Returns: + The value from data or the specified default. + """ + if isinstance(self.data, bytes): + raise ValueError("As the response.data is binary data, this operation is unsupported") + if self.data is None: + return None + return self.data.get(key, default) + + def validate(self): + """Check if the response from Slack was successful. + + Returns: + (AsyncSlackResponse) + This method returns it's own object. e.g. 'self' + + Raises: + SlackApiError: The request to the Slack API failed. + """ + if self.status_code == 200 and self.data and (isinstance(self.data, bytes) or self.data.get("ok", False)): + return self + msg = f"The request to the Slack API failed. (url: {self.api_url}, status: {self.status_code})" + raise e.SlackApiError(message=msg, response=self) diff --git a/slack_sdk/web/base_client.py b/slack_sdk/web/base_client.py new file mode 100644 index 000000000..1f5ad58c7 --- /dev/null +++ b/slack_sdk/web/base_client.py @@ -0,0 +1,640 @@ +"""A Python module for interacting with Slack's Web API.""" + +import copy +import hashlib +import hmac +import io +import json +import logging +import mimetypes +import urllib +import uuid +import warnings +from base64 import b64encode +from ssl import SSLContext +from typing import BinaryIO, Dict, List, Any +from typing import Optional, Union +from urllib.error import HTTPError +from urllib.parse import urlencode +from urllib.request import Request, urlopen, OpenerDirector, ProxyHandler, HTTPSHandler + +from slack_sdk.errors import SlackRequestError +from .deprecation import show_deprecation_warning_if_any +from .file_upload_v2_result import FileUploadV2Result +from .internal_utils import ( + convert_bool_to_0_or_1, + get_user_agent, + _get_url, + _build_req_args, + _build_unexpected_body_error_message, + _upload_file_via_v2_url, +) +from .slack_response import SlackResponse +from slack_sdk.http_retry import default_retry_handlers +from slack_sdk.http_retry.handler import RetryHandler +from slack_sdk.http_retry.request import HttpRequest as RetryHttpRequest +from slack_sdk.http_retry.response import HttpResponse as RetryHttpResponse +from slack_sdk.http_retry.state import RetryState +from slack_sdk.proxy_env_variable_loader import load_http_proxy_from_env + + +class BaseClient: + BASE_URL = "https://slack.com/api/" + + def __init__( + self, + token: Optional[str] = None, + base_url: str = BASE_URL, + timeout: int = 30, + ssl: Optional[SSLContext] = None, + proxy: Optional[str] = None, + headers: Optional[dict] = None, + user_agent_prefix: Optional[str] = None, + user_agent_suffix: Optional[str] = None, + # for Org-Wide App installation + team_id: Optional[str] = None, + logger: Optional[logging.Logger] = None, + retry_handlers: Optional[List[RetryHandler]] = None, + ): + self.token = None if token is None else token.strip() + """A string specifying an `xoxp-*` or `xoxb-*` token.""" + if not base_url.endswith("/"): + base_url += "/" + self.base_url = base_url + """A string representing the Slack API base URL. + Default is `'https://slack.com/api/'`.""" + self.timeout = timeout + """The maximum number of seconds the client will wait + to connect and receive a response from Slack. + Default is 30 seconds.""" + self.ssl = ssl + """An [`ssl.SSLContext`](https://docs.python.org/3/library/ssl.html#ssl.SSLContext) + instance, helpful for specifying your own custom + certificate chain.""" + self.proxy = proxy + """String representing a fully-qualified URL to a proxy through which + to route all requests to the Slack API. Even if this parameter + is not specified, if any of the following environment variables are + present, they will be loaded into this parameter: `HTTPS_PROXY`, + `https_proxy`, `HTTP_PROXY` or `http_proxy`.""" + self.headers = headers or {} + """`dict` representing additional request headers to attach to all requests.""" + self.headers["User-Agent"] = get_user_agent(user_agent_prefix, user_agent_suffix) + self.default_params = {} + if team_id is not None: + self.default_params["team_id"] = team_id + self._logger = logger if logger is not None else logging.getLogger(__name__) + + self.retry_handlers = retry_handlers if retry_handlers is not None else default_retry_handlers() + + if self.proxy is None or len(self.proxy.strip()) == 0: + env_variable = load_http_proxy_from_env(self._logger) + if env_variable is not None: + self.proxy = env_variable + + # ------------------------- + # accessors + + @property + def logger(self) -> logging.Logger: + """The logger this client uses.""" + return self._logger + + # ------------------------- + # api call + + def api_call( + self, + api_method: str, + *, + http_verb: str = "POST", + files: Optional[dict] = None, + data: Optional[dict] = None, + params: Optional[dict] = None, + json: Optional[dict] = None, + headers: Optional[dict] = None, + auth: Optional[dict] = None, + ) -> SlackResponse: + """Create a request and execute the API call to Slack. + + Args: + api_method (str): The target Slack API method. + e.g. 'chat.postMessage' + http_verb (str): HTTP Verb. e.g. 'POST' + files (dict): Files to multipart upload. + e.g. {image OR file: file_object OR file_path} + data: The body to attach to the request. If a dictionary is + provided, form-encoding will take place. + e.g. {'key1': 'value1', 'key2': 'value2'} + params (dict): The URL parameters to append to the URL. + e.g. {'key1': 'value1', 'key2': 'value2'} + json (dict): JSON for the body to attach to the request + (if files or data is not specified). + e.g. {'key1': 'value1', 'key2': 'value2'} + headers (dict): Additional request headers + auth (dict): A dictionary that consists of client_id and client_secret + + Returns: + (SlackResponse) + The server's response to an HTTP request. Data + from the response can be accessed like a dict. + If the response included 'next_cursor' it can + be iterated on to execute subsequent requests. + + Raises: + SlackApiError: The following Slack API call failed: + 'chat.postMessage'. + SlackRequestError: Json data can only be submitted as + POST requests. + """ + + api_url = _get_url(self.base_url, api_method) + headers = headers or {} + headers.update(self.headers) + req_args = _build_req_args( + token=self.token, + http_verb=http_verb, + files=files, # type: ignore[arg-type] + data=data, # type: ignore[arg-type] + default_params=self.default_params, + params=params, # type: ignore[arg-type] + json=json, # type: ignore[arg-type] + headers=headers, + auth=auth, # type: ignore[arg-type] + ssl=self.ssl, + proxy=self.proxy, + ) + + show_deprecation_warning_if_any(api_method) + return self._sync_send(api_url=api_url, req_args=req_args) + + # ================================================================= + # urllib based WebClient + # ================================================================= + + def _sync_send(self, api_url, req_args) -> SlackResponse: + params = req_args["params"] if "params" in req_args else None + data = req_args["data"] if "data" in req_args else None + files = req_args["files"] if "files" in req_args else None + _json = req_args["json"] if "json" in req_args else None + headers = req_args["headers"] if "headers" in req_args else None + token = params.get("token") if params and "token" in params else None + auth = req_args["auth"] if "auth" in req_args else None # Basic Auth for oauth.v2.access / oauth.access + if auth is not None: + headers = {} + if isinstance(auth, str): + headers["Authorization"] = auth + elif isinstance(auth, dict): + client_id, client_secret = auth["client_id"], auth["client_secret"] + value = b64encode(f"{client_id}:{client_secret}".encode("utf-8")).decode("ascii") + headers["Authorization"] = f"Basic {value}" + else: + self._logger.warning(f"As the auth: {auth}: {type(auth)} is unsupported, skipped") + + body_params = {} + if params: + body_params.update(params) + if data: + body_params.update(data) + + return self._urllib_api_call( + token=token, + url=api_url, + query_params={}, + body_params=body_params, + files=files, # type: ignore[arg-type] + json_body=_json, # type: ignore[arg-type] + additional_headers=headers, # type: ignore[arg-type] + ) + + def _request_for_pagination(self, api_url: str, req_args: Dict[str, Dict[str, Any]]) -> Dict[str, Any]: + """This method is supposed to be used only for SlackResponse pagination + + You can paginate using Python's for iterator as below: + + for response in client.conversations_list(limit=100): + # do something with each response here + """ + response = self._perform_urllib_http_request(url=api_url, args=req_args) + return { + "status_code": int(response["status"]), + "headers": dict(response["headers"]), + "data": json.loads(response["body"]), + } + + def _urllib_api_call( + self, + *, + token: Optional[str] = None, + url: str, + query_params: Dict[str, str], + json_body: Dict, + body_params: Dict[str, str], + files: Dict[str, io.BytesIO], + additional_headers: Dict[str, str], + ) -> SlackResponse: + """Performs a Slack API request and returns the result. + + Args: + token: Slack API Token (either bot token or user token) + url: Complete URL (e.g., https://slack.com/api/chat.postMessage) + query_params: Query string + json_body: JSON data structure (it's still a dict at this point), + if you give this argument, body_params and files will be skipped + body_params: Form body params + files: Files to upload + additional_headers: Request headers to append + + Returns: + API response + """ + files_to_close: List[BinaryIO] = [] + try: + # True/False -> "1"/"0" + query_params = convert_bool_to_0_or_1(query_params) # type: ignore[assignment] + body_params = convert_bool_to_0_or_1(body_params) # type: ignore[assignment] + + if self._logger.level <= logging.DEBUG: + + def convert_params(values: dict) -> dict: + if not values or not isinstance(values, dict): + return {} + return {k: ("(bytes)" if isinstance(v, bytes) else v) for k, v in values.items()} + + headers = {k: "(redacted)" if k.lower() == "authorization" else v for k, v in additional_headers.items()} + self._logger.debug( + f"Sending a request - url: {url}, " + f"query_params: {convert_params(query_params)}, " + f"body_params: {convert_params(body_params)}, " + f"files: {convert_params(files)}, " + f"json_body: {json_body}, " + f"headers: {headers}" + ) + + request_data = {} + if files is not None and isinstance(files, dict) and len(files) > 0: + if body_params: + for k, v in body_params.items(): + request_data.update({k: v}) + + for k, v in files.items(): # type: ignore[assignment] + if isinstance(v, str): + f: BinaryIO = open(v.encode("utf-8", "ignore"), "rb") + files_to_close.append(f) + request_data.update({k: f}) # type: ignore[dict-item] + elif isinstance(v, (bytearray, bytes)): + request_data.update({k: io.BytesIO(v)}) + else: + request_data.update({k: v}) + + request_headers = self._build_urllib_request_headers( + token=token or self.token, # type: ignore[arg-type] + has_json=json is not None, + has_files=files is not None, + additional_headers=additional_headers, + ) + request_args = { + "headers": request_headers, + "data": request_data, + "params": body_params, + "files": files, + "json": json_body, + } + if query_params: + q = urlencode(query_params) + url = f"{url}&{q}" if "?" in url else f"{url}?{q}" + + response = self._perform_urllib_http_request(url=url, args=request_args) # type: ignore[arg-type] + response_body = response.get("body", None) + response_body_data: Optional[Union[dict, bytes]] = response_body + if response_body is not None and not isinstance(response_body, bytes): + try: + response_body_data = json.loads(response["body"]) + except json.decoder.JSONDecodeError: + message = _build_unexpected_body_error_message(response.get("body", "")) + self._logger.error(f"Failed to decode Slack API response: {message}") + response_body_data = {"ok": False, "error": message} + + all_params: Dict[str, Any] = copy.copy(body_params) if body_params is not None else {} + if query_params: + all_params.update(query_params) + request_args["params"] = all_params # for backward-compatibility + + return SlackResponse( + client=self, + http_verb="POST", # you can use POST method for all the Web APIs + api_url=url, + req_args=request_args, + data=response_body_data, # type: ignore[arg-type] + headers=dict(response["headers"]), + status_code=response["status"], + ).validate() + finally: + for f in files_to_close: + if not f.closed: + f.close() + + def _perform_urllib_http_request(self, *, url: str, args: Dict[str, Dict[str, Any]]) -> Dict[str, Any]: + """Performs an HTTP request and parses the response. + + Args: + url: Complete URL (e.g., https://slack.com/api/chat.postMessage) + args: args has "headers", "data", "params", and "json" + "headers": Dict[str, str] + "data": Dict[str, Any] + "params": Dict[str, str], + "json": Dict[str, Any], + + Returns: + dict {status: int, headers: Headers, body: str} + """ + headers = args["headers"] + body: Optional[Union[bytes, str]] = None + if args["json"]: + body = json.dumps(args["json"]) + headers["Content-Type"] = "application/json;charset=utf-8" + elif args["data"]: + boundary = f"--------------{uuid.uuid4()}" + sep_boundary = b"\r\n--" + boundary.encode("ascii") + end_boundary = sep_boundary + b"--\r\n" + body_builder = io.BytesIO() + data = args["data"] + for key, value in data.items(): + readable = getattr(value, "readable", None) + if readable and value.readable(): + filename = "Uploaded file" + name_attr = getattr(value, "name", None) + if name_attr: + filename = name_attr.decode("utf-8") if isinstance(name_attr, bytes) else name_attr + if "filename" in data: + filename = data["filename"] + mimetype = mimetypes.guess_type(filename)[0] or "application/octet-stream" + title = ( + f'\r\nContent-Disposition: form-data; name="{key}"; filename="{filename}"\r\n' + + f"Content-Type: {mimetype}\r\n" + ) + value = value.read() + else: + title = f'\r\nContent-Disposition: form-data; name="{key}"\r\n' + value = str(value).encode("utf-8") + body_builder.write(sep_boundary) + body_builder.write(title.encode("utf-8")) + body_builder.write(b"\r\n") + body_builder.write(value) + + body_builder.write(end_boundary) + body = body_builder.getvalue() + headers["Content-Type"] = f"multipart/form-data; boundary={boundary}" + headers["Content-Length"] = len(body) + elif args["params"]: + body = urlencode(args["params"]) + headers["Content-Type"] = "application/x-www-form-urlencoded" + + if isinstance(body, str): + body = body.encode("utf-8") + + # NOTE: Intentionally ignore the `http_verb` here + # Slack APIs accepts any API method requests with POST methods + req = Request(method="POST", url=url, data=body, headers=headers) + resp = None + last_error = None + + retry_state = RetryState() + counter_for_safety = 0 + while counter_for_safety < 100: + counter_for_safety += 1 + # If this is a retry, the next try started here. We can reset the flag. + retry_state.next_attempt_requested = False + + try: + resp = self._perform_urllib_http_request_internal(url, req) + # The resp is a 200 OK response + if len(self.retry_handlers) > 0: + retry_request = RetryHttpRequest.from_urllib_http_request(req) + body_string = resp["body"] if isinstance(resp["body"], str) else None + body_bytes = body_string.encode("utf-8") if body_string is not None else resp["body"] + if body_string is not None and body_string.startswith("{"): + body = json.loads(body_string) + else: + body = {} # type: ignore[assignment] + retry_response = RetryHttpResponse( + status_code=resp["status"], + headers=resp["headers"], + body=body, # type: ignore[arg-type] + data=body_bytes, + ) + for handler in self.retry_handlers: + if handler.can_retry(state=retry_state, request=retry_request, response=retry_response): + if self._logger.level <= logging.DEBUG: + self._logger.info( + f"A retry handler found: {type(handler).__name__} for {req.method} {req.full_url}" + ) + handler.prepare_for_next_attempt( + state=retry_state, request=retry_request, response=retry_response + ) + break + if retry_state.next_attempt_requested is False: + return resp + + except HTTPError as e: + # As adding new values to HTTPError#headers can be ignored, building a new dict object here + response_headers = dict(e.headers.items()) + resp = {"status": e.code, "headers": response_headers} + if e.code == 429: + # for compatibility with aiohttp + if "retry-after" not in response_headers and "Retry-After" in response_headers: + response_headers["retry-after"] = response_headers["Retry-After"] + if "Retry-After" not in response_headers and "retry-after" in response_headers: + response_headers["Retry-After"] = response_headers["retry-after"] + + # read the response body here + charset = e.headers.get_content_charset() or "utf-8" + response_body: str = e.read().decode(charset) + resp["body"] = response_body + + # Try to find a retry handler for this error + retry_request = RetryHttpRequest.from_urllib_http_request(req) + retry_response = RetryHttpResponse( + status_code=e.code, + headers={k: [v] for k, v in response_headers.items()}, + data=response_body.encode("utf-8") if response_body is not None else None, + ) + for handler in self.retry_handlers: + if handler.can_retry( + state=retry_state, + request=retry_request, + response=retry_response, + error=e, + ): + if self._logger.level <= logging.DEBUG: + self._logger.info( + f"A retry handler found: {type(handler).__name__} for {req.method} {req.full_url} - {e}" + ) + handler.prepare_for_next_attempt( + state=retry_state, + request=retry_request, + response=retry_response, + error=e, + ) + break + + if retry_state.next_attempt_requested is False: + return resp + + except Exception as err: + last_error = err + self._logger.error(f"Failed to send a request to Slack API server: {err}") + + # Try to find a retry handler for this error + retry_request = RetryHttpRequest.from_urllib_http_request(req) + for handler in self.retry_handlers: + if handler.can_retry( + state=retry_state, + request=retry_request, + response=None, + error=err, + ): + if self._logger.level <= logging.DEBUG: + self._logger.info( + f"A retry handler found: {type(handler).__name__} for {req.method} {req.full_url} - {err}" + ) + handler.prepare_for_next_attempt( + state=retry_state, + request=retry_request, + response=None, + error=err, + ) + self._logger.info(f"Going to retry the same request: {req.method} {req.full_url}") + break + + if retry_state.next_attempt_requested is False: + raise err + + if resp is not None: + return resp + raise last_error # type: ignore[misc] + + def _perform_urllib_http_request_internal( + self, + url: str, + req: Request, + ) -> Dict[str, Any]: + # urllib not only opens http:// or https:// URLs, but also ftp:// and file://. + # With this it might be possible to open local files on the executing machine + # which might be a security risk if the URL to open can be manipulated by an external user. + # (BAN-B310) + if url.lower().startswith("http"): + opener: Optional[OpenerDirector] = None + if self.proxy is not None: + if isinstance(self.proxy, str): + opener = urllib.request.build_opener( + ProxyHandler({"http": self.proxy, "https": self.proxy}), + HTTPSHandler(context=self.ssl), + ) + else: + raise SlackRequestError(f"Invalid proxy detected: {self.proxy} must be a str value") + + if opener: + resp = opener.open(req, timeout=self.timeout) + else: + resp = urlopen(req, context=self.ssl, timeout=self.timeout) + if resp.headers.get_content_type() == "application/gzip": + # admin.analytics.getFile + body: bytes = resp.read() + if self._logger.level <= logging.DEBUG: + self._logger.debug( + "Received the following response - " + f"status: {resp.code}, " + f"headers: {dict(resp.headers)}, " + f"body: (binary)" + ) + return {"status": resp.code, "headers": resp.headers, "body": body} + + charset = resp.headers.get_content_charset() or "utf-8" + decoded_body: str = resp.read().decode(charset) # read the response body here + if self._logger.level <= logging.DEBUG: + self._logger.debug( + "Received the following response - " + f"status: {resp.code}, " + f"headers: {dict(resp.headers)}, " + f"body: {decoded_body}" + ) + return {"status": resp.code, "headers": resp.headers, "body": decoded_body} + raise SlackRequestError(f"Invalid URL detected: {url}") + + def _build_urllib_request_headers( + self, token: str, has_json: bool, has_files: bool, additional_headers: dict + ) -> Dict[str, str]: + headers = {"Content-Type": "application/x-www-form-urlencoded"} + headers.update(self.headers) + if token: + headers.update({"Authorization": "Bearer {}".format(token)}) + if additional_headers: + headers.update(additional_headers) + if has_json: + headers.update({"Content-Type": "application/json;charset=utf-8"}) + if has_files: + # will be set afterward + headers.pop("Content-Type", None) + return headers + + def _upload_file( + self, + *, + url: str, + data: bytes, + logger: logging.Logger, + timeout: int, + proxy: Optional[str], + ssl: Optional[SSLContext], + ) -> FileUploadV2Result: + """Upload a file using the issued upload URL""" + result = _upload_file_via_v2_url( + url=url, + data=data, + logger=logger, + timeout=timeout, + proxy=proxy, + ssl=ssl, + ) + return FileUploadV2Result( + status=result.get("status"), # type: ignore[arg-type] + body=result.get("body"), # type: ignore[arg-type] + ) + + # ================================================================= + + @staticmethod + def validate_slack_signature(*, signing_secret: str, data: str, timestamp: str, signature: str) -> bool: + """ + Slack creates a unique string for your app and shares it with you. Verify + requests from Slack with confidence by verifying signatures using your + signing secret. + + On each HTTP request that Slack sends, we add an X-Slack-Signature HTTP + header. The signature is created by combining the signing secret with the + body of the request we're sending using a standard HMAC-SHA256 keyed hash. + + https://docs.slack.dev/authentication/verifying-requests-from-slack/#how_to_make_a_request_signature_in_4_easy_steps__an_overview + + Args: + signing_secret: Your application's signing secret, available in the + Slack API dashboard + data: The raw body of the incoming request - no headers, just the body. + timestamp: from the 'X-Slack-Request-Timestamp' header + signature: from the 'X-Slack-Signature' header - the calculated signature + should match this. + + Returns: + True if signatures matches + """ + warnings.warn( + "As this method is deprecated since slackclient 2.6.0, " + "use `from slack.signature import SignatureVerifier` instead", + DeprecationWarning, + ) + format_req = str.encode(f"v0:{timestamp}:{data}") + encoded_secret = str.encode(signing_secret) + request_hash = hmac.new(encoded_secret, format_req, hashlib.sha256).hexdigest() + calculated_signature = f"v0={request_hash}" + return hmac.compare_digest(calculated_signature, signature) diff --git a/slack_sdk/web/chat_stream.py b/slack_sdk/web/chat_stream.py new file mode 100644 index 000000000..1a379c9cb --- /dev/null +++ b/slack_sdk/web/chat_stream.py @@ -0,0 +1,202 @@ +import json +import logging +from typing import TYPE_CHECKING, Dict, Optional, Sequence, Union + +import slack_sdk.errors as e +from slack_sdk.models.blocks.blocks import Block +from slack_sdk.models.metadata import Metadata +from slack_sdk.web.slack_response import SlackResponse + +if TYPE_CHECKING: + from slack_sdk import WebClient + + +class ChatStream: + """A helper class for streaming markdown text into a conversation using the chat streaming APIs. + + This class provides a convenient interface for the chat.startStream, chat.appendStream, and chat.stopStream API + methods, with automatic buffering and state management. + """ + + def __init__( + self, + client: "WebClient", + *, + channel: str, + logger: logging.Logger, + thread_ts: str, + buffer_size: int, + recipient_team_id: Optional[str] = None, + recipient_user_id: Optional[str] = None, + **kwargs, + ): + """Initialize a new ChatStream instance. + + The __init__ method creates a unique ChatStream instance that keeps track of one chat stream. + + Args: + client: The WebClient instance to use for API calls. + channel: An encoded ID that represents a channel, private group, or DM. + logger: A logging channel for outputs. + thread_ts: Provide another message's ts value to reply to. Streamed messages should always be replies to a user + request. + recipient_team_id: The encoded ID of the team the user receiving the streaming text belongs to. Required when + streaming to channels. + recipient_user_id: The encoded ID of the user to receive the streaming text. Required when streaming to channels. + buffer_size: The length of markdown_text to buffer in-memory before calling a method. Increasing this value + decreases the number of method calls made for the same amount of text, which is useful to avoid rate limits. + **kwargs: Additional arguments passed to the underlying API calls. + """ + self._client = client + self._logger = logger + self._token: Optional[str] = kwargs.pop("token", None) + self._stream_args = { + "channel": channel, + "thread_ts": thread_ts, + "recipient_team_id": recipient_team_id, + "recipient_user_id": recipient_user_id, + **kwargs, + } + self._buffer = "" + self._state = "starting" + self._stream_ts: Optional[str] = None + self._buffer_size = buffer_size + + def append( + self, + *, + markdown_text: str, + **kwargs, + ) -> Optional[SlackResponse]: + """Append to the stream. + + The "append" method appends to the chat stream being used. This method can be called multiple times. After the stream + is stopped this method cannot be called. + + Args: + markdown_text: Accepts message text formatted in markdown. Limit this field to 12,000 characters. This text is + what will be appended to the message received so far. + **kwargs: Additional arguments passed to the underlying API calls. + + Returns: + SlackResponse if the buffer was flushed, None if buffering. + + Raises: + SlackRequestError: If the stream is already completed. + + Example: + ```python + streamer = client.chat_stream( + channel="C0123456789", + thread_ts="1700000001.123456", + recipient_team_id="T0123456789", + recipient_user_id="U0123456789", + ) + streamer.append(markdown_text="**hello wo") + streamer.append(markdown_text="rld!**") + streamer.stop() + ``` + """ + if self._state == "completed": + raise e.SlackRequestError(f"Cannot append to stream: stream state is {self._state}") + if kwargs.get("token"): + self._token = kwargs.pop("token") + self._buffer += markdown_text + if len(self._buffer) >= self._buffer_size: + return self._flush_buffer(**kwargs) + details = { + "buffer_length": len(self._buffer), + "buffer_size": self._buffer_size, + "channel": self._stream_args.get("channel"), + "recipient_team_id": self._stream_args.get("recipient_team_id"), + "recipient_user_id": self._stream_args.get("recipient_user_id"), + "thread_ts": self._stream_args.get("thread_ts"), + } + self._logger.debug(f"ChatStream appended to buffer: {json.dumps(details)}") + return None + + def stop( + self, + *, + markdown_text: Optional[str] = None, + blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None, + metadata: Optional[Union[Dict, Metadata]] = None, + **kwargs, + ) -> SlackResponse: + """Stop the stream and finalize the message. + + Args: + blocks: A list of blocks that will be rendered at the bottom of the finalized message. + markdown_text: Accepts message text formatted in markdown. Limit this field to 12,000 characters. This text is + what will be appended to the message received so far. + metadata: JSON object with event_type and event_payload fields, presented as a URL-encoded string. Metadata you + post to Slack is accessible to any app or user who is a member of that workspace. + **kwargs: Additional arguments passed to the underlying API calls. + + Returns: + SlackResponse from the chat.stopStream API call. + + Raises: + SlackRequestError: If the stream is already completed. + + Example: + ```python + streamer = client.chat_stream( + channel="C0123456789", + thread_ts="1700000001.123456", + recipient_team_id="T0123456789", + recipient_user_id="U0123456789", + ) + streamer.append(markdown_text="**hello wo") + streamer.append(markdown_text="rld!**") + streamer.stop() + ``` + """ + if self._state == "completed": + raise e.SlackRequestError(f"Cannot stop stream: stream state is {self._state}") + if kwargs.get("token"): + self._token = kwargs.pop("token") + if markdown_text: + self._buffer += markdown_text + if not self._stream_ts: + response = self._client.chat_startStream( + **self._stream_args, + token=self._token, + ) + if not response.get("ts"): + raise e.SlackRequestError("Failed to stop stream: stream not started") + self._stream_ts = str(response["ts"]) + self._state = "in_progress" + response = self._client.chat_stopStream( + token=self._token, + channel=self._stream_args["channel"], + ts=self._stream_ts, + blocks=blocks, + markdown_text=self._buffer, + metadata=metadata, + **kwargs, + ) + self._state = "completed" + return response + + def _flush_buffer(self, **kwargs) -> SlackResponse: + """Flush the internal buffer by making appropriate API calls.""" + if not self._stream_ts: + response = self._client.chat_startStream( + **self._stream_args, + token=self._token, + **kwargs, + markdown_text=self._buffer, + ) + self._stream_ts = response.get("ts") + self._state = "in_progress" + else: + response = self._client.chat_appendStream( + token=self._token, + channel=self._stream_args["channel"], + ts=self._stream_ts, + **kwargs, + markdown_text=self._buffer, + ) + self._buffer = "" + return response diff --git a/slack_sdk/web/client.py b/slack_sdk/web/client.py new file mode 100644 index 000000000..dfa771832 --- /dev/null +++ b/slack_sdk/web/client.py @@ -0,0 +1,5947 @@ +"""A Python module for interacting with Slack's Web API.""" + +import json +import os +import warnings +from io import IOBase +from typing import Any, Dict, List, Optional, Sequence, Union + +import slack_sdk.errors as e +from slack_sdk.models.views import View +from slack_sdk.web.chat_stream import ChatStream + +from ..models.attachments import Attachment +from ..models.blocks import Block, RichTextBlock +from ..models.metadata import Metadata, EntityMetadata, EventAndEntityMetadata +from .base_client import BaseClient, SlackResponse +from .internal_utils import ( + _parse_web_class_objects, + _print_files_upload_v2_suggestion, + _remove_none_values, + _to_v2_file_upload_item, + _update_call_participants, + _validate_for_legacy_client, + _warn_if_message_text_content_is_missing, +) + + +class WebClient(BaseClient): + """A WebClient allows apps to communicate with the Slack Platform's Web API. + + https://docs.slack.dev/reference/methods + + The Slack Web API is an interface for querying information from + and enacting change in a Slack workspace. + + This client handles constructing and sending HTTP requests to Slack + as well as parsing any responses received into a `SlackResponse`. + + Attributes: + token (str): A string specifying an `xoxp-*` or `xoxb-*` token. + base_url (str): A string representing the Slack API base URL. + Default is `'https://slack.com/api/'` + timeout (int): The maximum number of seconds the client will wait + to connect and receive a response from Slack. + Default is 30 seconds. + ssl (SSLContext): An [`ssl.SSLContext`][1] instance, helpful for specifying + your own custom certificate chain. + proxy (str): String representing a fully-qualified URL to a proxy through + which to route all requests to the Slack API. Even if this parameter + is not specified, if any of the following environment variables are + present, they will be loaded into this parameter: `HTTPS_PROXY`, + `https_proxy`, `HTTP_PROXY` or `http_proxy`. + headers (dict): Additional request headers to attach to all requests. + + Methods: + `api_call`: Constructs a request and executes the API call to Slack. + + Example of recommended usage: + ```python + import os + from slack_sdk import WebClient + + client = WebClient(token=os.environ['SLACK_API_TOKEN']) + response = client.chat_postMessage( + channel='#random', + text="Hello world!") + assert response["ok"] + assert response["message"]["text"] == "Hello world!" + ``` + + Example manually creating an API request: + ```python + import os + from slack_sdk import WebClient + + client = WebClient(token=os.environ['SLACK_API_TOKEN']) + response = client.api_call( + api_method='chat.postMessage', + json={'channel': '#random','text': "Hello world!"} + ) + assert response["ok"] + assert response["message"]["text"] == "Hello world!" + ``` + + Note: + Any attributes or methods prefixed with _underscores are + intended to be "private" internal use only. They may be changed or + removed at anytime. + + [1]: https://docs.python.org/3/library/ssl.html#ssl.SSLContext + """ + + def admin_analytics_getFile( + self, + *, + type: str, + date: Optional[str] = None, + metadata_only: Optional[bool] = None, + **kwargs, + ) -> SlackResponse: + """Retrieve analytics data for a given date, presented as a compressed JSON file + https://docs.slack.dev/reference/methods/admin.analytics.getFile + """ + kwargs.update({"type": type}) + if date is not None: + kwargs.update({"date": date}) + if metadata_only is not None: + kwargs.update({"metadata_only": metadata_only}) + return self.api_call("admin.analytics.getFile", params=kwargs) + + def admin_apps_approve( + self, + *, + app_id: Optional[str] = None, + request_id: Optional[str] = None, + enterprise_id: Optional[str] = None, + team_id: Optional[str] = None, + **kwargs, + ) -> SlackResponse: + """Approve an app for installation on a workspace. + Either app_id or request_id is required. + These IDs can be obtained either directly via the app_requested event, + or by the admin.apps.requests.list method. + https://docs.slack.dev/reference/methods/admin.apps.approve + """ + if app_id: + kwargs.update({"app_id": app_id}) + elif request_id: + kwargs.update({"request_id": request_id}) + else: + raise e.SlackRequestError("The app_id or request_id argument must be specified.") + + kwargs.update( + { + "enterprise_id": enterprise_id, + "team_id": team_id, + } + ) + return self.api_call("admin.apps.approve", params=kwargs) + + def admin_apps_approved_list( + self, + *, + cursor: Optional[str] = None, + limit: Optional[int] = None, + enterprise_id: Optional[str] = None, + team_id: Optional[str] = None, + **kwargs, + ) -> SlackResponse: + """List approved apps for an org or workspace. + https://docs.slack.dev/reference/methods/admin.apps.approved.list + """ + kwargs.update( + { + "cursor": cursor, + "limit": limit, + "enterprise_id": enterprise_id, + "team_id": team_id, + } + ) + return self.api_call("admin.apps.approved.list", http_verb="GET", params=kwargs) + + def admin_apps_clearResolution( + self, + *, + app_id: str, + enterprise_id: Optional[str] = None, + team_id: Optional[str] = None, + **kwargs, + ) -> SlackResponse: + """Clear an app resolution + https://docs.slack.dev/reference/methods/admin.apps.clearResolution + """ + kwargs.update( + { + "app_id": app_id, + "enterprise_id": enterprise_id, + "team_id": team_id, + } + ) + return self.api_call("admin.apps.clearResolution", http_verb="POST", params=kwargs) + + def admin_apps_requests_cancel( + self, + *, + request_id: str, + enterprise_id: Optional[str] = None, + team_id: Optional[str] = None, + **kwargs, + ) -> SlackResponse: + """List app requests for a team/workspace. + https://docs.slack.dev/reference/methods/admin.apps.requests.cancel + """ + kwargs.update( + { + "request_id": request_id, + "enterprise_id": enterprise_id, + "team_id": team_id, + } + ) + return self.api_call("admin.apps.requests.cancel", http_verb="POST", params=kwargs) + + def admin_apps_requests_list( + self, + *, + cursor: Optional[str] = None, + limit: Optional[int] = None, + team_id: Optional[str] = None, + **kwargs, + ) -> SlackResponse: + """List app requests for a team/workspace. + https://docs.slack.dev/reference/methods/admin.apps.requests.list + """ + kwargs.update( + { + "cursor": cursor, + "limit": limit, + "team_id": team_id, + } + ) + return self.api_call("admin.apps.requests.list", http_verb="GET", params=kwargs) + + def admin_apps_restrict( + self, + *, + app_id: Optional[str] = None, + request_id: Optional[str] = None, + enterprise_id: Optional[str] = None, + team_id: Optional[str] = None, + **kwargs, + ) -> SlackResponse: + """Restrict an app for installation on a workspace. + Exactly one of the team_id or enterprise_id arguments is required, not both. + Either app_id or request_id is required. These IDs can be obtained either directly + via the app_requested event, or by the admin.apps.requests.list method. + https://docs.slack.dev/reference/methods/admin.apps.restrict + """ + if app_id: + kwargs.update({"app_id": app_id}) + elif request_id: + kwargs.update({"request_id": request_id}) + else: + raise e.SlackRequestError("The app_id or request_id argument must be specified.") + + kwargs.update( + { + "enterprise_id": enterprise_id, + "team_id": team_id, + } + ) + return self.api_call("admin.apps.restrict", params=kwargs) + + def admin_apps_restricted_list( + self, + *, + cursor: Optional[str] = None, + limit: Optional[int] = None, + enterprise_id: Optional[str] = None, + team_id: Optional[str] = None, + **kwargs, + ) -> SlackResponse: + """List restricted apps for an org or workspace. + https://docs.slack.dev/reference/methods/admin.apps.restricted.list + """ + kwargs.update( + { + "cursor": cursor, + "limit": limit, + "enterprise_id": enterprise_id, + "team_id": team_id, + } + ) + return self.api_call("admin.apps.restricted.list", http_verb="GET", params=kwargs) + + def admin_apps_uninstall( + self, + *, + app_id: str, + enterprise_id: Optional[str] = None, + team_ids: Optional[Union[str, Sequence[str]]] = None, + **kwargs, + ) -> SlackResponse: + """Uninstall an app from one or many workspaces, or an entire enterprise organization. + With an org-level token, enterprise_id or team_ids is required. + https://docs.slack.dev/reference/methods/admin.apps.uninstall + """ + kwargs.update({"app_id": app_id}) + if enterprise_id is not None: + kwargs.update({"enterprise_id": enterprise_id}) + if team_ids is not None: + if isinstance(team_ids, (list, tuple)): + kwargs.update({"team_ids": ",".join(team_ids)}) + else: + kwargs.update({"team_ids": team_ids}) + return self.api_call("admin.apps.uninstall", http_verb="POST", params=kwargs) + + def admin_apps_activities_list( + self, + *, + app_id: Optional[str] = None, + component_id: Optional[str] = None, + component_type: Optional[str] = None, + log_event_type: Optional[str] = None, + max_date_created: Optional[int] = None, + min_date_created: Optional[int] = None, + min_log_level: Optional[str] = None, + sort_direction: Optional[str] = None, + source: Optional[str] = None, + team_id: Optional[str] = None, + trace_id: Optional[str] = None, + cursor: Optional[str] = None, + limit: Optional[int] = None, + **kwargs, + ) -> SlackResponse: + """Get logs for a specified team/org + https://docs.slack.dev/reference/methods/admin.apps.activities.list + """ + kwargs.update( + { + "app_id": app_id, + "component_id": component_id, + "component_type": component_type, + "log_event_type": log_event_type, + "max_date_created": max_date_created, + "min_date_created": min_date_created, + "min_log_level": min_log_level, + "sort_direction": sort_direction, + "source": source, + "team_id": team_id, + "trace_id": trace_id, + "cursor": cursor, + "limit": limit, + } + ) + return self.api_call("admin.apps.activities.list", params=kwargs) + + def admin_apps_config_lookup( + self, + *, + app_ids: Union[str, Sequence[str]], + **kwargs, + ) -> SlackResponse: + """Look up the app config for connectors by their IDs + https://docs.slack.dev/reference/methods/admin.apps.config.lookup + """ + if isinstance(app_ids, (list, tuple)): + kwargs.update({"app_ids": ",".join(app_ids)}) + else: + kwargs.update({"app_ids": app_ids}) + return self.api_call("admin.apps.config.lookup", params=kwargs) + + def admin_apps_config_set( + self, + *, + app_id: str, + domain_restrictions: Optional[Dict[str, Any]] = None, + workflow_auth_strategy: Optional[str] = None, + **kwargs, + ) -> SlackResponse: + """Set the app config for a connector + https://docs.slack.dev/reference/methods/admin.apps.config.set + """ + kwargs.update( + { + "app_id": app_id, + "workflow_auth_strategy": workflow_auth_strategy, + } + ) + if domain_restrictions is not None: + kwargs.update({"domain_restrictions": json.dumps(domain_restrictions)}) + return self.api_call("admin.apps.config.set", params=kwargs) + + def admin_auth_policy_getEntities( + self, + *, + policy_name: str, + cursor: Optional[str] = None, + entity_type: Optional[str] = None, + limit: Optional[int] = None, + **kwargs, + ) -> SlackResponse: + """Fetch all the entities assigned to a particular authentication policy by name. + https://docs.slack.dev/reference/methods/admin.auth.policy.getEntities + """ + kwargs.update({"policy_name": policy_name}) + if cursor is not None: + kwargs.update({"cursor": cursor}) + if entity_type is not None: + kwargs.update({"entity_type": entity_type}) + if limit is not None: + kwargs.update({"limit": limit}) + return self.api_call("admin.auth.policy.getEntities", http_verb="POST", params=kwargs) + + def admin_auth_policy_assignEntities( + self, + *, + entity_ids: Union[str, Sequence[str]], + policy_name: str, + entity_type: str, + **kwargs, + ) -> SlackResponse: + """Assign entities to a particular authentication policy. + https://docs.slack.dev/reference/methods/admin.auth.policy.assignEntities + """ + if isinstance(entity_ids, (list, tuple)): + kwargs.update({"entity_ids": ",".join(entity_ids)}) + else: + kwargs.update({"entity_ids": entity_ids}) + kwargs.update({"policy_name": policy_name}) + kwargs.update({"entity_type": entity_type}) + return self.api_call("admin.auth.policy.assignEntities", http_verb="POST", params=kwargs) + + def admin_auth_policy_removeEntities( + self, + *, + entity_ids: Union[str, Sequence[str]], + policy_name: str, + entity_type: str, + **kwargs, + ) -> SlackResponse: + """Remove specified entities from a specified authentication policy. + https://docs.slack.dev/reference/methods/admin.auth.policy.removeEntities + """ + if isinstance(entity_ids, (list, tuple)): + kwargs.update({"entity_ids": ",".join(entity_ids)}) + else: + kwargs.update({"entity_ids": entity_ids}) + kwargs.update({"policy_name": policy_name}) + kwargs.update({"entity_type": entity_type}) + return self.api_call("admin.auth.policy.removeEntities", http_verb="POST", params=kwargs) + + def admin_conversations_createForObjects( + self, + *, + object_id: str, + salesforce_org_id: str, + invite_object_team: Optional[bool] = None, + **kwargs, + ) -> SlackResponse: + """Create a Salesforce channel for the corresponding object provided. + https://docs.slack.dev/reference/methods/admin.conversations.createForObjects + """ + kwargs.update( + {"object_id": object_id, "salesforce_org_id": salesforce_org_id, "invite_object_team": invite_object_team} + ) + return self.api_call("admin.conversations.createForObjects", params=kwargs) + + def admin_conversations_linkObjects( + self, + *, + channel: str, + record_id: str, + salesforce_org_id: str, + **kwargs, + ) -> SlackResponse: + """Link a Salesforce record to a channel. + https://docs.slack.dev/reference/methods/admin.conversations.linkObjects + """ + kwargs.update( + { + "channel": channel, + "record_id": record_id, + "salesforce_org_id": salesforce_org_id, + } + ) + return self.api_call("admin.conversations.linkObjects", params=kwargs) + + def admin_conversations_unlinkObjects( + self, + *, + channel: str, + new_name: str, + **kwargs, + ) -> SlackResponse: + """Unlink a Salesforce record from a channel. + https://docs.slack.dev/reference/methods/admin.conversations.unlinkObjects + """ + kwargs.update( + { + "channel": channel, + "new_name": new_name, + } + ) + return self.api_call("admin.conversations.unlinkObjects", params=kwargs) + + def admin_barriers_create( + self, + *, + barriered_from_usergroup_ids: Union[str, Sequence[str]], + primary_usergroup_id: str, + restricted_subjects: Union[str, Sequence[str]], + **kwargs, + ) -> SlackResponse: + """Create an Information Barrier + https://docs.slack.dev/reference/methods/admin.barriers.create + """ + kwargs.update({"primary_usergroup_id": primary_usergroup_id}) + if isinstance(barriered_from_usergroup_ids, (list, tuple)): + kwargs.update({"barriered_from_usergroup_ids": ",".join(barriered_from_usergroup_ids)}) + else: + kwargs.update({"barriered_from_usergroup_ids": barriered_from_usergroup_ids}) + if isinstance(restricted_subjects, (list, tuple)): + kwargs.update({"restricted_subjects": ",".join(restricted_subjects)}) + else: + kwargs.update({"restricted_subjects": restricted_subjects}) + return self.api_call("admin.barriers.create", http_verb="POST", params=kwargs) + + def admin_barriers_delete( + self, + *, + barrier_id: str, + **kwargs, + ) -> SlackResponse: + """Delete an existing Information Barrier + https://docs.slack.dev/reference/methods/admin.barriers.delete + """ + kwargs.update({"barrier_id": barrier_id}) + return self.api_call("admin.barriers.delete", http_verb="POST", params=kwargs) + + def admin_barriers_update( + self, + *, + barrier_id: str, + barriered_from_usergroup_ids: Union[str, Sequence[str]], + primary_usergroup_id: str, + restricted_subjects: Union[str, Sequence[str]], + **kwargs, + ) -> SlackResponse: + """Update an existing Information Barrier + https://docs.slack.dev/reference/methods/admin.barriers.update + """ + kwargs.update({"barrier_id": barrier_id, "primary_usergroup_id": primary_usergroup_id}) + if isinstance(barriered_from_usergroup_ids, (list, tuple)): + kwargs.update({"barriered_from_usergroup_ids": ",".join(barriered_from_usergroup_ids)}) + else: + kwargs.update({"barriered_from_usergroup_ids": barriered_from_usergroup_ids}) + if isinstance(restricted_subjects, (list, tuple)): + kwargs.update({"restricted_subjects": ",".join(restricted_subjects)}) + else: + kwargs.update({"restricted_subjects": restricted_subjects}) + return self.api_call("admin.barriers.update", http_verb="POST", params=kwargs) + + def admin_barriers_list( + self, + *, + cursor: Optional[str] = None, + limit: Optional[int] = None, + **kwargs, + ) -> SlackResponse: + """Get all Information Barriers for your organization + https://docs.slack.dev/reference/methods/admin.barriers.list""" + kwargs.update( + { + "cursor": cursor, + "limit": limit, + } + ) + return self.api_call("admin.barriers.list", http_verb="GET", params=kwargs) + + def admin_conversations_create( + self, + *, + is_private: bool, + name: str, + description: Optional[str] = None, + org_wide: Optional[bool] = None, + team_id: Optional[str] = None, + **kwargs, + ) -> SlackResponse: + """Create a public or private channel-based conversation. + https://docs.slack.dev/reference/methods/admin.conversations.create + """ + kwargs.update( + { + "is_private": is_private, + "name": name, + "description": description, + "org_wide": org_wide, + "team_id": team_id, + } + ) + return self.api_call("admin.conversations.create", params=kwargs) + + def admin_conversations_delete( + self, + *, + channel_id: str, + **kwargs, + ) -> SlackResponse: + """Delete a public or private channel. + https://docs.slack.dev/reference/methods/admin.conversations.delete + """ + kwargs.update({"channel_id": channel_id}) + return self.api_call("admin.conversations.delete", params=kwargs) + + def admin_conversations_invite( + self, + *, + channel_id: str, + user_ids: Union[str, Sequence[str]], + **kwargs, + ) -> SlackResponse: + """Invite a user to a public or private channel. + https://docs.slack.dev/reference/methods/admin.conversations.invite + """ + kwargs.update({"channel_id": channel_id}) + if isinstance(user_ids, (list, tuple)): + kwargs.update({"user_ids": ",".join(user_ids)}) + else: + kwargs.update({"user_ids": user_ids}) + # NOTE: the endpoint is unable to handle Content-Type: application/json as of Sep 3, 2020. + return self.api_call("admin.conversations.invite", params=kwargs) + + def admin_conversations_archive( + self, + *, + channel_id: str, + **kwargs, + ) -> SlackResponse: + """Archive a public or private channel. + https://docs.slack.dev/reference/methods/admin.conversations.archive + """ + kwargs.update({"channel_id": channel_id}) + return self.api_call("admin.conversations.archive", params=kwargs) + + def admin_conversations_unarchive( + self, + *, + channel_id: str, + **kwargs, + ) -> SlackResponse: + """Unarchive a public or private channel. + https://docs.slack.dev/reference/methods/admin.conversations.archive + """ + kwargs.update({"channel_id": channel_id}) + return self.api_call("admin.conversations.unarchive", params=kwargs) + + def admin_conversations_rename( + self, + *, + channel_id: str, + name: str, + **kwargs, + ) -> SlackResponse: + """Rename a public or private channel. + https://docs.slack.dev/reference/methods/admin.conversations.rename + """ + kwargs.update({"channel_id": channel_id, "name": name}) + return self.api_call("admin.conversations.rename", params=kwargs) + + def admin_conversations_search( + self, + *, + cursor: Optional[str] = None, + limit: Optional[int] = None, + query: Optional[str] = None, + search_channel_types: Optional[Union[str, Sequence[str]]] = None, + sort: Optional[str] = None, + sort_dir: Optional[str] = None, + team_ids: Optional[Union[str, Sequence[str]]] = None, + **kwargs, + ) -> SlackResponse: + """Search for public or private channels in an Enterprise organization. + https://docs.slack.dev/reference/methods/admin.conversations.search + """ + kwargs.update( + { + "cursor": cursor, + "limit": limit, + "query": query, + "sort": sort, + "sort_dir": sort_dir, + } + ) + + if isinstance(search_channel_types, (list, tuple)): + kwargs.update({"search_channel_types": ",".join(search_channel_types)}) + else: + kwargs.update({"search_channel_types": search_channel_types}) + + if isinstance(team_ids, (list, tuple)): + kwargs.update({"team_ids": ",".join(team_ids)}) + else: + kwargs.update({"team_ids": team_ids}) + + return self.api_call("admin.conversations.search", params=kwargs) + + def admin_conversations_convertToPrivate( + self, + *, + channel_id: str, + **kwargs, + ) -> SlackResponse: + """Convert a public channel to a private channel. + https://docs.slack.dev/reference/methods/admin.conversations.convertToPrivate + """ + kwargs.update({"channel_id": channel_id}) + return self.api_call("admin.conversations.convertToPrivate", params=kwargs) + + def admin_conversations_convertToPublic( + self, + *, + channel_id: str, + **kwargs, + ) -> SlackResponse: + """Convert a privte channel to a public channel. + https://docs.slack.dev/reference/methods/admin.conversations.convertToPublic + """ + kwargs.update({"channel_id": channel_id}) + return self.api_call("admin.conversations.convertToPublic", params=kwargs) + + def admin_conversations_setConversationPrefs( + self, + *, + channel_id: str, + prefs: Union[str, Dict[str, str]], + **kwargs, + ) -> SlackResponse: + """Set the posting permissions for a public or private channel. + https://docs.slack.dev/reference/methods/admin.conversations.setConversationPrefs + """ + kwargs.update({"channel_id": channel_id}) + if isinstance(prefs, dict): + kwargs.update({"prefs": json.dumps(prefs)}) + else: + kwargs.update({"prefs": prefs}) + return self.api_call("admin.conversations.setConversationPrefs", params=kwargs) + + def admin_conversations_getConversationPrefs( + self, + *, + channel_id: str, + **kwargs, + ) -> SlackResponse: + """Get conversation preferences for a public or private channel. + https://docs.slack.dev/reference/methods/admin.conversations.getConversationPrefs + """ + kwargs.update({"channel_id": channel_id}) + return self.api_call("admin.conversations.getConversationPrefs", params=kwargs) + + def admin_conversations_disconnectShared( + self, + *, + channel_id: str, + leaving_team_ids: Optional[Union[str, Sequence[str]]] = None, + **kwargs, + ) -> SlackResponse: + """Disconnect a connected channel from one or more workspaces. + https://docs.slack.dev/reference/methods/admin.conversations.disconnectShared + """ + kwargs.update({"channel_id": channel_id}) + if isinstance(leaving_team_ids, (list, tuple)): + kwargs.update({"leaving_team_ids": ",".join(leaving_team_ids)}) + else: + kwargs.update({"leaving_team_ids": leaving_team_ids}) + return self.api_call("admin.conversations.disconnectShared", params=kwargs) + + def admin_conversations_lookup( + self, + *, + last_message_activity_before: int, + team_ids: Union[str, Sequence[str]], + cursor: Optional[str] = None, + limit: Optional[int] = None, + max_member_count: Optional[int] = None, + **kwargs, + ) -> SlackResponse: + """Returns channels on the given team using the filters. + https://docs.slack.dev/reference/methods/admin.conversations.lookup + """ + kwargs.update( + { + "last_message_activity_before": last_message_activity_before, + "cursor": cursor, + "limit": limit, + "max_member_count": max_member_count, + } + ) + if isinstance(team_ids, (list, tuple)): + kwargs.update({"team_ids": ",".join(team_ids)}) + else: + kwargs.update({"team_ids": team_ids}) + return self.api_call("admin.conversations.lookup", params=kwargs) + + def admin_conversations_ekm_listOriginalConnectedChannelInfo( + self, + *, + channel_ids: Optional[Union[str, Sequence[str]]] = None, + cursor: Optional[str] = None, + limit: Optional[int] = None, + team_ids: Optional[Union[str, Sequence[str]]] = None, + **kwargs, + ) -> SlackResponse: + """List all disconnected channels—i.e., + channels that were once connected to other workspaces and then disconnected—and + the corresponding original channel IDs for key revocation with EKM. + https://docs.slack.dev/reference/methods/admin.conversations.ekm.listOriginalConnectedChannelInfo + """ + kwargs.update( + { + "cursor": cursor, + "limit": limit, + } + ) + if isinstance(channel_ids, (list, tuple)): + kwargs.update({"channel_ids": ",".join(channel_ids)}) + else: + kwargs.update({"channel_ids": channel_ids}) + if isinstance(team_ids, (list, tuple)): + kwargs.update({"team_ids": ",".join(team_ids)}) + else: + kwargs.update({"team_ids": team_ids}) + return self.api_call("admin.conversations.ekm.listOriginalConnectedChannelInfo", params=kwargs) + + def admin_conversations_restrictAccess_addGroup( + self, + *, + channel_id: str, + group_id: str, + team_id: Optional[str] = None, + **kwargs, + ) -> SlackResponse: + """Add an allowlist of IDP groups for accessing a channel. + https://docs.slack.dev/reference/methods/admin.conversations.restrictAccess.addGroup + """ + kwargs.update( + { + "channel_id": channel_id, + "group_id": group_id, + "team_id": team_id, + } + ) + return self.api_call( + "admin.conversations.restrictAccess.addGroup", + http_verb="GET", + params=kwargs, + ) + + def admin_conversations_restrictAccess_listGroups( + self, + *, + channel_id: str, + team_id: Optional[str] = None, + **kwargs, + ) -> SlackResponse: + """List all IDP Groups linked to a channel. + https://docs.slack.dev/reference/methods/admin.conversations.restrictAccess.listGroups + """ + kwargs.update( + { + "channel_id": channel_id, + "team_id": team_id, + } + ) + return self.api_call( + "admin.conversations.restrictAccess.listGroups", + http_verb="GET", + params=kwargs, + ) + + def admin_conversations_restrictAccess_removeGroup( + self, + *, + channel_id: str, + group_id: str, + team_id: str, + **kwargs, + ) -> SlackResponse: + """Remove a linked IDP group linked from a private channel. + https://docs.slack.dev/reference/methods/admin.conversations.restrictAccess.removeGroup + """ + kwargs.update( + { + "channel_id": channel_id, + "group_id": group_id, + "team_id": team_id, + } + ) + return self.api_call( + "admin.conversations.restrictAccess.removeGroup", + http_verb="GET", + params=kwargs, + ) + + def admin_conversations_setTeams( + self, + *, + channel_id: str, + org_channel: Optional[bool] = None, + target_team_ids: Optional[Union[str, Sequence[str]]] = None, + team_id: Optional[str] = None, + **kwargs, + ) -> SlackResponse: + """Set the workspaces in an Enterprise grid org that connect to a public or private channel. + https://docs.slack.dev/reference/methods/admin.conversations.setTeams + """ + kwargs.update( + { + "channel_id": channel_id, + "org_channel": org_channel, + "team_id": team_id, + } + ) + if isinstance(target_team_ids, (list, tuple)): + kwargs.update({"target_team_ids": ",".join(target_team_ids)}) + else: + kwargs.update({"target_team_ids": target_team_ids}) + return self.api_call("admin.conversations.setTeams", params=kwargs) + + def admin_conversations_getTeams( + self, + *, + channel_id: str, + cursor: Optional[str] = None, + limit: Optional[int] = None, + **kwargs, + ) -> SlackResponse: + """Set the workspaces in an Enterprise grid org that connect to a channel. + https://docs.slack.dev/reference/methods/admin.conversations.getTeams + """ + kwargs.update( + { + "channel_id": channel_id, + "cursor": cursor, + "limit": limit, + } + ) + return self.api_call("admin.conversations.getTeams", params=kwargs) + + def admin_conversations_getCustomRetention( + self, + *, + channel_id: str, + **kwargs, + ) -> SlackResponse: + """Get a channel's retention policy + https://docs.slack.dev/reference/methods/admin.conversations.getCustomRetention + """ + kwargs.update({"channel_id": channel_id}) + return self.api_call("admin.conversations.getCustomRetention", params=kwargs) + + def admin_conversations_removeCustomRetention( + self, + *, + channel_id: str, + **kwargs, + ) -> SlackResponse: + """Remove a channel's retention policy + https://docs.slack.dev/reference/methods/admin.conversations.removeCustomRetention + """ + kwargs.update({"channel_id": channel_id}) + return self.api_call("admin.conversations.removeCustomRetention", params=kwargs) + + def admin_conversations_setCustomRetention( + self, + *, + channel_id: str, + duration_days: int, + **kwargs, + ) -> SlackResponse: + """Set a channel's retention policy + https://docs.slack.dev/reference/methods/admin.conversations.setCustomRetention + """ + kwargs.update({"channel_id": channel_id, "duration_days": duration_days}) + return self.api_call("admin.conversations.setCustomRetention", params=kwargs) + + def admin_conversations_bulkArchive( + self, + *, + channel_ids: Union[Sequence[str], str], + **kwargs, + ) -> SlackResponse: + """Archive public or private channels in bulk. + https://docs.slack.dev/reference/methods/admin.conversations.bulkArchive + """ + kwargs.update({"channel_ids": ",".join(channel_ids) if isinstance(channel_ids, (list, tuple)) else channel_ids}) + return self.api_call("admin.conversations.bulkArchive", params=kwargs) + + def admin_conversations_bulkDelete( + self, + *, + channel_ids: Union[Sequence[str], str], + **kwargs, + ) -> SlackResponse: + """Delete public or private channels in bulk. + https://slack.com/api/admin.conversations.bulkDelete + """ + kwargs.update({"channel_ids": ",".join(channel_ids) if isinstance(channel_ids, (list, tuple)) else channel_ids}) + return self.api_call("admin.conversations.bulkDelete", params=kwargs) + + def admin_conversations_bulkMove( + self, + *, + channel_ids: Union[Sequence[str], str], + target_team_id: str, + **kwargs, + ) -> SlackResponse: + """Move public or private channels in bulk. + https://docs.slack.dev/reference/methods/admin.conversations.bulkMove + """ + kwargs.update( + { + "target_team_id": target_team_id, + "channel_ids": ",".join(channel_ids) if isinstance(channel_ids, (list, tuple)) else channel_ids, + } + ) + return self.api_call("admin.conversations.bulkMove", params=kwargs) + + def admin_emoji_add( + self, + *, + name: str, + url: str, + **kwargs, + ) -> SlackResponse: + """Add an emoji. + https://docs.slack.dev/reference/methods/admin.emoji.add + """ + kwargs.update({"name": name, "url": url}) + return self.api_call("admin.emoji.add", http_verb="GET", params=kwargs) + + def admin_emoji_addAlias( + self, + *, + alias_for: str, + name: str, + **kwargs, + ) -> SlackResponse: + """Add an emoji alias. + https://docs.slack.dev/reference/methods/admin.emoji.addAlias + """ + kwargs.update({"alias_for": alias_for, "name": name}) + return self.api_call("admin.emoji.addAlias", http_verb="GET", params=kwargs) + + def admin_emoji_list( + self, + *, + cursor: Optional[str] = None, + limit: Optional[int] = None, + **kwargs, + ) -> SlackResponse: + """List emoji for an Enterprise Grid organization. + https://docs.slack.dev/reference/methods/admin.emoji.list + """ + kwargs.update({"cursor": cursor, "limit": limit}) + return self.api_call("admin.emoji.list", http_verb="GET", params=kwargs) + + def admin_emoji_remove( + self, + *, + name: str, + **kwargs, + ) -> SlackResponse: + """Remove an emoji across an Enterprise Grid organization. + https://docs.slack.dev/reference/methods/admin.emoji.remove + """ + kwargs.update({"name": name}) + return self.api_call("admin.emoji.remove", http_verb="GET", params=kwargs) + + def admin_emoji_rename( + self, + *, + name: str, + new_name: str, + **kwargs, + ) -> SlackResponse: + """Rename an emoji. + https://docs.slack.dev/reference/methods/admin.emoji.rename + """ + kwargs.update({"name": name, "new_name": new_name}) + return self.api_call("admin.emoji.rename", http_verb="GET", params=kwargs) + + def admin_functions_list( + self, + *, + app_ids: Union[str, Sequence[str]], + team_id: Optional[str] = None, + cursor: Optional[str] = None, + limit: Optional[int] = None, + **kwargs, + ) -> SlackResponse: + """Look up functions by a set of apps + https://docs.slack.dev/reference/methods/admin.functions.list + """ + if isinstance(app_ids, (list, tuple)): + kwargs.update({"app_ids": ",".join(app_ids)}) + else: + kwargs.update({"app_ids": app_ids}) + kwargs.update( + { + "team_id": team_id, + "cursor": cursor, + "limit": limit, + } + ) + return self.api_call("admin.functions.list", params=kwargs) + + def admin_functions_permissions_lookup( + self, + *, + function_ids: Union[str, Sequence[str]], + **kwargs, + ) -> SlackResponse: + """Lookup the visibility of multiple Slack functions + and include the users if it is limited to particular named entities. + https://docs.slack.dev/reference/methods/admin.functions.permissions.lookup + """ + if isinstance(function_ids, (list, tuple)): + kwargs.update({"function_ids": ",".join(function_ids)}) + else: + kwargs.update({"function_ids": function_ids}) + return self.api_call("admin.functions.permissions.lookup", params=kwargs) + + def admin_functions_permissions_set( + self, + *, + function_id: str, + visibility: str, + user_ids: Optional[Union[str, Sequence[str]]] = None, + **kwargs, + ) -> SlackResponse: + """Set the visibility of a Slack function + and define the users or workspaces if it is set to named_entities + https://docs.slack.dev/reference/methods/admin.functions.permissions.set + """ + kwargs.update( + { + "function_id": function_id, + "visibility": visibility, + } + ) + if user_ids is not None: + if isinstance(user_ids, (list, tuple)): + kwargs.update({"user_ids": ",".join(user_ids)}) + else: + kwargs.update({"user_ids": user_ids}) + return self.api_call("admin.functions.permissions.set", params=kwargs) + + def admin_roles_addAssignments( + self, + *, + role_id: str, + entity_ids: Union[str, Sequence[str]], + user_ids: Union[str, Sequence[str]], + **kwargs, + ) -> SlackResponse: + """Adds members to the specified role with the specified scopes + https://docs.slack.dev/reference/methods/admin.roles.addAssignments + """ + kwargs.update({"role_id": role_id}) + if isinstance(entity_ids, (list, tuple)): + kwargs.update({"entity_ids": ",".join(entity_ids)}) + else: + kwargs.update({"entity_ids": entity_ids}) + if isinstance(user_ids, (list, tuple)): + kwargs.update({"user_ids": ",".join(user_ids)}) + else: + kwargs.update({"user_ids": user_ids}) + return self.api_call("admin.roles.addAssignments", params=kwargs) + + def admin_roles_listAssignments( + self, + *, + role_ids: Optional[Union[str, Sequence[str]]] = None, + entity_ids: Optional[Union[str, Sequence[str]]] = None, + cursor: Optional[str] = None, + limit: Optional[Union[str, int]] = None, + sort_dir: Optional[str] = None, + **kwargs, + ) -> SlackResponse: + """Lists assignments for all roles across entities. + Options to scope results by any combination of roles or entities + https://docs.slack.dev/reference/methods/admin.roles.listAssignments + """ + kwargs.update({"cursor": cursor, "limit": limit, "sort_dir": sort_dir}) + if isinstance(entity_ids, (list, tuple)): + kwargs.update({"entity_ids": ",".join(entity_ids)}) + else: + kwargs.update({"entity_ids": entity_ids}) + if isinstance(role_ids, (list, tuple)): + kwargs.update({"role_ids": ",".join(role_ids)}) + else: + kwargs.update({"role_ids": role_ids}) + return self.api_call("admin.roles.listAssignments", params=kwargs) + + def admin_roles_removeAssignments( + self, + *, + role_id: str, + entity_ids: Union[str, Sequence[str]], + user_ids: Union[str, Sequence[str]], + **kwargs, + ) -> SlackResponse: + """Removes a set of users from a role for the given scopes and entities + https://docs.slack.dev/reference/methods/admin.roles.removeAssignments + """ + kwargs.update({"role_id": role_id}) + if isinstance(entity_ids, (list, tuple)): + kwargs.update({"entity_ids": ",".join(entity_ids)}) + else: + kwargs.update({"entity_ids": entity_ids}) + if isinstance(user_ids, (list, tuple)): + kwargs.update({"user_ids": ",".join(user_ids)}) + else: + kwargs.update({"user_ids": user_ids}) + return self.api_call("admin.roles.removeAssignments", params=kwargs) + + def admin_users_session_reset( + self, + *, + user_id: str, + mobile_only: Optional[bool] = None, + web_only: Optional[bool] = None, + **kwargs, + ) -> SlackResponse: + """Wipes all valid sessions on all devices for a given user. + https://docs.slack.dev/reference/methods/admin.users.session.reset + """ + kwargs.update( + { + "user_id": user_id, + "mobile_only": mobile_only, + "web_only": web_only, + } + ) + return self.api_call("admin.users.session.reset", params=kwargs) + + def admin_users_session_resetBulk( + self, + *, + user_ids: Union[str, Sequence[str]], + mobile_only: Optional[bool] = None, + web_only: Optional[bool] = None, + **kwargs, + ) -> SlackResponse: + """Enqueues an asynchronous job to wipe all valid sessions on all devices for a given list of users + https://docs.slack.dev/reference/methods/admin.users.session.resetBulk + """ + if isinstance(user_ids, (list, tuple)): + kwargs.update({"user_ids": ",".join(user_ids)}) + else: + kwargs.update({"user_ids": user_ids}) + kwargs.update( + { + "mobile_only": mobile_only, + "web_only": web_only, + } + ) + return self.api_call("admin.users.session.resetBulk", params=kwargs) + + def admin_users_session_invalidate( + self, + *, + session_id: str, + team_id: str, + **kwargs, + ) -> SlackResponse: + """Invalidate a single session for a user by session_id. + https://docs.slack.dev/reference/methods/admin.users.session.invalidate + """ + kwargs.update({"session_id": session_id, "team_id": team_id}) + return self.api_call("admin.users.session.invalidate", params=kwargs) + + def admin_users_session_list( + self, + *, + cursor: Optional[str] = None, + limit: Optional[int] = None, + team_id: Optional[str] = None, + user_id: Optional[str] = None, + **kwargs, + ) -> SlackResponse: + """Lists all active user sessions for an organization + https://docs.slack.dev/reference/methods/admin.users.session.list + """ + kwargs.update( + { + "cursor": cursor, + "limit": limit, + "team_id": team_id, + "user_id": user_id, + } + ) + return self.api_call("admin.users.session.list", params=kwargs) + + def admin_teams_settings_setDefaultChannels( + self, + *, + team_id: str, + channel_ids: Union[str, Sequence[str]], + **kwargs, + ) -> SlackResponse: + """Set the default channels of a workspace. + https://docs.slack.dev/reference/methods/admin.teams.settings.setDefaultChannels + """ + kwargs.update({"team_id": team_id}) + if isinstance(channel_ids, (list, tuple)): + kwargs.update({"channel_ids": ",".join(channel_ids)}) + else: + kwargs.update({"channel_ids": channel_ids}) + return self.api_call("admin.teams.settings.setDefaultChannels", http_verb="GET", params=kwargs) + + def admin_users_session_getSettings( + self, + *, + user_ids: Union[str, Sequence[str]], + **kwargs, + ) -> SlackResponse: + """Get user-specific session settings—the session duration + and what happens when the client closes—given a list of users. + https://docs.slack.dev/reference/methods/admin.users.session.getSettings + """ + if isinstance(user_ids, (list, tuple)): + kwargs.update({"user_ids": ",".join(user_ids)}) + else: + kwargs.update({"user_ids": user_ids}) + return self.api_call("admin.users.session.getSettings", params=kwargs) + + def admin_users_session_setSettings( + self, + *, + user_ids: Union[str, Sequence[str]], + desktop_app_browser_quit: Optional[bool] = None, + duration: Optional[int] = None, + **kwargs, + ) -> SlackResponse: + """Configure the user-level session settings—the session duration + and what happens when the client closes—for one or more users. + https://docs.slack.dev/reference/methods/admin.users.session.setSettings + """ + if isinstance(user_ids, (list, tuple)): + kwargs.update({"user_ids": ",".join(user_ids)}) + else: + kwargs.update({"user_ids": user_ids}) + kwargs.update( + { + "desktop_app_browser_quit": desktop_app_browser_quit, + "duration": duration, + } + ) + return self.api_call("admin.users.session.setSettings", params=kwargs) + + def admin_users_session_clearSettings( + self, + *, + user_ids: Union[str, Sequence[str]], + **kwargs, + ) -> SlackResponse: + """Clear user-specific session settings—the session duration + and what happens when the client closes—for a list of users. + https://docs.slack.dev/reference/methods/admin.users.session.clearSettings + """ + if isinstance(user_ids, (list, tuple)): + kwargs.update({"user_ids": ",".join(user_ids)}) + else: + kwargs.update({"user_ids": user_ids}) + return self.api_call("admin.users.session.clearSettings", params=kwargs) + + def admin_users_unsupportedVersions_export( + self, + *, + date_end_of_support: Optional[Union[str, int]] = None, + date_sessions_started: Optional[Union[str, int]] = None, + **kwargs, + ) -> SlackResponse: + """Ask Slackbot to send you an export listing all workspace members using unsupported software, + presented as a zipped CSV file. + https://docs.slack.dev/reference/methods/admin.users.unsupportedVersions.export + """ + kwargs.update( + { + "date_end_of_support": date_end_of_support, + "date_sessions_started": date_sessions_started, + } + ) + return self.api_call("admin.users.unsupportedVersions.export", params=kwargs) + + def admin_inviteRequests_approve( + self, + *, + invite_request_id: str, + team_id: Optional[str] = None, + **kwargs, + ) -> SlackResponse: + """Approve a workspace invite request. + https://docs.slack.dev/reference/methods/admin.inviteRequests.approve + """ + kwargs.update({"invite_request_id": invite_request_id, "team_id": team_id}) + return self.api_call("admin.inviteRequests.approve", params=kwargs) + + def admin_inviteRequests_approved_list( + self, + *, + cursor: Optional[str] = None, + limit: Optional[int] = None, + team_id: Optional[str] = None, + **kwargs, + ) -> SlackResponse: + """List all approved workspace invite requests. + https://docs.slack.dev/reference/methods/admin.inviteRequests.approved.list + """ + kwargs.update( + { + "cursor": cursor, + "limit": limit, + "team_id": team_id, + } + ) + return self.api_call("admin.inviteRequests.approved.list", params=kwargs) + + def admin_inviteRequests_denied_list( + self, + *, + cursor: Optional[str] = None, + limit: Optional[int] = None, + team_id: Optional[str] = None, + **kwargs, + ) -> SlackResponse: + """List all denied workspace invite requests. + https://docs.slack.dev/reference/methods/admin.inviteRequests.denied.list + """ + kwargs.update( + { + "cursor": cursor, + "limit": limit, + "team_id": team_id, + } + ) + return self.api_call("admin.inviteRequests.denied.list", params=kwargs) + + def admin_inviteRequests_deny( + self, + *, + invite_request_id: str, + team_id: Optional[str] = None, + **kwargs, + ) -> SlackResponse: + """Deny a workspace invite request. + https://docs.slack.dev/reference/methods/admin.inviteRequests.deny + """ + kwargs.update({"invite_request_id": invite_request_id, "team_id": team_id}) + return self.api_call("admin.inviteRequests.deny", params=kwargs) + + def admin_inviteRequests_list( + self, + **kwargs, + ) -> SlackResponse: + """List all pending workspace invite requests.""" + return self.api_call("admin.inviteRequests.list", params=kwargs) + + def admin_teams_admins_list( + self, + *, + team_id: str, + cursor: Optional[str] = None, + limit: Optional[int] = None, + **kwargs, + ) -> SlackResponse: + """List all of the admins on a given workspace. + https://docs.slack.dev/reference/methods/admin.inviteRequests.list + """ + kwargs.update( + { + "cursor": cursor, + "limit": limit, + "team_id": team_id, + } + ) + return self.api_call("admin.teams.admins.list", http_verb="GET", params=kwargs) + + def admin_teams_create( + self, + *, + team_domain: str, + team_name: str, + team_description: Optional[str] = None, + team_discoverability: Optional[str] = None, + **kwargs, + ) -> SlackResponse: + """Create an Enterprise team. + https://docs.slack.dev/reference/methods/admin.teams.create + """ + kwargs.update( + { + "team_domain": team_domain, + "team_name": team_name, + "team_description": team_description, + "team_discoverability": team_discoverability, + } + ) + return self.api_call("admin.teams.create", params=kwargs) + + def admin_teams_list( + self, + *, + cursor: Optional[str] = None, + limit: Optional[int] = None, + **kwargs, + ) -> SlackResponse: + """List all teams on an Enterprise organization. + https://docs.slack.dev/reference/methods/admin.teams.list + """ + kwargs.update({"cursor": cursor, "limit": limit}) + return self.api_call("admin.teams.list", params=kwargs) + + def admin_teams_owners_list( + self, + *, + team_id: str, + cursor: Optional[str] = None, + limit: Optional[int] = None, + **kwargs, + ) -> SlackResponse: + """List all of the admins on a given workspace. + https://docs.slack.dev/reference/methods/admin.teams.owners.list + """ + kwargs.update({"team_id": team_id, "cursor": cursor, "limit": limit}) + return self.api_call("admin.teams.owners.list", http_verb="GET", params=kwargs) + + def admin_teams_settings_info( + self, + *, + team_id: str, + **kwargs, + ) -> SlackResponse: + """Fetch information about settings in a workspace + https://docs.slack.dev/reference/methods/admin.teams.settings.info + """ + kwargs.update({"team_id": team_id}) + return self.api_call("admin.teams.settings.info", params=kwargs) + + def admin_teams_settings_setDescription( + self, + *, + team_id: str, + description: str, + **kwargs, + ) -> SlackResponse: + """Set the description of a given workspace. + https://docs.slack.dev/reference/methods/admin.teams.settings.setDescription + """ + kwargs.update({"team_id": team_id, "description": description}) + return self.api_call("admin.teams.settings.setDescription", params=kwargs) + + def admin_teams_settings_setDiscoverability( + self, + *, + team_id: str, + discoverability: str, + **kwargs, + ) -> SlackResponse: + """Sets the icon of a workspace. + https://docs.slack.dev/reference/methods/admin.teams.settings.setDiscoverability + """ + kwargs.update({"team_id": team_id, "discoverability": discoverability}) + return self.api_call("admin.teams.settings.setDiscoverability", params=kwargs) + + def admin_teams_settings_setIcon( + self, + *, + team_id: str, + image_url: str, + **kwargs, + ) -> SlackResponse: + """Sets the icon of a workspace. + https://docs.slack.dev/reference/methods/admin.teams.settings.setIcon + """ + kwargs.update({"team_id": team_id, "image_url": image_url}) + return self.api_call("admin.teams.settings.setIcon", http_verb="GET", params=kwargs) + + def admin_teams_settings_setName( + self, + *, + team_id: str, + name: str, + **kwargs, + ) -> SlackResponse: + """Sets the icon of a workspace. + https://docs.slack.dev/reference/methods/admin.teams.settings.setName + """ + kwargs.update({"team_id": team_id, "name": name}) + return self.api_call("admin.teams.settings.setName", params=kwargs) + + def admin_usergroups_addChannels( + self, + *, + channel_ids: Union[str, Sequence[str]], + usergroup_id: str, + team_id: Optional[str] = None, + **kwargs, + ) -> SlackResponse: + """Add one or more default channels to an IDP group. + https://docs.slack.dev/reference/methods/admin.usergroups.addChannels + """ + kwargs.update({"team_id": team_id, "usergroup_id": usergroup_id}) + if isinstance(channel_ids, (list, tuple)): + kwargs.update({"channel_ids": ",".join(channel_ids)}) + else: + kwargs.update({"channel_ids": channel_ids}) + return self.api_call("admin.usergroups.addChannels", params=kwargs) + + def admin_usergroups_addTeams( + self, + *, + usergroup_id: str, + team_ids: Union[str, Sequence[str]], + auto_provision: Optional[bool] = None, + **kwargs, + ) -> SlackResponse: + """Associate one or more default workspaces with an organization-wide IDP group. + https://docs.slack.dev/reference/methods/admin.usergroups.addTeams + """ + kwargs.update({"usergroup_id": usergroup_id, "auto_provision": auto_provision}) + if isinstance(team_ids, (list, tuple)): + kwargs.update({"team_ids": ",".join(team_ids)}) + else: + kwargs.update({"team_ids": team_ids}) + return self.api_call("admin.usergroups.addTeams", params=kwargs) + + def admin_usergroups_listChannels( + self, + *, + usergroup_id: str, + include_num_members: Optional[bool] = None, + team_id: Optional[bool] = None, + **kwargs, + ) -> SlackResponse: + """Add one or more default channels to an IDP group. + https://docs.slack.dev/reference/methods/admin.usergroups.listChannels + """ + kwargs.update( + { + "usergroup_id": usergroup_id, + "include_num_members": include_num_members, + "team_id": team_id, + } + ) + return self.api_call("admin.usergroups.listChannels", params=kwargs) + + def admin_usergroups_removeChannels( + self, + *, + usergroup_id: str, + channel_ids: Union[str, Sequence[str]], + **kwargs, + ) -> SlackResponse: + """Add one or more default channels to an IDP group. + https://docs.slack.dev/reference/methods/admin.usergroups.removeChannels + """ + kwargs.update({"usergroup_id": usergroup_id}) + if isinstance(channel_ids, (list, tuple)): + kwargs.update({"channel_ids": ",".join(channel_ids)}) + else: + kwargs.update({"channel_ids": channel_ids}) + return self.api_call("admin.usergroups.removeChannels", params=kwargs) + + def admin_users_assign( + self, + *, + team_id: str, + user_id: str, + channel_ids: Optional[Union[str, Sequence[str]]] = None, + is_restricted: Optional[bool] = None, + is_ultra_restricted: Optional[bool] = None, + **kwargs, + ) -> SlackResponse: + """Add an Enterprise user to a workspace. + https://docs.slack.dev/reference/methods/admin.users.assign + """ + kwargs.update( + { + "team_id": team_id, + "user_id": user_id, + "is_restricted": is_restricted, + "is_ultra_restricted": is_ultra_restricted, + } + ) + if isinstance(channel_ids, (list, tuple)): + kwargs.update({"channel_ids": ",".join(channel_ids)}) + else: + kwargs.update({"channel_ids": channel_ids}) + return self.api_call("admin.users.assign", params=kwargs) + + def admin_users_invite( + self, + *, + team_id: str, + email: str, + channel_ids: Union[str, Sequence[str]], + custom_message: Optional[str] = None, + email_password_policy_enabled: Optional[bool] = None, + guest_expiration_ts: Optional[Union[str, float]] = None, + is_restricted: Optional[bool] = None, + is_ultra_restricted: Optional[bool] = None, + real_name: Optional[str] = None, + resend: Optional[bool] = None, + **kwargs, + ) -> SlackResponse: + """Invite a user to a workspace. + https://docs.slack.dev/reference/methods/admin.users.invite + """ + kwargs.update( + { + "team_id": team_id, + "email": email, + "custom_message": custom_message, + "email_password_policy_enabled": email_password_policy_enabled, + "guest_expiration_ts": str(guest_expiration_ts) if guest_expiration_ts is not None else None, + "is_restricted": is_restricted, + "is_ultra_restricted": is_ultra_restricted, + "real_name": real_name, + "resend": resend, + } + ) + if isinstance(channel_ids, (list, tuple)): + kwargs.update({"channel_ids": ",".join(channel_ids)}) + else: + kwargs.update({"channel_ids": channel_ids}) + return self.api_call("admin.users.invite", params=kwargs) + + def admin_users_list( + self, + *, + team_id: Optional[str] = None, + include_deactivated_user_workspaces: Optional[bool] = None, + is_active: Optional[bool] = None, + cursor: Optional[str] = None, + limit: Optional[int] = None, + **kwargs, + ) -> SlackResponse: + """List users on a workspace + https://docs.slack.dev/reference/methods/admin.users.list + """ + kwargs.update( + { + "team_id": team_id, + "include_deactivated_user_workspaces": include_deactivated_user_workspaces, + "is_active": is_active, + "cursor": cursor, + "limit": limit, + } + ) + return self.api_call("admin.users.list", params=kwargs) + + def admin_users_remove( + self, + *, + team_id: str, + user_id: str, + **kwargs, + ) -> SlackResponse: + """Remove a user from a workspace. + https://docs.slack.dev/reference/methods/admin.users.remove + """ + kwargs.update({"team_id": team_id, "user_id": user_id}) + return self.api_call("admin.users.remove", params=kwargs) + + def admin_users_setAdmin( + self, + *, + team_id: str, + user_id: str, + **kwargs, + ) -> SlackResponse: + """Set an existing guest, regular user, or owner to be an admin user. + https://docs.slack.dev/reference/methods/admin.users.setAdmin + """ + kwargs.update({"team_id": team_id, "user_id": user_id}) + return self.api_call("admin.users.setAdmin", params=kwargs) + + def admin_users_setExpiration( + self, + *, + expiration_ts: int, + user_id: str, + team_id: Optional[str] = None, + **kwargs, + ) -> SlackResponse: + """Set an expiration for a guest user. + https://docs.slack.dev/reference/methods/admin.users.setExpiration + """ + kwargs.update({"expiration_ts": expiration_ts, "team_id": team_id, "user_id": user_id}) + return self.api_call("admin.users.setExpiration", params=kwargs) + + def admin_users_setOwner( + self, + *, + team_id: str, + user_id: str, + **kwargs, + ) -> SlackResponse: + """Set an existing guest, regular user, or admin user to be a workspace owner. + https://docs.slack.dev/reference/methods/admin.users.setOwner + """ + kwargs.update({"team_id": team_id, "user_id": user_id}) + return self.api_call("admin.users.setOwner", params=kwargs) + + def admin_users_setRegular( + self, + *, + team_id: str, + user_id: str, + **kwargs, + ) -> SlackResponse: + """Set an existing guest user, admin user, or owner to be a regular user. + https://docs.slack.dev/reference/methods/admin.users.setRegular + """ + kwargs.update({"team_id": team_id, "user_id": user_id}) + return self.api_call("admin.users.setRegular", params=kwargs) + + def admin_workflows_search( + self, + *, + app_id: Optional[str] = None, + collaborator_ids: Optional[Union[str, Sequence[str]]] = None, + cursor: Optional[str] = None, + limit: Optional[int] = None, + no_collaborators: Optional[bool] = None, + num_trigger_ids: Optional[int] = None, + query: Optional[str] = None, + sort: Optional[str] = None, + sort_dir: Optional[str] = None, + source: Optional[str] = None, + **kwargs, + ) -> SlackResponse: + """Search workflows within the team or enterprise + https://docs.slack.dev/reference/methods/admin.workflows.search + """ + if collaborator_ids is not None: + if isinstance(collaborator_ids, (list, tuple)): + kwargs.update({"collaborator_ids": ",".join(collaborator_ids)}) + else: + kwargs.update({"collaborator_ids": collaborator_ids}) + kwargs.update( + { + "app_id": app_id, + "cursor": cursor, + "limit": limit, + "no_collaborators": no_collaborators, + "num_trigger_ids": num_trigger_ids, + "query": query, + "sort": sort, + "sort_dir": sort_dir, + "source": source, + } + ) + return self.api_call("admin.workflows.search", params=kwargs) + + def admin_workflows_permissions_lookup( + self, + *, + workflow_ids: Union[str, Sequence[str]], + max_workflow_triggers: Optional[int] = None, + **kwargs, + ) -> SlackResponse: + """Look up the permissions for a set of workflows + https://docs.slack.dev/reference/methods/admin.workflows.permissions.lookup + """ + if isinstance(workflow_ids, (list, tuple)): + kwargs.update({"workflow_ids": ",".join(workflow_ids)}) + else: + kwargs.update({"workflow_ids": workflow_ids}) + kwargs.update( + { + "max_workflow_triggers": max_workflow_triggers, + } + ) + return self.api_call("admin.workflows.permissions.lookup", params=kwargs) + + def admin_workflows_collaborators_add( + self, + *, + collaborator_ids: Union[str, Sequence[str]], + workflow_ids: Union[str, Sequence[str]], + **kwargs, + ) -> SlackResponse: + """Add collaborators to workflows within the team or enterprise + https://docs.slack.dev/reference/methods/admin.workflows.collaborators.add + """ + if isinstance(collaborator_ids, (list, tuple)): + kwargs.update({"collaborator_ids": ",".join(collaborator_ids)}) + else: + kwargs.update({"collaborator_ids": collaborator_ids}) + if isinstance(workflow_ids, (list, tuple)): + kwargs.update({"workflow_ids": ",".join(workflow_ids)}) + else: + kwargs.update({"workflow_ids": workflow_ids}) + return self.api_call("admin.workflows.collaborators.add", params=kwargs) + + def admin_workflows_collaborators_remove( + self, + *, + collaborator_ids: Union[str, Sequence[str]], + workflow_ids: Union[str, Sequence[str]], + **kwargs, + ) -> SlackResponse: + """Remove collaborators from workflows within the team or enterprise + https://docs.slack.dev/reference/methods/admin.workflows.collaborators.remove + """ + if isinstance(collaborator_ids, (list, tuple)): + kwargs.update({"collaborator_ids": ",".join(collaborator_ids)}) + else: + kwargs.update({"collaborator_ids": collaborator_ids}) + if isinstance(workflow_ids, (list, tuple)): + kwargs.update({"workflow_ids": ",".join(workflow_ids)}) + else: + kwargs.update({"workflow_ids": workflow_ids}) + return self.api_call("admin.workflows.collaborators.remove", params=kwargs) + + def admin_workflows_unpublish( + self, + *, + workflow_ids: Union[str, Sequence[str]], + **kwargs, + ) -> SlackResponse: + """Unpublish workflows within the team or enterprise + https://docs.slack.dev/reference/methods/admin.workflows.unpublish + """ + if isinstance(workflow_ids, (list, tuple)): + kwargs.update({"workflow_ids": ",".join(workflow_ids)}) + else: + kwargs.update({"workflow_ids": workflow_ids}) + return self.api_call("admin.workflows.unpublish", params=kwargs) + + def api_test( + self, + *, + error: Optional[str] = None, + **kwargs, + ) -> SlackResponse: + """Checks API calling code. + https://docs.slack.dev/reference/methods/api.test + """ + kwargs.update({"error": error}) + return self.api_call("api.test", params=kwargs) + + def apps_connections_open( + self, + *, + app_token: str, + **kwargs, + ) -> SlackResponse: + """Generate a temporary Socket Mode WebSocket URL that your app can connect to + in order to receive events and interactive payloads + https://docs.slack.dev/reference/methods/apps.connections.open + """ + kwargs.update({"token": app_token}) + return self.api_call("apps.connections.open", http_verb="POST", params=kwargs) + + def apps_event_authorizations_list( + self, + *, + event_context: str, + cursor: Optional[str] = None, + limit: Optional[int] = None, + **kwargs, + ) -> SlackResponse: + """Get a list of authorizations for the given event context. + Each authorization represents an app installation that the event is visible to. + https://docs.slack.dev/reference/methods/apps.event.authorizations.list + """ + kwargs.update({"event_context": event_context, "cursor": cursor, "limit": limit}) + return self.api_call("apps.event.authorizations.list", params=kwargs) + + def apps_uninstall( + self, + *, + client_id: str, + client_secret: str, + **kwargs, + ) -> SlackResponse: + """Uninstalls your app from a workspace. + https://docs.slack.dev/reference/methods/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://docs.slack.dev/reference/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://docs.slack.dev/reference/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://docs.slack.dev/reference/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://docs.slack.dev/reference/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://docs.slack.dev/reference/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://docs.slack.dev/reference/methods/tooling.tokens.rotate + """ + kwargs.update({"refresh_token": refresh_token}) + return self.api_call("tooling.tokens.rotate", params=kwargs) + + def assistant_threads_setStatus( + self, + *, + channel_id: str, + thread_ts: str, + status: str, + loading_messages: Optional[List[str]] = None, + **kwargs, + ) -> SlackResponse: + """Set the status for an AI assistant thread. + https://docs.slack.dev/reference/methods/assistant.threads.setStatus + """ + kwargs.update( + {"channel_id": channel_id, "thread_ts": thread_ts, "status": status, "loading_messages": loading_messages} + ) + kwargs = _remove_none_values(kwargs) + return self.api_call("assistant.threads.setStatus", json=kwargs) + + def assistant_threads_setTitle( + self, + *, + channel_id: str, + thread_ts: str, + title: str, + **kwargs, + ) -> SlackResponse: + """Set the title for the given assistant thread. + https://docs.slack.dev/reference/methods/assistant.threads.setTitle + """ + kwargs.update({"channel_id": channel_id, "thread_ts": thread_ts, "title": title}) + return self.api_call("assistant.threads.setTitle", params=kwargs) + + def assistant_threads_setSuggestedPrompts( + self, + *, + channel_id: str, + thread_ts: str, + title: Optional[str] = None, + prompts: List[Dict[str, str]], + **kwargs, + ) -> SlackResponse: + """Set suggested prompts for the given assistant thread. + https://docs.slack.dev/reference/methods/assistant.threads.setSuggestedPrompts + """ + kwargs.update({"channel_id": channel_id, "thread_ts": thread_ts, "prompts": prompts}) + if title is not None: + kwargs.update({"title": title}) + return self.api_call("assistant.threads.setSuggestedPrompts", json=kwargs) + + def auth_revoke( + self, + *, + test: Optional[bool] = None, + **kwargs, + ) -> SlackResponse: + """Revokes a token. + https://docs.slack.dev/reference/methods/auth.revoke + """ + kwargs.update({"test": test}) + return self.api_call("auth.revoke", http_verb="GET", params=kwargs) + + def auth_test( + self, + **kwargs, + ) -> SlackResponse: + """Checks authentication & identity. + https://docs.slack.dev/reference/methods/auth.test + """ + return self.api_call("auth.test", params=kwargs) + + def auth_teams_list( + self, + cursor: Optional[str] = None, + limit: Optional[int] = None, + include_icon: Optional[bool] = None, + **kwargs, + ) -> SlackResponse: + """List the workspaces a token can access. + https://docs.slack.dev/reference/methods/auth.teams.list + """ + kwargs.update({"cursor": cursor, "limit": limit, "include_icon": include_icon}) + return self.api_call("auth.teams.list", params=kwargs) + + def bookmarks_add( + self, + *, + channel_id: str, + title: str, + type: str, + emoji: Optional[str] = None, + entity_id: Optional[str] = None, + link: Optional[str] = None, # include when type is 'link' + parent_id: Optional[str] = None, + **kwargs, + ) -> SlackResponse: + """Add bookmark to a channel. + https://docs.slack.dev/reference/methods/bookmarks.add + """ + kwargs.update( + { + "channel_id": channel_id, + "title": title, + "type": type, + "emoji": emoji, + "entity_id": entity_id, + "link": link, + "parent_id": parent_id, + } + ) + return self.api_call("bookmarks.add", http_verb="POST", params=kwargs) + + def bookmarks_edit( + self, + *, + bookmark_id: str, + channel_id: str, + emoji: Optional[str] = None, + link: Optional[str] = None, + title: Optional[str] = None, + **kwargs, + ) -> SlackResponse: + """Edit bookmark. + https://docs.slack.dev/reference/methods/bookmarks.edit + """ + kwargs.update( + { + "bookmark_id": bookmark_id, + "channel_id": channel_id, + "emoji": emoji, + "link": link, + "title": title, + } + ) + return self.api_call("bookmarks.edit", http_verb="POST", params=kwargs) + + def bookmarks_list( + self, + *, + channel_id: str, + **kwargs, + ) -> SlackResponse: + """List bookmark for the channel. + https://docs.slack.dev/reference/methods/bookmarks.list + """ + kwargs.update({"channel_id": channel_id}) + return self.api_call("bookmarks.list", http_verb="POST", params=kwargs) + + def bookmarks_remove( + self, + *, + bookmark_id: str, + channel_id: str, + **kwargs, + ) -> SlackResponse: + """Remove bookmark from the channel. + https://docs.slack.dev/reference/methods/bookmarks.remove + """ + kwargs.update({"bookmark_id": bookmark_id, "channel_id": channel_id}) + return self.api_call("bookmarks.remove", http_verb="POST", params=kwargs) + + def bots_info( + self, + *, + bot: Optional[str] = None, + team_id: Optional[str] = None, + **kwargs, + ) -> SlackResponse: + """Gets information about a bot user. + https://docs.slack.dev/reference/methods/bots.info + """ + kwargs.update({"bot": bot, "team_id": team_id}) + return self.api_call("bots.info", http_verb="GET", params=kwargs) + + def calls_add( + self, + *, + external_unique_id: str, + join_url: str, + created_by: Optional[str] = None, + date_start: Optional[int] = None, + desktop_app_join_url: Optional[str] = None, + external_display_id: Optional[str] = None, + title: Optional[str] = None, + users: Optional[Union[str, Sequence[Dict[str, str]]]] = None, + **kwargs, + ) -> SlackResponse: + """Registers a new Call. + https://docs.slack.dev/reference/methods/calls.add + """ + kwargs.update( + { + "external_unique_id": external_unique_id, + "join_url": join_url, + "created_by": created_by, + "date_start": date_start, + "desktop_app_join_url": desktop_app_join_url, + "external_display_id": external_display_id, + "title": title, + } + ) + _update_call_participants( + kwargs, + users if users is not None else kwargs.get("users"), # type: ignore[arg-type] + ) + return self.api_call("calls.add", http_verb="POST", params=kwargs) + + def calls_end( + self, + *, + id: str, + duration: Optional[int] = None, + **kwargs, + ) -> SlackResponse: + """Ends a Call. + https://docs.slack.dev/reference/methods/calls.end + """ + kwargs.update({"id": id, "duration": duration}) + return self.api_call("calls.end", http_verb="POST", params=kwargs) + + def calls_info( + self, + *, + id: str, + **kwargs, + ) -> SlackResponse: + """Returns information about a Call. + https://docs.slack.dev/reference/methods/calls.info + """ + 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, Sequence[Dict[str, str]]], + **kwargs, + ) -> SlackResponse: + """Registers new participants added to a Call. + https://docs.slack.dev/reference/methods/calls.participants.add + """ + kwargs.update({"id": id}) + _update_call_participants(kwargs, users) + return self.api_call("calls.participants.add", http_verb="POST", params=kwargs) + + def calls_participants_remove( + self, + *, + id: str, + users: Union[str, Sequence[Dict[str, str]]], + **kwargs, + ) -> SlackResponse: + """Registers participants removed from a Call. + https://docs.slack.dev/reference/methods/calls.participants.remove + """ + kwargs.update({"id": id}) + _update_call_participants(kwargs, users) + return self.api_call("calls.participants.remove", http_verb="POST", params=kwargs) + + def calls_update( + self, + *, + id: str, + desktop_app_join_url: Optional[str] = None, + join_url: Optional[str] = None, + title: Optional[str] = None, + **kwargs, + ) -> SlackResponse: + """Updates information about a Call. + https://docs.slack.dev/reference/methods/calls.update + """ + kwargs.update( + { + "id": id, + "desktop_app_join_url": desktop_app_join_url, + "join_url": join_url, + "title": title, + } + ) + return self.api_call("calls.update", http_verb="POST", params=kwargs) + + def canvases_create( + self, + *, + title: Optional[str] = None, + document_content: Dict[str, str], + **kwargs, + ) -> SlackResponse: + """Create Canvas for a user + https://docs.slack.dev/reference/methods/canvases.create + """ + kwargs.update({"title": title, "document_content": document_content}) + return self.api_call("canvases.create", json=kwargs) + + def canvases_edit( + self, + *, + canvas_id: str, + changes: Sequence[Dict[str, Any]], + **kwargs, + ) -> SlackResponse: + """Update an existing canvas + https://docs.slack.dev/reference/methods/canvases.edit + """ + kwargs.update({"canvas_id": canvas_id, "changes": changes}) + return self.api_call("canvases.edit", json=kwargs) + + def canvases_delete( + self, + *, + canvas_id: str, + **kwargs, + ) -> SlackResponse: + """Deletes a canvas + https://docs.slack.dev/reference/methods/canvases.delete + """ + kwargs.update({"canvas_id": canvas_id}) + return self.api_call("canvases.delete", params=kwargs) + + def canvases_access_set( + self, + *, + canvas_id: str, + access_level: str, + channel_ids: Optional[Union[Sequence[str], str]] = None, + user_ids: Optional[Union[Sequence[str], str]] = None, + **kwargs, + ) -> SlackResponse: + """Sets the access level to a canvas for specified entities + https://docs.slack.dev/reference/methods/canvases.access.set + """ + kwargs.update({"canvas_id": canvas_id, "access_level": access_level}) + if channel_ids is not None: + if isinstance(channel_ids, (list, tuple)): + kwargs.update({"channel_ids": ",".join(channel_ids)}) + else: + kwargs.update({"channel_ids": channel_ids}) + if user_ids is not None: + if isinstance(user_ids, (list, tuple)): + kwargs.update({"user_ids": ",".join(user_ids)}) + else: + kwargs.update({"user_ids": user_ids}) + + return self.api_call("canvases.access.set", params=kwargs) + + def canvases_access_delete( + self, + *, + canvas_id: str, + channel_ids: Optional[Union[Sequence[str], str]] = None, + user_ids: Optional[Union[Sequence[str], str]] = None, + **kwargs, + ) -> SlackResponse: + """Create a Channel Canvas for a channel + https://docs.slack.dev/reference/methods/canvases.access.delete + """ + kwargs.update({"canvas_id": canvas_id}) + if channel_ids is not None: + if isinstance(channel_ids, (list, tuple)): + kwargs.update({"channel_ids": ",".join(channel_ids)}) + else: + kwargs.update({"channel_ids": channel_ids}) + if user_ids is not None: + if isinstance(user_ids, (list, tuple)): + kwargs.update({"user_ids": ",".join(user_ids)}) + else: + kwargs.update({"user_ids": user_ids}) + return self.api_call("canvases.access.delete", params=kwargs) + + def canvases_sections_lookup( + self, + *, + canvas_id: str, + criteria: Dict[str, Any], + **kwargs, + ) -> SlackResponse: + """Find sections matching the provided criteria + https://docs.slack.dev/reference/methods/canvases.sections.lookup + """ + kwargs.update({"canvas_id": canvas_id, "criteria": json.dumps(criteria)}) + return self.api_call("canvases.sections.lookup", params=kwargs) + + # -------------------------- + # Deprecated: channels.* + # You can use conversations.* APIs instead. + # https://docs.slack.dev/changelog/2020-01-deprecating-antecedents-to-the-conversations-api/ + # -------------------------- + + def channels_archive( + self, + *, + channel: str, + **kwargs, + ) -> SlackResponse: + """Archives a channel.""" + kwargs.update({"channel": channel}) + kwargs = _remove_none_values(kwargs) + return self.api_call("channels.archive", json=kwargs) + + def channels_create( + self, + *, + name: str, + **kwargs, + ) -> SlackResponse: + """Creates a channel.""" + kwargs.update({"name": name}) + kwargs = _remove_none_values(kwargs) + return self.api_call("channels.create", json=kwargs) + + def channels_history( + self, + *, + channel: str, + **kwargs, + ) -> SlackResponse: + """Fetches history of messages and events from a channel.""" + kwargs.update({"channel": channel}) + return self.api_call("channels.history", http_verb="GET", params=kwargs) + + def channels_info( + self, + *, + channel: str, + **kwargs, + ) -> SlackResponse: + """Gets information about a channel.""" + kwargs.update({"channel": channel}) + return self.api_call("channels.info", http_verb="GET", params=kwargs) + + def channels_invite( + self, + *, + channel: str, + user: str, + **kwargs, + ) -> SlackResponse: + """Invites a user to a channel.""" + kwargs.update({"channel": channel, "user": user}) + kwargs = _remove_none_values(kwargs) + return self.api_call("channels.invite", json=kwargs) + + def channels_join( + self, + *, + name: str, + **kwargs, + ) -> SlackResponse: + """Joins a channel, creating it if needed.""" + kwargs.update({"name": name}) + kwargs = _remove_none_values(kwargs) + return self.api_call("channels.join", json=kwargs) + + def channels_kick( + self, + *, + channel: str, + user: str, + **kwargs, + ) -> SlackResponse: + """Removes a user from a channel.""" + kwargs.update({"channel": channel, "user": user}) + kwargs = _remove_none_values(kwargs) + return self.api_call("channels.kick", json=kwargs) + + def channels_leave( + self, + *, + channel: str, + **kwargs, + ) -> SlackResponse: + """Leaves a channel.""" + kwargs.update({"channel": channel}) + kwargs = _remove_none_values(kwargs) + return self.api_call("channels.leave", json=kwargs) + + def channels_list( + self, + **kwargs, + ) -> SlackResponse: + """Lists all channels in a Slack team.""" + return self.api_call("channels.list", http_verb="GET", params=kwargs) + + def channels_mark( + self, + *, + channel: str, + ts: str, + **kwargs, + ) -> SlackResponse: + """Sets the read cursor in a channel.""" + kwargs.update({"channel": channel, "ts": ts}) + kwargs = _remove_none_values(kwargs) + return self.api_call("channels.mark", json=kwargs) + + def channels_rename( + self, + *, + channel: str, + name: str, + **kwargs, + ) -> SlackResponse: + """Renames a channel.""" + kwargs.update({"channel": channel, "name": name}) + kwargs = _remove_none_values(kwargs) + return self.api_call("channels.rename", json=kwargs) + + def channels_replies( + self, + *, + channel: str, + thread_ts: str, + **kwargs, + ) -> SlackResponse: + """Retrieve a thread of messages posted to a channel""" + kwargs.update({"channel": channel, "thread_ts": thread_ts}) + return self.api_call("channels.replies", http_verb="GET", params=kwargs) + + def channels_setPurpose( + self, + *, + channel: str, + purpose: str, + **kwargs, + ) -> SlackResponse: + """Sets the purpose for a channel.""" + kwargs.update({"channel": channel, "purpose": purpose}) + kwargs = _remove_none_values(kwargs) + return self.api_call("channels.setPurpose", json=kwargs) + + def channels_setTopic( + self, + *, + channel: str, + topic: str, + **kwargs, + ) -> SlackResponse: + """Sets the topic for a channel.""" + kwargs.update({"channel": channel, "topic": topic}) + kwargs = _remove_none_values(kwargs) + return self.api_call("channels.setTopic", json=kwargs) + + def channels_unarchive( + self, + *, + channel: str, + **kwargs, + ) -> SlackResponse: + """Unarchives a channel.""" + kwargs.update({"channel": channel}) + kwargs = _remove_none_values(kwargs) + return self.api_call("channels.unarchive", json=kwargs) + + # -------------------------- + + def chat_appendStream( + self, + *, + channel: str, + ts: str, + markdown_text: str, + **kwargs, + ) -> SlackResponse: + """Appends text to an existing streaming conversation. + https://docs.slack.dev/reference/methods/chat.appendStream + """ + kwargs.update( + { + "channel": channel, + "ts": ts, + "markdown_text": markdown_text, + } + ) + kwargs = _remove_none_values(kwargs) + return self.api_call("chat.appendStream", json=kwargs) + + def chat_delete( + self, + *, + channel: str, + ts: str, + as_user: Optional[bool] = None, + **kwargs, + ) -> SlackResponse: + """Deletes a message. + https://docs.slack.dev/reference/methods/chat.delete + """ + kwargs.update({"channel": channel, "ts": ts, "as_user": as_user}) + return self.api_call("chat.delete", params=kwargs) + + def chat_deleteScheduledMessage( + self, + *, + channel: str, + scheduled_message_id: str, + as_user: Optional[bool] = None, + **kwargs, + ) -> SlackResponse: + """Deletes a scheduled message. + https://docs.slack.dev/reference/methods/chat.deleteScheduledMessage + """ + kwargs.update( + { + "channel": channel, + "scheduled_message_id": scheduled_message_id, + "as_user": as_user, + } + ) + return self.api_call("chat.deleteScheduledMessage", params=kwargs) + + def chat_getPermalink( + self, + *, + channel: str, + message_ts: str, + **kwargs, + ) -> SlackResponse: + """Retrieve a permalink URL for a specific extant message + https://docs.slack.dev/reference/methods/chat.getPermalink + """ + kwargs.update({"channel": channel, "message_ts": message_ts}) + return self.api_call("chat.getPermalink", http_verb="GET", params=kwargs) + + def chat_meMessage( + self, + *, + channel: str, + text: str, + **kwargs, + ) -> SlackResponse: + """Share a me message into a channel. + https://docs.slack.dev/reference/methods/chat.meMessage + """ + kwargs.update({"channel": channel, "text": text}) + return self.api_call("chat.meMessage", params=kwargs) + + def chat_postEphemeral( + self, + *, + channel: str, + user: str, + text: Optional[str] = None, + as_user: Optional[bool] = None, + attachments: Optional[Union[str, Sequence[Union[Dict, Attachment]]]] = None, + blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None, + thread_ts: Optional[str] = None, + icon_emoji: Optional[str] = None, + icon_url: Optional[str] = None, + link_names: Optional[bool] = None, + username: Optional[str] = None, + parse: Optional[str] = None, + markdown_text: Optional[str] = None, + **kwargs, + ) -> SlackResponse: + """Sends an ephemeral message to a user in a channel. + https://docs.slack.dev/reference/methods/chat.postEphemeral + """ + kwargs.update( + { + "channel": channel, + "user": user, + "text": text, + "as_user": as_user, + "attachments": attachments, + "blocks": blocks, + "thread_ts": thread_ts, + "icon_emoji": icon_emoji, + "icon_url": icon_url, + "link_names": link_names, + "username": username, + "parse": parse, + "markdown_text": markdown_text, + } + ) + _parse_web_class_objects(kwargs) + kwargs = _remove_none_values(kwargs) + _warn_if_message_text_content_is_missing("chat.postEphemeral", kwargs) + # NOTE: intentionally using json over params for the API methods using blocks/attachments + return self.api_call("chat.postEphemeral", json=kwargs) + + def chat_postMessage( + self, + *, + channel: str, + text: Optional[str] = None, + as_user: Optional[bool] = None, + attachments: Optional[Union[str, Sequence[Union[Dict, Attachment]]]] = None, + blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None, + thread_ts: Optional[str] = None, + reply_broadcast: Optional[bool] = None, + unfurl_links: Optional[bool] = None, + unfurl_media: Optional[bool] = None, + container_id: Optional[str] = None, + icon_emoji: Optional[str] = None, + icon_url: Optional[str] = None, + mrkdwn: Optional[bool] = None, + link_names: Optional[bool] = None, + username: Optional[str] = None, + parse: Optional[str] = None, # none, full + metadata: Optional[Union[Dict, Metadata, EventAndEntityMetadata]] = None, + markdown_text: Optional[str] = None, + **kwargs, + ) -> SlackResponse: + """Sends a message to a channel. + https://docs.slack.dev/reference/methods/chat.postMessage + """ + kwargs.update( + { + "channel": channel, + "text": text, + "as_user": as_user, + "attachments": attachments, + "blocks": blocks, + "thread_ts": thread_ts, + "reply_broadcast": reply_broadcast, + "unfurl_links": unfurl_links, + "unfurl_media": unfurl_media, + "container_id": container_id, + "icon_emoji": icon_emoji, + "icon_url": icon_url, + "mrkdwn": mrkdwn, + "link_names": link_names, + "username": username, + "parse": parse, + "metadata": metadata, + "markdown_text": markdown_text, + } + ) + _parse_web_class_objects(kwargs) + kwargs = _remove_none_values(kwargs) + _warn_if_message_text_content_is_missing("chat.postMessage", kwargs) + # NOTE: intentionally using json over params for the API methods using blocks/attachments + return self.api_call("chat.postMessage", json=kwargs) + + def chat_scheduleMessage( + self, + *, + channel: str, + post_at: Union[str, int], + text: Optional[str] = None, + as_user: Optional[bool] = None, + attachments: Optional[Union[str, Sequence[Union[Dict, Attachment]]]] = None, + blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None, + thread_ts: Optional[str] = None, + parse: Optional[str] = None, + reply_broadcast: Optional[bool] = None, + unfurl_links: Optional[bool] = None, + unfurl_media: Optional[bool] = None, + link_names: Optional[bool] = None, + metadata: Optional[Union[Dict, Metadata]] = None, + markdown_text: Optional[str] = None, + **kwargs, + ) -> SlackResponse: + """Schedules a message. + https://docs.slack.dev/reference/methods/chat.scheduleMessage + """ + kwargs.update( + { + "channel": channel, + "post_at": post_at, + "text": text, + "as_user": as_user, + "attachments": attachments, + "blocks": blocks, + "thread_ts": thread_ts, + "reply_broadcast": reply_broadcast, + "parse": parse, + "unfurl_links": unfurl_links, + "unfurl_media": unfurl_media, + "link_names": link_names, + "metadata": metadata, + "markdown_text": markdown_text, + } + ) + _parse_web_class_objects(kwargs) + kwargs = _remove_none_values(kwargs) + _warn_if_message_text_content_is_missing("chat.scheduleMessage", kwargs) + # NOTE: intentionally using json over params for the API methods using blocks/attachments + return self.api_call("chat.scheduleMessage", json=kwargs) + + def chat_scheduledMessages_list( + self, + *, + channel: Optional[str] = None, + cursor: Optional[str] = None, + latest: Optional[str] = None, + limit: Optional[int] = None, + oldest: Optional[str] = None, + team_id: Optional[str] = None, + **kwargs, + ) -> SlackResponse: + """Lists all scheduled messages. + https://docs.slack.dev/reference/methods/chat.scheduledMessages.list + """ + kwargs.update( + { + "channel": channel, + "cursor": cursor, + "latest": latest, + "limit": limit, + "oldest": oldest, + "team_id": team_id, + } + ) + return self.api_call("chat.scheduledMessages.list", params=kwargs) + + def chat_startStream( + self, + *, + channel: str, + thread_ts: str, + markdown_text: Optional[str] = None, + recipient_team_id: Optional[str] = None, + recipient_user_id: Optional[str] = None, + **kwargs, + ) -> SlackResponse: + """Starts a new streaming conversation. + https://docs.slack.dev/reference/methods/chat.startStream + """ + kwargs.update( + { + "channel": channel, + "thread_ts": thread_ts, + "markdown_text": markdown_text, + "recipient_team_id": recipient_team_id, + "recipient_user_id": recipient_user_id, + } + ) + kwargs = _remove_none_values(kwargs) + return self.api_call("chat.startStream", json=kwargs) + + def chat_stopStream( + self, + *, + channel: str, + ts: str, + markdown_text: Optional[str] = None, + blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None, + metadata: Optional[Union[Dict, Metadata]] = None, + **kwargs, + ) -> SlackResponse: + """Stops a streaming conversation. + https://docs.slack.dev/reference/methods/chat.stopStream + """ + kwargs.update( + { + "channel": channel, + "ts": ts, + "markdown_text": markdown_text, + "blocks": blocks, + "metadata": metadata, + } + ) + _parse_web_class_objects(kwargs) + kwargs = _remove_none_values(kwargs) + return self.api_call("chat.stopStream", json=kwargs) + + def chat_stream( + self, + *, + buffer_size: int = 256, + channel: str, + thread_ts: str, + recipient_team_id: Optional[str] = None, + recipient_user_id: Optional[str] = None, + **kwargs, + ) -> ChatStream: + """Stream markdown text into a conversation. + + This method starts a new chat stream in a conversation that can be appended to. After appending an entire message, + the stream can be stopped with concluding arguments such as "blocks" for gathering feedback. + + The following methods are used: + + - chat.startStream: Starts a new streaming conversation. + [Reference](https://docs.slack.dev/reference/methods/chat.startStream). + - chat.appendStream: Appends text to an existing streaming conversation. + [Reference](https://docs.slack.dev/reference/methods/chat.appendStream). + - chat.stopStream: Stops a streaming conversation. + [Reference](https://docs.slack.dev/reference/methods/chat.stopStream). + + Args: + buffer_size: The length of markdown_text to buffer in-memory before calling a stream method. Increasing this + value decreases the number of method calls made for the same amount of text, which is useful to avoid rate + limits. Default: 256. + channel: An encoded ID that represents a channel, private group, or DM. + thread_ts: Provide another message's ts value to reply to. Streamed messages should always be replies to a user + request. + recipient_team_id: The encoded ID of the team the user receiving the streaming text belongs to. Required when + streaming to channels. + recipient_user_id: The encoded ID of the user to receive the streaming text. Required when streaming to channels. + **kwargs: Additional arguments passed to the underlying API calls. + + Returns: + ChatStream instance for managing the stream + + Example: + ```python + streamer = client.chat_stream( + channel="C0123456789", + thread_ts="1700000001.123456", + recipient_team_id="T0123456789", + recipient_user_id="U0123456789", + ) + streamer.append(markdown_text="**hello wo") + streamer.append(markdown_text="rld!**") + streamer.stop() + ``` + """ + return ChatStream( + self, + logger=self._logger, + channel=channel, + thread_ts=thread_ts, + recipient_team_id=recipient_team_id, + recipient_user_id=recipient_user_id, + buffer_size=buffer_size, + **kwargs, + ) + + def chat_unfurl( + self, + *, + channel: Optional[str] = None, + ts: Optional[str] = None, + source: Optional[str] = None, + unfurl_id: Optional[str] = None, + unfurls: Optional[Dict[str, Dict]] = None, # or user_auth_* + metadata: Optional[Union[Dict, EventAndEntityMetadata]] = None, + user_auth_blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None, + user_auth_message: Optional[str] = None, + user_auth_required: Optional[bool] = None, + user_auth_url: Optional[str] = None, + **kwargs, + ) -> SlackResponse: + """Provide custom unfurl behavior for user-posted URLs. + https://docs.slack.dev/reference/methods/chat.unfurl + """ + kwargs.update( + { + "channel": channel, + "ts": ts, + "source": source, + "unfurl_id": unfurl_id, + "unfurls": unfurls, + "metadata": metadata, + "user_auth_blocks": user_auth_blocks, + "user_auth_message": user_auth_message, + "user_auth_required": user_auth_required, + "user_auth_url": user_auth_url, + } + ) + _parse_web_class_objects(kwargs) # for user_auth_blocks + kwargs = _remove_none_values(kwargs) + # NOTE: intentionally using json over params for API methods using blocks/attachments + return self.api_call("chat.unfurl", json=kwargs) + + def chat_update( + self, + *, + channel: str, + ts: str, + text: Optional[str] = None, + attachments: Optional[Union[str, Sequence[Union[Dict, Attachment]]]] = None, + blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None, + as_user: Optional[bool] = None, + file_ids: Optional[Union[str, Sequence[str]]] = None, + link_names: Optional[bool] = None, + parse: Optional[str] = None, # none, full + reply_broadcast: Optional[bool] = None, + metadata: Optional[Union[Dict, Metadata]] = None, + markdown_text: Optional[str] = None, + **kwargs, + ) -> SlackResponse: + """Updates a message in a channel. + https://docs.slack.dev/reference/methods/chat.update + """ + kwargs.update( + { + "channel": channel, + "ts": ts, + "text": text, + "attachments": attachments, + "blocks": blocks, + "as_user": as_user, + "link_names": link_names, + "parse": parse, + "reply_broadcast": reply_broadcast, + "metadata": metadata, + "markdown_text": markdown_text, + } + ) + if isinstance(file_ids, (list, tuple)): + kwargs.update({"file_ids": ",".join(file_ids)}) + else: + kwargs.update({"file_ids": file_ids}) + _parse_web_class_objects(kwargs) + kwargs = _remove_none_values(kwargs) + _warn_if_message_text_content_is_missing("chat.update", kwargs) + # NOTE: intentionally using json over params for API methods using blocks/attachments + return self.api_call("chat.update", json=kwargs) + + def conversations_acceptSharedInvite( + self, + *, + channel_name: str, + channel_id: Optional[str] = None, + invite_id: Optional[str] = None, + free_trial_accepted: Optional[bool] = None, + is_private: Optional[bool] = None, + team_id: Optional[str] = None, + **kwargs, + ) -> SlackResponse: + """Accepts an invitation to a Slack Connect channel. + https://docs.slack.dev/reference/methods/conversations.acceptSharedInvite + """ + if channel_id is None and invite_id is None: + raise e.SlackRequestError("Either channel_id or invite_id must be provided.") + kwargs.update( + { + "channel_name": channel_name, + "channel_id": channel_id, + "invite_id": invite_id, + "free_trial_accepted": free_trial_accepted, + "is_private": is_private, + "team_id": team_id, + } + ) + return self.api_call("conversations.acceptSharedInvite", http_verb="POST", params=kwargs) + + def conversations_approveSharedInvite( + self, + *, + invite_id: str, + target_team: Optional[str] = None, + **kwargs, + ) -> SlackResponse: + """Approves an invitation to a Slack Connect channel. + https://docs.slack.dev/reference/methods/conversations.approveSharedInvite + """ + kwargs.update({"invite_id": invite_id, "target_team": target_team}) + return self.api_call("conversations.approveSharedInvite", http_verb="POST", params=kwargs) + + def conversations_archive( + self, + *, + channel: str, + **kwargs, + ) -> SlackResponse: + """Archives a conversation. + https://docs.slack.dev/reference/methods/conversations.archive + """ + kwargs.update({"channel": channel}) + return self.api_call("conversations.archive", params=kwargs) + + def conversations_close( + self, + *, + channel: str, + **kwargs, + ) -> SlackResponse: + """Closes a direct message or multi-person direct message. + https://docs.slack.dev/reference/methods/conversations.close + """ + kwargs.update({"channel": channel}) + return self.api_call("conversations.close", params=kwargs) + + def conversations_create( + self, + *, + name: str, + is_private: Optional[bool] = None, + team_id: Optional[str] = None, + **kwargs, + ) -> SlackResponse: + """Initiates a public or private channel-based conversation + https://docs.slack.dev/reference/methods/conversations.create + """ + kwargs.update({"name": name, "is_private": is_private, "team_id": team_id}) + return self.api_call("conversations.create", params=kwargs) + + def conversations_declineSharedInvite( + self, + *, + invite_id: str, + target_team: Optional[str] = None, + **kwargs, + ) -> SlackResponse: + """Declines a Slack Connect channel invite. + https://docs.slack.dev/reference/methods/conversations.declineSharedInvite + """ + kwargs.update({"invite_id": invite_id, "target_team": target_team}) + return self.api_call("conversations.declineSharedInvite", http_verb="GET", params=kwargs) + + def conversations_externalInvitePermissions_set( + self, *, action: str, channel: str, target_team: str, **kwargs + ) -> SlackResponse: + """Sets a team in a shared External Limited channel to a shared Slack Connect channel or vice versa. + https://docs.slack.dev/reference/methods/conversations.externalInvitePermissions.set + """ + kwargs.update( + { + "action": action, + "channel": channel, + "target_team": target_team, + } + ) + return self.api_call("conversations.externalInvitePermissions.set", params=kwargs) + + def conversations_history( + self, + *, + channel: str, + cursor: Optional[str] = None, + inclusive: Optional[bool] = None, + include_all_metadata: Optional[bool] = None, + latest: Optional[str] = None, + limit: Optional[int] = None, + oldest: Optional[str] = None, + **kwargs, + ) -> SlackResponse: + """Fetches a conversation's history of messages and events. + https://docs.slack.dev/reference/methods/conversations.history + """ + kwargs.update( + { + "channel": channel, + "cursor": cursor, + "inclusive": inclusive, + "include_all_metadata": include_all_metadata, + "limit": limit, + "latest": latest, + "oldest": oldest, + } + ) + return self.api_call("conversations.history", http_verb="GET", params=kwargs) + + def conversations_info( + self, + *, + channel: str, + include_locale: Optional[bool] = None, + include_num_members: Optional[bool] = None, + **kwargs, + ) -> SlackResponse: + """Retrieve information about a conversation. + https://docs.slack.dev/reference/methods/conversations.info + """ + kwargs.update( + { + "channel": channel, + "include_locale": include_locale, + "include_num_members": include_num_members, + } + ) + return self.api_call("conversations.info", http_verb="GET", params=kwargs) + + def conversations_invite( + self, + *, + channel: str, + users: Union[str, Sequence[str]], + force: Optional[bool] = None, + **kwargs, + ) -> SlackResponse: + """Invites users to a channel. + https://docs.slack.dev/reference/methods/conversations.invite + """ + kwargs.update( + { + "channel": channel, + "force": force, + } + ) + if isinstance(users, (list, tuple)): + kwargs.update({"users": ",".join(users)}) + else: + kwargs.update({"users": users}) + return self.api_call("conversations.invite", params=kwargs) + + def conversations_inviteShared( + self, + *, + channel: str, + emails: Optional[Union[str, Sequence[str]]] = None, + user_ids: Optional[Union[str, Sequence[str]]] = None, + **kwargs, + ) -> SlackResponse: + """Sends an invitation to a Slack Connect channel. + https://docs.slack.dev/reference/methods/conversations.inviteShared + """ + if emails is None and user_ids is None: + raise e.SlackRequestError("Either emails or user ids must be provided.") + kwargs.update({"channel": channel}) + if isinstance(emails, (list, tuple)): + kwargs.update({"emails": ",".join(emails)}) + else: + kwargs.update({"emails": emails}) + if isinstance(user_ids, (list, tuple)): + kwargs.update({"user_ids": ",".join(user_ids)}) + else: + kwargs.update({"user_ids": user_ids}) + return self.api_call("conversations.inviteShared", http_verb="GET", params=kwargs) + + def conversations_join( + self, + *, + channel: str, + **kwargs, + ) -> SlackResponse: + """Joins an existing conversation. + https://docs.slack.dev/reference/methods/conversations.join + """ + kwargs.update({"channel": channel}) + return self.api_call("conversations.join", params=kwargs) + + def conversations_kick( + self, + *, + channel: str, + user: str, + **kwargs, + ) -> SlackResponse: + """Removes a user from a conversation. + https://docs.slack.dev/reference/methods/conversations.kick + """ + kwargs.update({"channel": channel, "user": user}) + return self.api_call("conversations.kick", params=kwargs) + + def conversations_leave( + self, + *, + channel: str, + **kwargs, + ) -> SlackResponse: + """Leaves a conversation. + https://docs.slack.dev/reference/methods/conversations.leave + """ + kwargs.update({"channel": channel}) + return self.api_call("conversations.leave", params=kwargs) + + def conversations_list( + self, + *, + cursor: Optional[str] = None, + exclude_archived: Optional[bool] = None, + limit: Optional[int] = None, + team_id: Optional[str] = None, + types: Optional[Union[str, Sequence[str]]] = None, + **kwargs, + ) -> SlackResponse: + """Lists all channels in a Slack team. + https://docs.slack.dev/reference/methods/conversations.list + """ + kwargs.update( + { + "cursor": cursor, + "exclude_archived": exclude_archived, + "limit": limit, + "team_id": team_id, + } + ) + if isinstance(types, (list, tuple)): + kwargs.update({"types": ",".join(types)}) + else: + kwargs.update({"types": types}) + return self.api_call("conversations.list", http_verb="GET", params=kwargs) + + def conversations_listConnectInvites( + self, + *, + count: Optional[int] = None, + cursor: Optional[str] = None, + team_id: Optional[str] = None, + **kwargs, + ) -> SlackResponse: + """List shared channel invites that have been generated + or received but have not yet been approved by all parties. + https://docs.slack.dev/reference/methods/conversations.listConnectInvites + """ + kwargs.update({"count": count, "cursor": cursor, "team_id": team_id}) + return self.api_call("conversations.listConnectInvites", params=kwargs) + + def conversations_mark( + self, + *, + channel: str, + ts: str, + **kwargs, + ) -> SlackResponse: + """Sets the read cursor in a channel. + https://docs.slack.dev/reference/methods/conversations.mark + """ + kwargs.update({"channel": channel, "ts": ts}) + return self.api_call("conversations.mark", params=kwargs) + + def conversations_members( + self, + *, + channel: str, + cursor: Optional[str] = None, + limit: Optional[int] = None, + **kwargs, + ) -> SlackResponse: + """Retrieve members of a conversation. + https://docs.slack.dev/reference/methods/conversations.members + """ + kwargs.update({"channel": channel, "cursor": cursor, "limit": limit}) + return self.api_call("conversations.members", http_verb="GET", params=kwargs) + + def conversations_open( + self, + *, + channel: Optional[str] = None, + return_im: Optional[bool] = None, + users: Optional[Union[str, Sequence[str]]] = None, + **kwargs, + ) -> SlackResponse: + """Opens or resumes a direct message or multi-person direct message. + https://docs.slack.dev/reference/methods/conversations.open + """ + if channel is None and users is None: + raise e.SlackRequestError("Either channel or users must be provided.") + kwargs.update({"channel": channel, "return_im": return_im}) + if isinstance(users, (list, tuple)): + kwargs.update({"users": ",".join(users)}) + else: + kwargs.update({"users": users}) + return self.api_call("conversations.open", params=kwargs) + + def conversations_rename( + self, + *, + channel: str, + name: str, + **kwargs, + ) -> SlackResponse: + """Renames a conversation. + https://docs.slack.dev/reference/methods/conversations.rename + """ + kwargs.update({"channel": channel, "name": name}) + return self.api_call("conversations.rename", params=kwargs) + + def conversations_replies( + self, + *, + channel: str, + ts: str, + cursor: Optional[str] = None, + inclusive: Optional[bool] = None, + include_all_metadata: Optional[bool] = None, + latest: Optional[str] = None, + limit: Optional[int] = None, + oldest: Optional[str] = None, + **kwargs, + ) -> SlackResponse: + """Retrieve a thread of messages posted to a conversation + https://docs.slack.dev/reference/methods/conversations.replies + """ + kwargs.update( + { + "channel": channel, + "ts": ts, + "cursor": cursor, + "inclusive": inclusive, + "include_all_metadata": include_all_metadata, + "limit": limit, + "latest": latest, + "oldest": oldest, + } + ) + return self.api_call("conversations.replies", http_verb="GET", params=kwargs) + + def conversations_requestSharedInvite_approve( + self, + *, + invite_id: str, + channel_id: Optional[str] = None, + is_external_limited: Optional[str] = None, + message: Optional[Dict[str, Any]] = None, + **kwargs, + ) -> SlackResponse: + """Approve a request to add an external user to a channel. This also sends them a Slack Connect invite. + https://docs.slack.dev/reference/methods/conversations.requestSharedInvite.approve + """ + kwargs.update( + { + "invite_id": invite_id, + "channel_id": channel_id, + "is_external_limited": is_external_limited, + } + ) + if message is not None: + kwargs.update({"message": json.dumps(message)}) + return self.api_call("conversations.requestSharedInvite.approve", params=kwargs) + + def conversations_requestSharedInvite_deny( + self, + *, + invite_id: str, + message: Optional[str] = None, + **kwargs, + ) -> SlackResponse: + """Deny a request to invite an external user to a channel. + https://docs.slack.dev/reference/methods/conversations.requestSharedInvite.deny + """ + kwargs.update({"invite_id": invite_id, "message": message}) + return self.api_call("conversations.requestSharedInvite.deny", params=kwargs) + + def conversations_requestSharedInvite_list( + self, + *, + cursor: Optional[str] = None, + include_approved: Optional[bool] = None, + include_denied: Optional[bool] = None, + include_expired: Optional[bool] = None, + invite_ids: Optional[Union[str, Sequence[str]]] = None, + limit: Optional[int] = None, + user_id: Optional[str] = None, + **kwargs, + ) -> SlackResponse: + """Lists requests to add external users to channels with ability to filter. + https://docs.slack.dev/reference/methods/conversations.requestSharedInvite.list + """ + kwargs.update( + { + "cursor": cursor, + "include_approved": include_approved, + "include_denied": include_denied, + "include_expired": include_expired, + "limit": limit, + "user_id": user_id, + } + ) + if invite_ids is not None: + if isinstance(invite_ids, (list, tuple)): + kwargs.update({"invite_ids": ",".join(invite_ids)}) + else: + kwargs.update({"invite_ids": invite_ids}) + return self.api_call("conversations.requestSharedInvite.list", params=kwargs) + + def conversations_setPurpose( + self, + *, + channel: str, + purpose: str, + **kwargs, + ) -> SlackResponse: + """Sets the purpose for a conversation. + https://docs.slack.dev/reference/methods/conversations.setPurpose + """ + kwargs.update({"channel": channel, "purpose": purpose}) + return self.api_call("conversations.setPurpose", params=kwargs) + + def conversations_setTopic( + self, + *, + channel: str, + topic: str, + **kwargs, + ) -> SlackResponse: + """Sets the topic for a conversation. + https://docs.slack.dev/reference/methods/conversations.setTopic + """ + kwargs.update({"channel": channel, "topic": topic}) + return self.api_call("conversations.setTopic", params=kwargs) + + def conversations_unarchive( + self, + *, + channel: str, + **kwargs, + ) -> SlackResponse: + """Reverses conversation archival. + https://docs.slack.dev/reference/methods/conversations.unarchive + """ + kwargs.update({"channel": channel}) + return self.api_call("conversations.unarchive", params=kwargs) + + def conversations_canvases_create( + self, + *, + channel_id: str, + document_content: Dict[str, str], + **kwargs, + ) -> SlackResponse: + """Create a Channel Canvas for a channel + https://docs.slack.dev/reference/methods/conversations.canvases.create + """ + kwargs.update({"channel_id": channel_id, "document_content": document_content}) + return self.api_call("conversations.canvases.create", json=kwargs) + + def dialog_open( + self, + *, + dialog: Dict[str, Any], + trigger_id: str, + **kwargs, + ) -> SlackResponse: + """Open a dialog with a user. + https://docs.slack.dev/reference/methods/dialog.open + """ + kwargs.update({"dialog": dialog, "trigger_id": trigger_id}) + kwargs = _remove_none_values(kwargs) + # NOTE: As the dialog can be a dict, this API call works only with json format. + return self.api_call("dialog.open", json=kwargs) + + def dnd_endDnd( + self, + **kwargs, + ) -> SlackResponse: + """Ends the current user's Do Not Disturb session immediately. + https://docs.slack.dev/reference/methods/dnd.endDnd + """ + return self.api_call("dnd.endDnd", params=kwargs) + + def dnd_endSnooze( + self, + **kwargs, + ) -> SlackResponse: + """Ends the current user's snooze mode immediately. + https://docs.slack.dev/reference/methods/dnd.endSnooze + """ + return self.api_call("dnd.endSnooze", params=kwargs) + + def dnd_info( + self, + *, + team_id: Optional[str] = None, + user: Optional[str] = None, + **kwargs, + ) -> SlackResponse: + """Retrieves a user's current Do Not Disturb status. + https://docs.slack.dev/reference/methods/dnd.info + """ + kwargs.update({"team_id": team_id, "user": user}) + return self.api_call("dnd.info", http_verb="GET", params=kwargs) + + def dnd_setSnooze( + self, + *, + num_minutes: Union[int, str], + **kwargs, + ) -> SlackResponse: + """Turns on Do Not Disturb mode for the current user, or changes its duration. + https://docs.slack.dev/reference/methods/dnd.setSnooze + """ + kwargs.update({"num_minutes": num_minutes}) + return self.api_call("dnd.setSnooze", http_verb="GET", params=kwargs) + + def dnd_teamInfo( + self, + users: Union[str, Sequence[str]], + team_id: Optional[str] = None, + **kwargs, + ) -> SlackResponse: + """Retrieves the Do Not Disturb status for users on a team. + https://docs.slack.dev/reference/methods/dnd.teamInfo + """ + if isinstance(users, (list, tuple)): + kwargs.update({"users": ",".join(users)}) + else: + kwargs.update({"users": users}) + kwargs.update({"team_id": team_id}) + return self.api_call("dnd.teamInfo", http_verb="GET", params=kwargs) + + def emoji_list( + self, + include_categories: Optional[bool] = None, + **kwargs, + ) -> SlackResponse: + """Lists custom emoji for a team. + https://docs.slack.dev/reference/methods/emoji.list + """ + kwargs.update({"include_categories": include_categories}) + return self.api_call("emoji.list", http_verb="GET", params=kwargs) + + def entity_presentDetails( + self, + trigger_id: str, + metadata: Optional[Union[Dict, EntityMetadata]] = None, + user_auth_required: Optional[bool] = None, + user_auth_url: Optional[str] = None, + error: Optional[Dict[str, Any]] = None, + **kwargs, + ) -> SlackResponse: + """Provides entity details for the flexpane. + https://docs.slack.dev/reference/methods/entity.presentDetails/ + """ + kwargs.update({"trigger_id": trigger_id}) + if metadata is not None: + kwargs.update({"metadata": metadata}) + if user_auth_required is not None: + kwargs.update({"user_auth_required": user_auth_required}) + if user_auth_url is not None: + kwargs.update({"user_auth_url": user_auth_url}) + if error is not None: + kwargs.update({"error": error}) + _parse_web_class_objects(kwargs) + return self.api_call("entity.presentDetails", json=kwargs) + + def files_comments_delete( + self, + *, + file: str, + id: str, + **kwargs, + ) -> SlackResponse: + """Deletes an existing comment on a file. + https://docs.slack.dev/reference/methods/files.comments.delete + """ + kwargs.update({"file": file, "id": id}) + return self.api_call("files.comments.delete", params=kwargs) + + def files_delete( + self, + *, + file: str, + **kwargs, + ) -> SlackResponse: + """Deletes a file. + https://docs.slack.dev/reference/methods/files.delete + """ + kwargs.update({"file": file}) + return self.api_call("files.delete", params=kwargs) + + def files_info( + self, + *, + file: str, + count: Optional[int] = None, + cursor: Optional[str] = None, + limit: Optional[int] = None, + page: Optional[int] = None, + **kwargs, + ) -> SlackResponse: + """Gets information about a team file. + https://docs.slack.dev/reference/methods/files.info + """ + kwargs.update( + { + "file": file, + "count": count, + "cursor": cursor, + "limit": limit, + "page": page, + } + ) + return self.api_call("files.info", http_verb="GET", params=kwargs) + + def files_list( + self, + *, + channel: Optional[str] = None, + count: Optional[int] = None, + page: Optional[int] = None, + show_files_hidden_by_limit: Optional[bool] = None, + team_id: Optional[str] = None, + ts_from: Optional[str] = None, + ts_to: Optional[str] = None, + types: Optional[Union[str, Sequence[str]]] = None, + user: Optional[str] = None, + **kwargs, + ) -> SlackResponse: + """Lists & filters team files. + https://docs.slack.dev/reference/methods/files.list + """ + kwargs.update( + { + "channel": channel, + "count": count, + "page": page, + "show_files_hidden_by_limit": show_files_hidden_by_limit, + "team_id": team_id, + "ts_from": ts_from, + "ts_to": ts_to, + "user": user, + } + ) + if isinstance(types, (list, tuple)): + kwargs.update({"types": ",".join(types)}) + else: + kwargs.update({"types": types}) + return self.api_call("files.list", http_verb="GET", params=kwargs) + + def files_remote_info( + self, + *, + external_id: Optional[str] = None, + file: Optional[str] = None, + **kwargs, + ) -> SlackResponse: + """Retrieve information about a remote file added to Slack. + https://docs.slack.dev/reference/methods/files.remote.info + """ + kwargs.update({"external_id": external_id, "file": file}) + return self.api_call("files.remote.info", http_verb="GET", params=kwargs) + + def files_remote_list( + self, + *, + channel: Optional[str] = None, + cursor: Optional[str] = None, + limit: Optional[int] = None, + ts_from: Optional[str] = None, + ts_to: Optional[str] = None, + **kwargs, + ) -> SlackResponse: + """Retrieve information about a remote file added to Slack. + https://docs.slack.dev/reference/methods/files.remote.list + """ + kwargs.update( + { + "channel": channel, + "cursor": cursor, + "limit": limit, + "ts_from": ts_from, + "ts_to": ts_to, + } + ) + return self.api_call("files.remote.list", http_verb="GET", params=kwargs) + + def files_remote_add( + self, + *, + external_id: str, + external_url: str, + title: str, + filetype: Optional[str] = None, + indexable_file_contents: Optional[Union[str, bytes, IOBase]] = None, + preview_image: Optional[Union[str, bytes, IOBase]] = None, + **kwargs, + ) -> SlackResponse: + """Adds a file from a remote service. + https://docs.slack.dev/reference/methods/files.remote.add + """ + kwargs.update( + { + "external_id": external_id, + "external_url": external_url, + "title": title, + "filetype": filetype, + } + ) + files = None + # preview_image (file): Preview of the document via multipart/form-data. + if preview_image is not None or indexable_file_contents is not None: + files = { + "preview_image": preview_image, + "indexable_file_contents": indexable_file_contents, + } + + return self.api_call( + # Intentionally using "POST" method over "GET" here + "files.remote.add", + http_verb="POST", + data=kwargs, + files=files, + ) + + def files_remote_update( + self, + *, + external_id: Optional[str] = None, + external_url: Optional[str] = None, + file: Optional[str] = None, + title: Optional[str] = None, + filetype: Optional[str] = None, + indexable_file_contents: Optional[str] = None, + preview_image: Optional[str] = None, + **kwargs, + ) -> SlackResponse: + """Updates an existing remote file. + https://docs.slack.dev/reference/methods/files.remote.update + """ + kwargs.update( + { + "external_id": external_id, + "external_url": external_url, + "file": file, + "title": title, + "filetype": filetype, + } + ) + files = None + # preview_image (file): Preview of the document via multipart/form-data. + if preview_image is not None or indexable_file_contents is not None: + files = { + "preview_image": preview_image, + "indexable_file_contents": indexable_file_contents, + } + + return self.api_call( + # Intentionally using "POST" method over "GET" here + "files.remote.update", + http_verb="POST", + data=kwargs, + files=files, + ) + + def files_remote_remove( + self, + *, + external_id: Optional[str] = None, + file: Optional[str] = None, + **kwargs, + ) -> SlackResponse: + """Remove a remote file. + https://docs.slack.dev/reference/methods/files.remote.remove + """ + kwargs.update({"external_id": external_id, "file": file}) + return self.api_call("files.remote.remove", http_verb="POST", params=kwargs) + + def files_remote_share( + self, + *, + channels: Union[str, Sequence[str]], + external_id: Optional[str] = None, + file: Optional[str] = None, + **kwargs, + ) -> SlackResponse: + """Share a remote file into a channel. + https://docs.slack.dev/reference/methods/files.remote.share + """ + if external_id is None and file is None: + raise e.SlackRequestError("Either external_id or file must be provided.") + if isinstance(channels, (list, tuple)): + kwargs.update({"channels": ",".join(channels)}) + else: + kwargs.update({"channels": channels}) + kwargs.update({"external_id": external_id, "file": file}) + return self.api_call("files.remote.share", http_verb="GET", params=kwargs) + + def files_revokePublicURL( + self, + *, + file: str, + **kwargs, + ) -> SlackResponse: + """Revokes public/external sharing access for a file + https://docs.slack.dev/reference/methods/files.revokePublicURL + """ + kwargs.update({"file": file}) + return self.api_call("files.revokePublicURL", params=kwargs) + + def files_sharedPublicURL( + self, + *, + file: str, + **kwargs, + ) -> SlackResponse: + """Enables a file for public/external sharing. + https://docs.slack.dev/reference/methods/files.sharedPublicURL + """ + kwargs.update({"file": file}) + return self.api_call("files.sharedPublicURL", params=kwargs) + + def files_upload( + self, + *, + file: Optional[Union[str, bytes, IOBase]] = None, + content: Optional[Union[str, bytes]] = None, + filename: Optional[str] = None, + filetype: Optional[str] = None, + initial_comment: Optional[str] = None, + thread_ts: Optional[str] = None, + title: Optional[str] = None, + channels: Optional[Union[str, Sequence[str]]] = None, + **kwargs, + ) -> SlackResponse: + """Uploads or creates a file. + https://docs.slack.dev/reference/methods/files.upload + """ + _print_files_upload_v2_suggestion() + + if file is None and content is None: + raise e.SlackRequestError("The file or content argument must be specified.") + if file is not None and content is not None: + raise e.SlackRequestError("You cannot specify both the file and the content argument.") + + if isinstance(channels, (list, tuple)): + kwargs.update({"channels": ",".join(channels)}) + else: + kwargs.update({"channels": channels}) + kwargs.update( + { + "filename": filename, + "filetype": filetype, + "initial_comment": initial_comment, + "thread_ts": thread_ts, + "title": title, + } + ) + if file: + if kwargs.get("filename") is None and isinstance(file, str): + # use the local filename if filename is missing + if kwargs.get("filename") is None: + kwargs["filename"] = file.split(os.path.sep)[-1] + return self.api_call("files.upload", files={"file": file}, data=kwargs) + else: + kwargs["content"] = content + return self.api_call("files.upload", data=kwargs) + + def files_upload_v2( + self, + *, + # for sending a single file + filename: Optional[str] = None, # you can skip this only when sending along with content parameter + file: Optional[Union[str, bytes, IOBase, os.PathLike]] = None, + content: Optional[Union[str, bytes]] = None, + title: Optional[str] = None, + alt_txt: Optional[str] = None, + snippet_type: Optional[str] = None, + # To upload multiple files at a time + file_uploads: Optional[List[Dict[str, Any]]] = None, + channel: Optional[str] = None, + channels: Optional[List[str]] = None, + initial_comment: Optional[str] = None, + thread_ts: Optional[str] = None, + request_file_info: bool = True, # since v3.23, this flag is no longer necessary + **kwargs, + ) -> SlackResponse: + """This wrapper method provides an easy way to upload files using the following endpoints: + + - step1: https://docs.slack.dev/reference/methods/files.getUploadURLExternal + + - step2: "https://files.slack.com/upload/v1/..." URLs returned from files.getUploadURLExternal API + + - step3: https://docs.slack.dev/reference/methods/files.completeUploadExternal + and https://docs.slack.dev/reference/methods/files.info + + """ + if file is None and content is None and file_uploads is None: + raise e.SlackRequestError("Any of file, content, and file_uploads must be specified.") + if file is not None and content is not None: + raise e.SlackRequestError("You cannot specify both the file and the content argument.") + + # deprecated arguments: + filetype = kwargs.get("filetype") + + if filetype is not None: + warnings.warn("The filetype parameter is no longer supported. Please remove it from the arguments.") + + # step1: files.getUploadURLExternal per file + files: List[Dict[str, Any]] = [] + if file_uploads is not None: + for f in file_uploads: + files.append(_to_v2_file_upload_item(f)) + else: + f = _to_v2_file_upload_item( + { + "filename": filename, + "file": file, + "content": content, + "title": title, + "alt_txt": alt_txt, + "snippet_type": snippet_type, + } + ) + files.append(f) + + for f in files: + url_response = self.files_getUploadURLExternal( + filename=f.get("filename"), # type: ignore[arg-type] + length=f.get("length"), # type: ignore[arg-type] + alt_txt=f.get("alt_txt"), + snippet_type=f.get("snippet_type"), + token=kwargs.get("token"), + ) + _validate_for_legacy_client(url_response) + f["file_id"] = url_response.get("file_id") # type: ignore[union-attr, unused-ignore] + f["upload_url"] = url_response.get("upload_url") # type: ignore[union-attr, unused-ignore] + + # step2: "https://files.slack.com/upload/v1/..." per file + for f in files: + upload_result = self._upload_file( + url=f["upload_url"], + data=f["data"], + logger=self._logger, + timeout=self.timeout, + proxy=self.proxy, + ssl=self.ssl, + ) + if upload_result.status != 200: + status = upload_result.status + body = upload_result.body + message = ( + "Failed to upload a file " + f"(status: {status}, body: {body}, filename: {f.get('filename')}, title: {f.get('title')})" + ) + raise e.SlackRequestError(message) + + # step3: files.completeUploadExternal with all the sets of (file_id + title) + completion = self.files_completeUploadExternal( + files=[{"id": f["file_id"], "title": f["title"]} for f in files], + channel_id=channel, + channels=channels, + initial_comment=initial_comment, + thread_ts=thread_ts, + **kwargs, + ) + if len(completion.get("files")) == 1: # type: ignore[arg-type, union-attr, unused-ignore] + completion.data["file"] = completion.get("files")[0] # type: ignore[index, union-attr, unused-ignore] + return completion + + def files_getUploadURLExternal( + self, + *, + filename: str, + length: int, + alt_txt: Optional[str] = None, + snippet_type: Optional[str] = None, + **kwargs, + ) -> SlackResponse: + """Gets a URL for an edge external upload. + https://docs.slack.dev/reference/methods/files.getUploadURLExternal + """ + kwargs.update( + { + "filename": filename, + "length": length, + "alt_txt": alt_txt, + "snippet_type": snippet_type, + } + ) + return self.api_call("files.getUploadURLExternal", params=kwargs) + + def files_completeUploadExternal( + self, + *, + files: List[Dict[str, str]], + channel_id: Optional[str] = None, + channels: Optional[List[str]] = None, + initial_comment: Optional[str] = None, + thread_ts: Optional[str] = None, + **kwargs, + ) -> SlackResponse: + """Finishes an upload started with files.getUploadURLExternal. + https://docs.slack.dev/reference/methods/files.completeUploadExternal + """ + _files = [{k: v for k, v in f.items() if v is not None} for f in files] + kwargs.update( + { + "files": json.dumps(_files), + "channel_id": channel_id, + "initial_comment": initial_comment, + "thread_ts": thread_ts, + } + ) + if channels: + kwargs["channels"] = ",".join(channels) + return self.api_call("files.completeUploadExternal", params=kwargs) + + def functions_completeSuccess( + self, + *, + function_execution_id: str, + outputs: Dict[str, Any], + **kwargs, + ) -> SlackResponse: + """Signal the successful completion of a function + https://docs.slack.dev/reference/methods/functions.completeSuccess + """ + kwargs.update({"function_execution_id": function_execution_id, "outputs": json.dumps(outputs)}) + return self.api_call("functions.completeSuccess", params=kwargs) + + def functions_completeError( + self, + *, + function_execution_id: str, + error: str, + **kwargs, + ) -> SlackResponse: + """Signal the failure to execute a function + https://docs.slack.dev/reference/methods/functions.completeError + """ + kwargs.update({"function_execution_id": function_execution_id, "error": error}) + return self.api_call("functions.completeError", params=kwargs) + + # -------------------------- + # Deprecated: groups.* + # You can use conversations.* APIs instead. + # https://docs.slack.dev/changelog/2020-01-deprecating-antecedents-to-the-conversations-api/ + # -------------------------- + + def groups_archive( + self, + *, + channel: str, + **kwargs, + ) -> SlackResponse: + """Archives a private channel.""" + kwargs.update({"channel": channel}) + kwargs = _remove_none_values(kwargs) + return self.api_call("groups.archive", json=kwargs) + + def groups_create( + self, + *, + name: str, + **kwargs, + ) -> SlackResponse: + """Creates a private channel.""" + kwargs.update({"name": name}) + kwargs = _remove_none_values(kwargs) + return self.api_call("groups.create", json=kwargs) + + def groups_createChild( + self, + *, + channel: str, + **kwargs, + ) -> SlackResponse: + """Clones and archives a private channel.""" + kwargs.update({"channel": channel}) + return self.api_call("groups.createChild", http_verb="GET", params=kwargs) + + def groups_history( + self, + *, + channel: str, + **kwargs, + ) -> SlackResponse: + """Fetches history of messages and events from a private channel.""" + kwargs.update({"channel": channel}) + return self.api_call("groups.history", http_verb="GET", params=kwargs) + + def groups_info( + self, + *, + channel: str, + **kwargs, + ) -> SlackResponse: + """Gets information about a private channel.""" + kwargs.update({"channel": channel}) + return self.api_call("groups.info", http_verb="GET", params=kwargs) + + def groups_invite( + self, + *, + channel: str, + user: str, + **kwargs, + ) -> SlackResponse: + """Invites a user to a private channel.""" + kwargs.update({"channel": channel, "user": user}) + kwargs = _remove_none_values(kwargs) + return self.api_call("groups.invite", json=kwargs) + + def groups_kick( + self, + *, + channel: str, + user: str, + **kwargs, + ) -> SlackResponse: + """Removes a user from a private channel.""" + kwargs.update({"channel": channel, "user": user}) + kwargs = _remove_none_values(kwargs) + return self.api_call("groups.kick", json=kwargs) + + def groups_leave( + self, + *, + channel: str, + **kwargs, + ) -> SlackResponse: + """Leaves a private channel.""" + kwargs.update({"channel": channel}) + kwargs = _remove_none_values(kwargs) + return self.api_call("groups.leave", json=kwargs) + + def groups_list( + self, + **kwargs, + ) -> SlackResponse: + """Lists private channels that the calling user has access to.""" + return self.api_call("groups.list", http_verb="GET", params=kwargs) + + def groups_mark( + self, + *, + channel: str, + ts: str, + **kwargs, + ) -> SlackResponse: + """Sets the read cursor in a private channel.""" + kwargs.update({"channel": channel, "ts": ts}) + kwargs = _remove_none_values(kwargs) + return self.api_call("groups.mark", json=kwargs) + + def groups_open( + self, + *, + channel: str, + **kwargs, + ) -> SlackResponse: + """Opens a private channel.""" + kwargs.update({"channel": channel}) + kwargs = _remove_none_values(kwargs) + return self.api_call("groups.open", json=kwargs) + + def groups_rename( + self, + *, + channel: str, + name: str, + **kwargs, + ) -> SlackResponse: + """Renames a private channel.""" + kwargs.update({"channel": channel, "name": name}) + kwargs = _remove_none_values(kwargs) + return self.api_call("groups.rename", json=kwargs) + + def groups_replies( + self, + *, + channel: str, + thread_ts: str, + **kwargs, + ) -> SlackResponse: + """Retrieve a thread of messages posted to a private channel""" + kwargs.update({"channel": channel, "thread_ts": thread_ts}) + return self.api_call("groups.replies", http_verb="GET", params=kwargs) + + def groups_setPurpose( + self, + *, + channel: str, + purpose: str, + **kwargs, + ) -> SlackResponse: + """Sets the purpose for a private channel.""" + kwargs.update({"channel": channel, "purpose": purpose}) + kwargs = _remove_none_values(kwargs) + return self.api_call("groups.setPurpose", json=kwargs) + + def groups_setTopic( + self, + *, + channel: str, + topic: str, + **kwargs, + ) -> SlackResponse: + """Sets the topic for a private channel.""" + kwargs.update({"channel": channel, "topic": topic}) + kwargs = _remove_none_values(kwargs) + return self.api_call("groups.setTopic", json=kwargs) + + def groups_unarchive( + self, + *, + channel: str, + **kwargs, + ) -> SlackResponse: + """Unarchives a private channel.""" + kwargs.update({"channel": channel}) + kwargs = _remove_none_values(kwargs) + return self.api_call("groups.unarchive", json=kwargs) + + # -------------------------- + # Deprecated: im.* + # You can use conversations.* APIs instead. + # https://docs.slack.dev/changelog/2020-01-deprecating-antecedents-to-the-conversations-api/ + # -------------------------- + + def im_close( + self, + *, + channel: str, + **kwargs, + ) -> SlackResponse: + """Close a direct message channel.""" + kwargs.update({"channel": channel}) + kwargs = _remove_none_values(kwargs) + return self.api_call("im.close", json=kwargs) + + def im_history( + self, + *, + channel: str, + **kwargs, + ) -> SlackResponse: + """Fetches history of messages and events from direct message channel.""" + kwargs.update({"channel": channel}) + return self.api_call("im.history", http_verb="GET", params=kwargs) + + def im_list( + self, + **kwargs, + ) -> SlackResponse: + """Lists direct message channels for the calling user.""" + return self.api_call("im.list", http_verb="GET", params=kwargs) + + def im_mark( + self, + *, + channel: str, + ts: str, + **kwargs, + ) -> SlackResponse: + """Sets the read cursor in a direct message channel.""" + kwargs.update({"channel": channel, "ts": ts}) + kwargs = _remove_none_values(kwargs) + return self.api_call("im.mark", json=kwargs) + + def im_open( + self, + *, + user: str, + **kwargs, + ) -> SlackResponse: + """Opens a direct message channel.""" + kwargs.update({"user": user}) + kwargs = _remove_none_values(kwargs) + return self.api_call("im.open", json=kwargs) + + def im_replies( + self, + *, + channel: str, + thread_ts: str, + **kwargs, + ) -> SlackResponse: + """Retrieve a thread of messages posted to a direct message conversation""" + kwargs.update({"channel": channel, "thread_ts": thread_ts}) + return self.api_call("im.replies", http_verb="GET", params=kwargs) + + # -------------------------- + + def migration_exchange( + self, + *, + users: Union[str, Sequence[str]], + team_id: Optional[str] = None, + to_old: Optional[bool] = None, + **kwargs, + ) -> SlackResponse: + """For Enterprise Grid workspaces, map local user IDs to global user IDs + https://docs.slack.dev/reference/methods/migration.exchange + """ + if isinstance(users, (list, tuple)): + kwargs.update({"users": ",".join(users)}) + else: + kwargs.update({"users": users}) + kwargs.update({"team_id": team_id, "to_old": to_old}) + return self.api_call("migration.exchange", http_verb="GET", params=kwargs) + + # -------------------------- + # Deprecated: mpim.* + # You can use conversations.* APIs instead. + # https://docs.slack.dev/changelog/2020-01-deprecating-antecedents-to-the-conversations-api/ + # -------------------------- + + def mpim_close( + self, + *, + channel: str, + **kwargs, + ) -> SlackResponse: + """Closes a multiparty direct message channel.""" + kwargs.update({"channel": channel}) + kwargs = _remove_none_values(kwargs) + return self.api_call("mpim.close", json=kwargs) + + def mpim_history( + self, + *, + channel: str, + **kwargs, + ) -> SlackResponse: + """Fetches history of messages and events from a multiparty direct message.""" + kwargs.update({"channel": channel}) + return self.api_call("mpim.history", http_verb="GET", params=kwargs) + + def mpim_list( + self, + **kwargs, + ) -> SlackResponse: + """Lists multiparty direct message channels for the calling user.""" + return self.api_call("mpim.list", http_verb="GET", params=kwargs) + + def mpim_mark( + self, + *, + channel: str, + ts: str, + **kwargs, + ) -> SlackResponse: + """Sets the read cursor in a multiparty direct message channel.""" + kwargs.update({"channel": channel, "ts": ts}) + kwargs = _remove_none_values(kwargs) + return self.api_call("mpim.mark", json=kwargs) + + def mpim_open( + self, + *, + users: Union[str, Sequence[str]], + **kwargs, + ) -> SlackResponse: + """This method opens a multiparty direct message.""" + if isinstance(users, (list, tuple)): + kwargs.update({"users": ",".join(users)}) + else: + kwargs.update({"users": users}) + return self.api_call("mpim.open", params=kwargs) + + def mpim_replies( + self, + *, + channel: str, + thread_ts: str, + **kwargs, + ) -> SlackResponse: + """Retrieve a thread of messages posted to a direct message conversation from a + multiparty direct message. + """ + kwargs.update({"channel": channel, "thread_ts": thread_ts}) + return self.api_call("mpim.replies", http_verb="GET", params=kwargs) + + # -------------------------- + + def oauth_v2_access( + self, + *, + client_id: str, + client_secret: str, + # This field is required when processing the OAuth redirect URL requests + # while it's absent for token rotation + code: Optional[str] = None, + redirect_uri: Optional[str] = None, + # This field is required for token rotation + grant_type: Optional[str] = None, + # This field is required for token rotation + refresh_token: Optional[str] = None, + **kwargs, + ) -> SlackResponse: + """Exchanges a temporary OAuth verifier code for an access token. + https://docs.slack.dev/reference/methods/oauth.v2.access + """ + if redirect_uri is not None: + kwargs.update({"redirect_uri": redirect_uri}) + if code is not None: + kwargs.update({"code": code}) + if grant_type is not None: + kwargs.update({"grant_type": grant_type}) + if refresh_token is not None: + kwargs.update({"refresh_token": refresh_token}) + return self.api_call( + "oauth.v2.access", + data=kwargs, + auth={"client_id": client_id, "client_secret": client_secret}, + ) + + def oauth_access( + self, + *, + client_id: str, + client_secret: str, + code: str, + redirect_uri: Optional[str] = None, + **kwargs, + ) -> SlackResponse: + """Exchanges a temporary OAuth verifier code for an access token. + https://docs.slack.dev/reference/methods/oauth.access + """ + if redirect_uri is not None: + kwargs.update({"redirect_uri": redirect_uri}) + kwargs.update({"code": code}) + return self.api_call( + "oauth.access", + data=kwargs, + auth={"client_id": client_id, "client_secret": client_secret}, + ) + + def oauth_v2_exchange( + self, + *, + token: str, + client_id: str, + client_secret: str, + **kwargs, + ) -> SlackResponse: + """Exchanges a legacy access token for a new expiring access token and refresh token + https://docs.slack.dev/reference/methods/oauth.v2.exchange + """ + kwargs.update({"client_id": client_id, "client_secret": client_secret, "token": token}) + return self.api_call("oauth.v2.exchange", params=kwargs) + + def openid_connect_token( + self, + client_id: str, + client_secret: str, + code: Optional[str] = None, + redirect_uri: Optional[str] = None, + grant_type: Optional[str] = None, + refresh_token: Optional[str] = None, + **kwargs, + ) -> SlackResponse: + """Exchanges a temporary OAuth verifier code for an access token for Sign in with Slack. + https://docs.slack.dev/reference/methods/openid.connect.token + """ + if redirect_uri is not None: + kwargs.update({"redirect_uri": redirect_uri}) + if code is not None: + kwargs.update({"code": code}) + if grant_type is not None: + kwargs.update({"grant_type": grant_type}) + if refresh_token is not None: + kwargs.update({"refresh_token": refresh_token}) + return self.api_call( + "openid.connect.token", + data=kwargs, + auth={"client_id": client_id, "client_secret": client_secret}, + ) + + def openid_connect_userInfo( + self, + **kwargs, + ) -> SlackResponse: + """Get the identity of a user who has authorized Sign in with Slack. + https://docs.slack.dev/reference/methods/openid.connect.userInfo + """ + return self.api_call("openid.connect.userInfo", params=kwargs) + + def pins_add( + self, + *, + channel: str, + timestamp: Optional[str] = None, + **kwargs, + ) -> SlackResponse: + """Pins an item to a channel. + https://docs.slack.dev/reference/methods/pins.add + """ + kwargs.update({"channel": channel, "timestamp": timestamp}) + return self.api_call("pins.add", params=kwargs) + + def pins_list( + self, + *, + channel: str, + **kwargs, + ) -> SlackResponse: + """Lists items pinned to a channel. + https://docs.slack.dev/reference/methods/pins.list + """ + kwargs.update({"channel": channel}) + return self.api_call("pins.list", http_verb="GET", params=kwargs) + + def pins_remove( + self, + *, + channel: str, + timestamp: Optional[str] = None, + **kwargs, + ) -> SlackResponse: + """Un-pins an item from a channel. + https://docs.slack.dev/reference/methods/pins.remove + """ + kwargs.update({"channel": channel, "timestamp": timestamp}) + return self.api_call("pins.remove", params=kwargs) + + def reactions_add( + self, + *, + channel: str, + name: str, + timestamp: str, + **kwargs, + ) -> SlackResponse: + """Adds a reaction to an item. + https://docs.slack.dev/reference/methods/reactions.add + """ + kwargs.update({"channel": channel, "name": name, "timestamp": timestamp}) + return self.api_call("reactions.add", params=kwargs) + + def reactions_get( + self, + *, + channel: Optional[str] = None, + file: Optional[str] = None, + file_comment: Optional[str] = None, + full: Optional[bool] = None, + timestamp: Optional[str] = None, + **kwargs, + ) -> SlackResponse: + """Gets reactions for an item. + https://docs.slack.dev/reference/methods/reactions.get + """ + kwargs.update( + { + "channel": channel, + "file": file, + "file_comment": file_comment, + "full": full, + "timestamp": timestamp, + } + ) + return self.api_call("reactions.get", http_verb="GET", params=kwargs) + + def reactions_list( + self, + *, + count: Optional[int] = None, + cursor: Optional[str] = None, + full: Optional[bool] = None, + limit: Optional[int] = None, + page: Optional[int] = None, + team_id: Optional[str] = None, + user: Optional[str] = None, + **kwargs, + ) -> SlackResponse: + """Lists reactions made by a user. + https://docs.slack.dev/reference/methods/reactions.list + """ + kwargs.update( + { + "count": count, + "cursor": cursor, + "full": full, + "limit": limit, + "page": page, + "team_id": team_id, + "user": user, + } + ) + return self.api_call("reactions.list", http_verb="GET", params=kwargs) + + def reactions_remove( + self, + *, + name: str, + channel: Optional[str] = None, + file: Optional[str] = None, + file_comment: Optional[str] = None, + timestamp: Optional[str] = None, + **kwargs, + ) -> SlackResponse: + """Removes a reaction from an item. + https://docs.slack.dev/reference/methods/reactions.remove + """ + kwargs.update( + { + "name": name, + "channel": channel, + "file": file, + "file_comment": file_comment, + "timestamp": timestamp, + } + ) + return self.api_call("reactions.remove", params=kwargs) + + def reminders_add( + self, + *, + text: str, + time: str, + team_id: Optional[str] = None, + user: Optional[str] = None, + recurrence: Optional[str] = None, + **kwargs, + ) -> SlackResponse: + """Creates a reminder. + https://docs.slack.dev/reference/methods/reminders.add + """ + kwargs.update( + { + "text": text, + "time": time, + "team_id": team_id, + "user": user, + "recurrence": recurrence, + } + ) + return self.api_call("reminders.add", params=kwargs) + + def reminders_complete( + self, + *, + reminder: str, + team_id: Optional[str] = None, + **kwargs, + ) -> SlackResponse: + """Marks a reminder as complete. + https://docs.slack.dev/reference/methods/reminders.complete + """ + kwargs.update({"reminder": reminder, "team_id": team_id}) + return self.api_call("reminders.complete", params=kwargs) + + def reminders_delete( + self, + *, + reminder: str, + team_id: Optional[str] = None, + **kwargs, + ) -> SlackResponse: + """Deletes a reminder. + https://docs.slack.dev/reference/methods/reminders.delete + """ + kwargs.update({"reminder": reminder, "team_id": team_id}) + return self.api_call("reminders.delete", params=kwargs) + + def reminders_info( + self, + *, + reminder: str, + team_id: Optional[str] = None, + **kwargs, + ) -> SlackResponse: + """Gets information about a reminder. + https://docs.slack.dev/reference/methods/reminders.info + """ + kwargs.update({"reminder": reminder, "team_id": team_id}) + return self.api_call("reminders.info", http_verb="GET", params=kwargs) + + def reminders_list( + self, + *, + team_id: Optional[str] = None, + **kwargs, + ) -> SlackResponse: + """Lists all reminders created by or for a given user. + https://docs.slack.dev/reference/methods/reminders.list + """ + kwargs.update({"team_id": team_id}) + return self.api_call("reminders.list", http_verb="GET", params=kwargs) + + def rtm_connect( + self, + *, + batch_presence_aware: Optional[bool] = None, + presence_sub: Optional[bool] = None, + **kwargs, + ) -> SlackResponse: + """Starts a Real Time Messaging session. + https://docs.slack.dev/reference/methods/rtm.connect + """ + kwargs.update({"batch_presence_aware": batch_presence_aware, "presence_sub": presence_sub}) + return self.api_call("rtm.connect", http_verb="GET", params=kwargs) + + def rtm_start( + self, + *, + batch_presence_aware: Optional[bool] = None, + include_locale: Optional[bool] = None, + mpim_aware: Optional[bool] = None, + no_latest: Optional[bool] = None, + no_unreads: Optional[bool] = None, + presence_sub: Optional[bool] = None, + simple_latest: Optional[bool] = None, + **kwargs, + ) -> SlackResponse: + """Starts a Real Time Messaging session. + https://docs.slack.dev/reference/methods/rtm.start + """ + kwargs.update( + { + "batch_presence_aware": batch_presence_aware, + "include_locale": include_locale, + "mpim_aware": mpim_aware, + "no_latest": no_latest, + "no_unreads": no_unreads, + "presence_sub": presence_sub, + "simple_latest": simple_latest, + } + ) + return self.api_call("rtm.start", http_verb="GET", params=kwargs) + + def search_all( + self, + *, + query: str, + count: Optional[int] = None, + highlight: Optional[bool] = None, + page: Optional[int] = None, + sort: Optional[str] = None, + sort_dir: Optional[str] = None, + team_id: Optional[str] = None, + **kwargs, + ) -> SlackResponse: + """Searches for messages and files matching a query. + https://docs.slack.dev/reference/methods/search.all + """ + kwargs.update( + { + "query": query, + "count": count, + "highlight": highlight, + "page": page, + "sort": sort, + "sort_dir": sort_dir, + "team_id": team_id, + } + ) + return self.api_call("search.all", http_verb="GET", params=kwargs) + + def search_files( + self, + *, + query: str, + count: Optional[int] = None, + highlight: Optional[bool] = None, + page: Optional[int] = None, + sort: Optional[str] = None, + sort_dir: Optional[str] = None, + team_id: Optional[str] = None, + **kwargs, + ) -> SlackResponse: + """Searches for files matching a query. + https://docs.slack.dev/reference/methods/search.files + """ + kwargs.update( + { + "query": query, + "count": count, + "highlight": highlight, + "page": page, + "sort": sort, + "sort_dir": sort_dir, + "team_id": team_id, + } + ) + return self.api_call("search.files", http_verb="GET", params=kwargs) + + def search_messages( + self, + *, + query: str, + count: Optional[int] = None, + cursor: Optional[str] = None, + highlight: Optional[bool] = None, + page: Optional[int] = None, + sort: Optional[str] = None, + sort_dir: Optional[str] = None, + team_id: Optional[str] = None, + **kwargs, + ) -> SlackResponse: + """Searches for messages matching a query. + https://docs.slack.dev/reference/methods/search.messages + """ + kwargs.update( + { + "query": query, + "count": count, + "cursor": cursor, + "highlight": highlight, + "page": page, + "sort": sort, + "sort_dir": sort_dir, + "team_id": team_id, + } + ) + return self.api_call("search.messages", http_verb="GET", params=kwargs) + + def slackLists_access_delete( + self, + *, + list_id: str, + channel_ids: Optional[List[str]] = None, + user_ids: Optional[List[str]] = None, + **kwargs, + ) -> SlackResponse: + """Revoke access to a List for specified entities. + https://docs.slack.dev/reference/methods/slackLists.access.delete + """ + kwargs.update({"list_id": list_id, "channel_ids": channel_ids, "user_ids": user_ids}) + kwargs = _remove_none_values(kwargs) + return self.api_call("slackLists.access.delete", json=kwargs) + + def slackLists_access_set( + self, + *, + list_id: str, + access_level: str, + channel_ids: Optional[List[str]] = None, + user_ids: Optional[List[str]] = None, + **kwargs, + ) -> SlackResponse: + """Set the access level to a List for specified entities. + https://docs.slack.dev/reference/methods/slackLists.access.set + """ + kwargs.update({"list_id": list_id, "access_level": access_level, "channel_ids": channel_ids, "user_ids": user_ids}) + kwargs = _remove_none_values(kwargs) + return self.api_call("slackLists.access.set", json=kwargs) + + def slackLists_create( + self, + *, + name: str, + description_blocks: Optional[Union[str, Sequence[Union[Dict, RichTextBlock]]]] = None, + schema: Optional[List[Dict[str, Any]]] = None, + copy_from_list_id: Optional[str] = None, + include_copied_list_records: Optional[bool] = None, + todo_mode: Optional[bool] = None, + **kwargs, + ) -> SlackResponse: + """Creates a List. + https://docs.slack.dev/reference/methods/slackLists.create + """ + kwargs.update( + { + "name": name, + "description_blocks": description_blocks, + "schema": schema, + "copy_from_list_id": copy_from_list_id, + "include_copied_list_records": include_copied_list_records, + "todo_mode": todo_mode, + } + ) + kwargs = _remove_none_values(kwargs) + return self.api_call("slackLists.create", json=kwargs) + + def slackLists_download_get( + self, + *, + list_id: str, + job_id: str, + **kwargs, + ) -> SlackResponse: + """Retrieve List download URL from an export job to download List contents. + https://docs.slack.dev/reference/methods/slackLists.download.get + """ + kwargs.update( + { + "list_id": list_id, + "job_id": job_id, + } + ) + kwargs = _remove_none_values(kwargs) + return self.api_call("slackLists.download.get", json=kwargs) + + def slackLists_download_start( + self, + *, + list_id: str, + include_archived: Optional[bool] = None, + **kwargs, + ) -> SlackResponse: + """Initiate a job to export List contents. + https://docs.slack.dev/reference/methods/slackLists.download.start + """ + kwargs.update( + { + "list_id": list_id, + "include_archived": include_archived, + } + ) + kwargs = _remove_none_values(kwargs) + return self.api_call("slackLists.download.start", json=kwargs) + + def slackLists_items_create( + self, + *, + list_id: str, + duplicated_item_id: Optional[str] = None, + parent_item_id: Optional[str] = None, + initial_fields: Optional[List[Dict[str, Any]]] = None, + **kwargs, + ) -> SlackResponse: + """Add a new item to an existing List. + https://docs.slack.dev/reference/methods/slackLists.items.create + """ + kwargs.update( + { + "list_id": list_id, + "duplicated_item_id": duplicated_item_id, + "parent_item_id": parent_item_id, + "initial_fields": initial_fields, + } + ) + kwargs = _remove_none_values(kwargs) + return self.api_call("slackLists.items.create", json=kwargs) + + def slackLists_items_delete( + self, + *, + list_id: str, + id: str, + **kwargs, + ) -> SlackResponse: + """Deletes an item from an existing List. + https://docs.slack.dev/reference/methods/slackLists.items.delete + """ + kwargs.update( + { + "list_id": list_id, + "id": id, + } + ) + kwargs = _remove_none_values(kwargs) + return self.api_call("slackLists.items.delete", json=kwargs) + + def slackLists_items_deleteMultiple( + self, + *, + list_id: str, + ids: List[str], + **kwargs, + ) -> SlackResponse: + """Deletes multiple items from an existing List. + https://docs.slack.dev/reference/methods/slackLists.items.deleteMultiple + """ + kwargs.update( + { + "list_id": list_id, + "ids": ids, + } + ) + kwargs = _remove_none_values(kwargs) + return self.api_call("slackLists.items.deleteMultiple", json=kwargs) + + def slackLists_items_info( + self, + *, + list_id: str, + id: str, + include_is_subscribed: Optional[bool] = None, + **kwargs, + ) -> SlackResponse: + """Get a row from a List. + https://docs.slack.dev/reference/methods/slackLists.items.info + """ + kwargs.update( + { + "list_id": list_id, + "id": id, + "include_is_subscribed": include_is_subscribed, + } + ) + kwargs = _remove_none_values(kwargs) + return self.api_call("slackLists.items.info", json=kwargs) + + def slackLists_items_list( + self, + *, + list_id: str, + limit: Optional[int] = None, + cursor: Optional[str] = None, + archived: Optional[bool] = None, + **kwargs, + ) -> SlackResponse: + """Get records from a List. + https://docs.slack.dev/reference/methods/slackLists.items.list + """ + kwargs.update( + { + "list_id": list_id, + "limit": limit, + "cursor": cursor, + "archived": archived, + } + ) + kwargs = _remove_none_values(kwargs) + return self.api_call("slackLists.items.list", json=kwargs) + + def slackLists_items_update( + self, + *, + list_id: str, + cells: List[Dict[str, Any]], + **kwargs, + ) -> SlackResponse: + """Updates cells in a List. + https://docs.slack.dev/reference/methods/slackLists.items.update + """ + kwargs.update( + { + "list_id": list_id, + "cells": cells, + } + ) + kwargs = _remove_none_values(kwargs) + return self.api_call("slackLists.items.update", json=kwargs) + + def slackLists_update( + self, + *, + id: str, + name: Optional[str] = None, + description_blocks: Optional[Union[str, Sequence[Union[Dict, RichTextBlock]]]] = None, + todo_mode: Optional[bool] = None, + **kwargs, + ) -> SlackResponse: + """Update a List. + https://docs.slack.dev/reference/methods/slackLists.update + """ + kwargs.update( + { + "id": id, + "name": name, + "description_blocks": description_blocks, + "todo_mode": todo_mode, + } + ) + kwargs = _remove_none_values(kwargs) + return self.api_call("slackLists.update", json=kwargs) + + def stars_add( + self, + *, + channel: Optional[str] = None, + file: Optional[str] = None, + file_comment: Optional[str] = None, + timestamp: Optional[str] = None, + **kwargs, + ) -> SlackResponse: + """Adds a star to an item. + https://docs.slack.dev/reference/methods/stars.add + """ + kwargs.update( + { + "channel": channel, + "file": file, + "file_comment": file_comment, + "timestamp": timestamp, + } + ) + return self.api_call("stars.add", params=kwargs) + + def stars_list( + self, + *, + count: Optional[int] = None, + cursor: Optional[str] = None, + limit: Optional[int] = None, + page: Optional[int] = None, + team_id: Optional[str] = None, + **kwargs, + ) -> SlackResponse: + """Lists stars for a user. + https://docs.slack.dev/reference/methods/stars.list + """ + kwargs.update( + { + "count": count, + "cursor": cursor, + "limit": limit, + "page": page, + "team_id": team_id, + } + ) + return self.api_call("stars.list", http_verb="GET", params=kwargs) + + def stars_remove( + self, + *, + channel: Optional[str] = None, + file: Optional[str] = None, + file_comment: Optional[str] = None, + timestamp: Optional[str] = None, + **kwargs, + ) -> SlackResponse: + """Removes a star from an item. + https://docs.slack.dev/reference/methods/stars.remove + """ + kwargs.update( + { + "channel": channel, + "file": file, + "file_comment": file_comment, + "timestamp": timestamp, + } + ) + return self.api_call("stars.remove", params=kwargs) + + def team_accessLogs( + self, + *, + before: Optional[Union[int, str]] = None, + count: Optional[Union[int, str]] = None, + page: Optional[Union[int, str]] = None, + team_id: Optional[str] = None, + cursor: Optional[str] = None, + limit: Optional[int] = None, + **kwargs, + ) -> SlackResponse: + """Gets the access logs for the current team. + https://docs.slack.dev/reference/methods/team.accessLogs + """ + kwargs.update( + { + "before": before, + "count": count, + "page": page, + "team_id": team_id, + "cursor": cursor, + "limit": limit, + } + ) + return self.api_call("team.accessLogs", http_verb="GET", params=kwargs) + + def team_billableInfo( + self, + *, + team_id: Optional[str] = None, + user: Optional[str] = None, + **kwargs, + ) -> SlackResponse: + """Gets billable users information for the current team. + https://docs.slack.dev/reference/methods/team.billableInfo + """ + kwargs.update({"team_id": team_id, "user": user}) + return self.api_call("team.billableInfo", http_verb="GET", params=kwargs) + + def team_billing_info( + self, + **kwargs, + ) -> SlackResponse: + """Reads a workspace's billing plan information. + https://docs.slack.dev/reference/methods/team.billing.info + """ + return self.api_call("team.billing.info", params=kwargs) + + def team_externalTeams_disconnect( + self, + *, + target_team: str, + **kwargs, + ) -> SlackResponse: + """Disconnects an external organization. + https://docs.slack.dev/reference/methods/team.externalTeams.disconnect + """ + kwargs.update( + { + "target_team": target_team, + } + ) + return self.api_call("team.externalTeams.disconnect", params=kwargs) + + def team_externalTeams_list( + self, + *, + connection_status_filter: Optional[str] = None, + slack_connect_pref_filter: Optional[Sequence[str]] = None, + sort_direction: Optional[str] = None, + sort_field: Optional[str] = None, + workspace_filter: Optional[Sequence[str]] = None, + cursor: Optional[str] = None, + limit: Optional[int] = None, + **kwargs, + ) -> SlackResponse: + """Returns a list of all the external teams connected and details about the connection. + https://docs.slack.dev/reference/methods/team.externalTeams.list + """ + kwargs.update( + { + "connection_status_filter": connection_status_filter, + "sort_direction": sort_direction, + "sort_field": sort_field, + "cursor": cursor, + "limit": limit, + } + ) + if slack_connect_pref_filter is not None: + if isinstance(slack_connect_pref_filter, (list, tuple)): + kwargs.update({"slack_connect_pref_filter": ",".join(slack_connect_pref_filter)}) + else: + kwargs.update({"slack_connect_pref_filter": slack_connect_pref_filter}) + if workspace_filter is not None: + if isinstance(workspace_filter, (list, tuple)): + kwargs.update({"workspace_filter": ",".join(workspace_filter)}) + else: + kwargs.update({"workspace_filter": workspace_filter}) + return self.api_call("team.externalTeams.list", http_verb="GET", params=kwargs) + + def team_info( + self, + *, + team: Optional[str] = None, + domain: Optional[str] = None, + **kwargs, + ) -> SlackResponse: + """Gets information about the current team. + https://docs.slack.dev/reference/methods/team.info + """ + kwargs.update({"team": team, "domain": domain}) + return self.api_call("team.info", http_verb="GET", params=kwargs) + + def team_integrationLogs( + self, + *, + app_id: Optional[str] = None, + change_type: Optional[str] = None, + count: Optional[Union[int, str]] = None, + page: Optional[Union[int, str]] = None, + service_id: Optional[str] = None, + team_id: Optional[str] = None, + user: Optional[str] = None, + **kwargs, + ) -> SlackResponse: + """Gets the integration logs for the current team. + https://docs.slack.dev/reference/methods/team.integrationLogs + """ + kwargs.update( + { + "app_id": app_id, + "change_type": change_type, + "count": count, + "page": page, + "service_id": service_id, + "team_id": team_id, + "user": user, + } + ) + return self.api_call("team.integrationLogs", http_verb="GET", params=kwargs) + + def team_profile_get( + self, + *, + visibility: Optional[str] = None, + **kwargs, + ) -> SlackResponse: + """Retrieve a team's profile. + https://docs.slack.dev/reference/methods/team.profile.get + """ + kwargs.update({"visibility": visibility}) + return self.api_call("team.profile.get", http_verb="GET", params=kwargs) + + def team_preferences_list( + self, + **kwargs, + ) -> SlackResponse: + """Retrieve a list of a workspace's team preferences. + https://docs.slack.dev/reference/methods/team.preferences.list + """ + return self.api_call("team.preferences.list", params=kwargs) + + def usergroups_create( + self, + *, + name: str, + channels: Optional[Union[str, Sequence[str]]] = None, + description: Optional[str] = None, + handle: Optional[str] = None, + include_count: Optional[bool] = None, + team_id: Optional[str] = None, + **kwargs, + ) -> SlackResponse: + """Create a User Group + https://docs.slack.dev/reference/methods/usergroups.create + """ + kwargs.update( + { + "name": name, + "description": description, + "handle": handle, + "include_count": include_count, + "team_id": team_id, + } + ) + if isinstance(channels, (list, tuple)): + kwargs.update({"channels": ",".join(channels)}) + else: + kwargs.update({"channels": channels}) + return self.api_call("usergroups.create", params=kwargs) + + def usergroups_disable( + self, + *, + usergroup: str, + include_count: Optional[bool] = None, + team_id: Optional[str] = None, + **kwargs, + ) -> SlackResponse: + """Disable an existing User Group + https://docs.slack.dev/reference/methods/usergroups.disable + """ + kwargs.update({"usergroup": usergroup, "include_count": include_count, "team_id": team_id}) + return self.api_call("usergroups.disable", params=kwargs) + + def usergroups_enable( + self, + *, + usergroup: str, + include_count: Optional[bool] = None, + team_id: Optional[str] = None, + **kwargs, + ) -> SlackResponse: + """Enable a User Group + https://docs.slack.dev/reference/methods/usergroups.enable + """ + kwargs.update({"usergroup": usergroup, "include_count": include_count, "team_id": team_id}) + return self.api_call("usergroups.enable", params=kwargs) + + def usergroups_list( + self, + *, + include_count: Optional[bool] = None, + include_disabled: Optional[bool] = None, + include_users: Optional[bool] = None, + team_id: Optional[str] = None, + **kwargs, + ) -> SlackResponse: + """List all User Groups for a team + https://docs.slack.dev/reference/methods/usergroups.list + """ + kwargs.update( + { + "include_count": include_count, + "include_disabled": include_disabled, + "include_users": include_users, + "team_id": team_id, + } + ) + return self.api_call("usergroups.list", http_verb="GET", params=kwargs) + + def usergroups_update( + self, + *, + usergroup: str, + channels: Optional[Union[str, Sequence[str]]] = None, + description: Optional[str] = None, + handle: Optional[str] = None, + include_count: Optional[bool] = None, + name: Optional[str] = None, + team_id: Optional[str] = None, + **kwargs, + ) -> SlackResponse: + """Update an existing User Group + https://docs.slack.dev/reference/methods/usergroups.update + """ + kwargs.update( + { + "usergroup": usergroup, + "description": description, + "handle": handle, + "include_count": include_count, + "name": name, + "team_id": team_id, + } + ) + if isinstance(channels, (list, tuple)): + kwargs.update({"channels": ",".join(channels)}) + else: + kwargs.update({"channels": channels}) + return self.api_call("usergroups.update", params=kwargs) + + def usergroups_users_list( + self, + *, + usergroup: str, + include_disabled: Optional[bool] = None, + team_id: Optional[str] = None, + **kwargs, + ) -> SlackResponse: + """List all users in a User Group + https://docs.slack.dev/reference/methods/usergroups.users.list + """ + kwargs.update( + { + "usergroup": usergroup, + "include_disabled": include_disabled, + "team_id": team_id, + } + ) + return self.api_call("usergroups.users.list", http_verb="GET", params=kwargs) + + def usergroups_users_update( + self, + *, + usergroup: str, + users: Union[str, Sequence[str]], + include_count: Optional[bool] = None, + team_id: Optional[str] = None, + **kwargs, + ) -> SlackResponse: + """Update the list of users for a User Group + https://docs.slack.dev/reference/methods/usergroups.users.update + """ + kwargs.update( + { + "usergroup": usergroup, + "include_count": include_count, + "team_id": team_id, + } + ) + if isinstance(users, (list, tuple)): + kwargs.update({"users": ",".join(users)}) + else: + kwargs.update({"users": users}) + return self.api_call("usergroups.users.update", params=kwargs) + + def users_conversations( + self, + *, + cursor: Optional[str] = None, + exclude_archived: Optional[bool] = None, + limit: Optional[int] = None, + team_id: Optional[str] = None, + types: Optional[Union[str, Sequence[str]]] = None, + user: Optional[str] = None, + **kwargs, + ) -> SlackResponse: + """List conversations the calling user may access. + https://docs.slack.dev/reference/methods/users.conversations + """ + kwargs.update( + { + "cursor": cursor, + "exclude_archived": exclude_archived, + "limit": limit, + "team_id": team_id, + "user": user, + } + ) + if isinstance(types, (list, tuple)): + kwargs.update({"types": ",".join(types)}) + else: + kwargs.update({"types": types}) + return self.api_call("users.conversations", http_verb="GET", params=kwargs) + + def users_deletePhoto( + self, + **kwargs, + ) -> SlackResponse: + """Delete the user profile photo + https://docs.slack.dev/reference/methods/users.deletePhoto + """ + return self.api_call("users.deletePhoto", http_verb="GET", params=kwargs) + + def users_getPresence( + self, + *, + user: str, + **kwargs, + ) -> SlackResponse: + """Gets user presence information. + https://docs.slack.dev/reference/methods/users.getPresence + """ + kwargs.update({"user": user}) + return self.api_call("users.getPresence", http_verb="GET", params=kwargs) + + def users_identity( + self, + **kwargs, + ) -> SlackResponse: + """Get a user's identity. + https://docs.slack.dev/reference/methods/users.identity + """ + return self.api_call("users.identity", http_verb="GET", params=kwargs) + + def users_info( + self, + *, + user: str, + include_locale: Optional[bool] = None, + **kwargs, + ) -> SlackResponse: + """Gets information about a user. + https://docs.slack.dev/reference/methods/users.info + """ + kwargs.update({"user": user, "include_locale": include_locale}) + return self.api_call("users.info", http_verb="GET", params=kwargs) + + def users_list( + self, + *, + cursor: Optional[str] = None, + include_locale: Optional[bool] = None, + limit: Optional[int] = None, + team_id: Optional[str] = None, + **kwargs, + ) -> SlackResponse: + """Lists all users in a Slack team. + https://docs.slack.dev/reference/methods/users.list + """ + kwargs.update( + { + "cursor": cursor, + "include_locale": include_locale, + "limit": limit, + "team_id": team_id, + } + ) + return self.api_call("users.list", http_verb="GET", params=kwargs) + + def users_lookupByEmail( + self, + *, + email: str, + **kwargs, + ) -> SlackResponse: + """Find a user with an email address. + https://docs.slack.dev/reference/methods/users.lookupByEmail + """ + kwargs.update({"email": email}) + return self.api_call("users.lookupByEmail", http_verb="GET", params=kwargs) + + def users_setPhoto( + self, + *, + image: Union[str, IOBase], + crop_w: Optional[Union[int, str]] = None, + crop_x: Optional[Union[int, str]] = None, + crop_y: Optional[Union[int, str]] = None, + **kwargs, + ) -> SlackResponse: + """Set the user profile photo + https://docs.slack.dev/reference/methods/users.setPhoto + """ + kwargs.update({"crop_w": crop_w, "crop_x": crop_x, "crop_y": crop_y}) + return self.api_call("users.setPhoto", files={"image": image}, data=kwargs) + + def users_setPresence( + self, + *, + presence: str, + **kwargs, + ) -> SlackResponse: + """Manually sets user presence. + https://docs.slack.dev/reference/methods/users.setPresence + """ + kwargs.update({"presence": presence}) + return self.api_call("users.setPresence", params=kwargs) + + def users_discoverableContacts_lookup( + self, + email: str, + **kwargs, + ) -> SlackResponse: + """Lookup an email address to see if someone is on Slack + https://docs.slack.dev/reference/methods/users.discoverableContacts.lookup + """ + kwargs.update({"email": email}) + return self.api_call("users.discoverableContacts.lookup", params=kwargs) + + def users_profile_get( + self, + *, + user: Optional[str] = None, + include_labels: Optional[bool] = None, + **kwargs, + ) -> SlackResponse: + """Retrieves a user's profile information. + https://docs.slack.dev/reference/methods/users.profile.get + """ + kwargs.update({"user": user, "include_labels": include_labels}) + return self.api_call("users.profile.get", http_verb="GET", params=kwargs) + + def users_profile_set( + self, + *, + name: Optional[str] = None, + value: Optional[str] = None, + user: Optional[str] = None, + profile: Optional[Dict] = None, + **kwargs, + ) -> SlackResponse: + """Set the profile information for a user. + https://docs.slack.dev/reference/methods/users.profile.set + """ + kwargs.update( + { + "name": name, + "profile": profile, + "user": user, + "value": value, + } + ) + kwargs = _remove_none_values(kwargs) + # NOTE: Intentionally using json for the "profile" parameter + return self.api_call("users.profile.set", json=kwargs) + + def views_open( + self, + *, + trigger_id: Optional[str] = None, + interactivity_pointer: Optional[str] = None, + view: Union[dict, View], + **kwargs, + ) -> SlackResponse: + """Open a view for a user. + https://docs.slack.dev/reference/methods/views.open + See https://docs.slack.dev/surfaces/modals/ for details. + """ + kwargs.update({"trigger_id": trigger_id, "interactivity_pointer": interactivity_pointer}) + if isinstance(view, View): + kwargs.update({"view": view.to_dict()}) + else: + kwargs.update({"view": view}) + kwargs = _remove_none_values(kwargs) + # NOTE: Intentionally using json for the "view" parameter + return self.api_call("views.open", json=kwargs) + + def views_push( + self, + *, + trigger_id: Optional[str] = None, + interactivity_pointer: Optional[str] = None, + view: Union[dict, View], + **kwargs, + ) -> SlackResponse: + """Push a view onto the stack of a root view. + Push a new view onto the existing view stack by passing a view + payload and a valid trigger_id generated from an interaction + within the existing modal. + Read the modals documentation (https://docs.slack.dev/surfaces/modals/) + to learn more about the lifecycle and intricacies of views. + https://docs.slack.dev/reference/methods/views.push + """ + kwargs.update({"trigger_id": trigger_id, "interactivity_pointer": interactivity_pointer}) + if isinstance(view, View): + kwargs.update({"view": view.to_dict()}) + else: + kwargs.update({"view": view}) + kwargs = _remove_none_values(kwargs) + # NOTE: Intentionally using json for the "view" parameter + return self.api_call("views.push", json=kwargs) + + def views_update( + self, + *, + view: Union[dict, View], + external_id: Optional[str] = None, + view_id: Optional[str] = None, + hash: Optional[str] = None, + **kwargs, + ) -> SlackResponse: + """Update an existing view. + Update a view by passing a new view definition along with the + view_id returned in views.open or the external_id. + See the modals documentation (https://docs.slack.dev/surfaces/modals/#updating_views) + to learn more about updating views and avoiding race conditions with the hash argument. + https://docs.slack.dev/reference/methods/views.update + """ + if isinstance(view, View): + kwargs.update({"view": view.to_dict()}) + else: + kwargs.update({"view": view}) + if external_id: + kwargs.update({"external_id": external_id}) + elif view_id: + kwargs.update({"view_id": view_id}) + else: + raise e.SlackRequestError("Either view_id or external_id is required.") + kwargs.update({"hash": hash}) + kwargs = _remove_none_values(kwargs) + # NOTE: Intentionally using json for the "view" parameter + return self.api_call("views.update", json=kwargs) + + def views_publish( + self, + *, + user_id: str, + view: Union[dict, View], + hash: Optional[str] = None, + **kwargs, + ) -> SlackResponse: + """Publish a static view for a User. + Create or update the view that comprises an + app's Home tab (https://docs.slack.dev/surfaces/app-home/) + https://docs.slack.dev/reference/methods/views.publish + """ + kwargs.update({"user_id": user_id, "hash": hash}) + if isinstance(view, View): + kwargs.update({"view": view.to_dict()}) + else: + kwargs.update({"view": view}) + kwargs = _remove_none_values(kwargs) + # NOTE: Intentionally using json for the "view" parameter + return self.api_call("views.publish", json=kwargs) + + def workflows_featured_add( + self, + *, + channel_id: str, + trigger_ids: Union[str, Sequence[str]], + **kwargs, + ) -> SlackResponse: + """Add featured workflows to a channel. + https://docs.slack.dev/reference/methods/workflows.featured.add + """ + kwargs.update({"channel_id": channel_id}) + if isinstance(trigger_ids, (list, tuple)): + kwargs.update({"trigger_ids": ",".join(trigger_ids)}) + else: + kwargs.update({"trigger_ids": trigger_ids}) + return self.api_call("workflows.featured.add", params=kwargs) + + def workflows_featured_list( + self, + *, + channel_ids: Union[str, Sequence[str]], + **kwargs, + ) -> SlackResponse: + """List the featured workflows for specified channels. + https://docs.slack.dev/reference/methods/workflows.featured.list + """ + if isinstance(channel_ids, (list, tuple)): + kwargs.update({"channel_ids": ",".join(channel_ids)}) + else: + kwargs.update({"channel_ids": channel_ids}) + return self.api_call("workflows.featured.list", params=kwargs) + + def workflows_featured_remove( + self, + *, + channel_id: str, + trigger_ids: Union[str, Sequence[str]], + **kwargs, + ) -> SlackResponse: + """Remove featured workflows from a channel. + https://docs.slack.dev/reference/methods/workflows.featured.remove + """ + kwargs.update({"channel_id": channel_id}) + if isinstance(trigger_ids, (list, tuple)): + kwargs.update({"trigger_ids": ",".join(trigger_ids)}) + else: + kwargs.update({"trigger_ids": trigger_ids}) + return self.api_call("workflows.featured.remove", params=kwargs) + + def workflows_featured_set( + self, + *, + channel_id: str, + trigger_ids: Union[str, Sequence[str]], + **kwargs, + ) -> SlackResponse: + """Set featured workflows for a channel. + https://docs.slack.dev/reference/methods/workflows.featured.set + """ + kwargs.update({"channel_id": channel_id}) + if isinstance(trigger_ids, (list, tuple)): + kwargs.update({"trigger_ids": ",".join(trigger_ids)}) + else: + kwargs.update({"trigger_ids": trigger_ids}) + return self.api_call("workflows.featured.set", params=kwargs) + + def workflows_stepCompleted( + self, + *, + workflow_step_execute_id: str, + outputs: Optional[dict] = None, + **kwargs, + ) -> SlackResponse: + """Indicate a successful outcome of a workflow step's execution. + https://docs.slack.dev/reference/methods/workflows.stepCompleted + """ + kwargs.update({"workflow_step_execute_id": workflow_step_execute_id}) + if outputs is not None: + kwargs.update({"outputs": outputs}) + kwargs = _remove_none_values(kwargs) + # NOTE: Intentionally using json for the "outputs" parameter + return self.api_call("workflows.stepCompleted", json=kwargs) + + def workflows_stepFailed( + self, + *, + workflow_step_execute_id: str, + error: Dict[str, str], + **kwargs, + ) -> SlackResponse: + """Indicate an unsuccessful outcome of a workflow step's execution. + https://docs.slack.dev/reference/methods/workflows.stepFailed + """ + kwargs.update( + { + "workflow_step_execute_id": workflow_step_execute_id, + "error": error, + } + ) + kwargs = _remove_none_values(kwargs) + # NOTE: Intentionally using json for the "error" parameter + return self.api_call("workflows.stepFailed", json=kwargs) + + def workflows_updateStep( + self, + *, + workflow_step_edit_id: str, + inputs: Optional[Dict[str, Any]] = None, + outputs: Optional[List[Dict[str, str]]] = None, + **kwargs, + ) -> SlackResponse: + """Update the configuration for a workflow extension step. + https://docs.slack.dev/reference/methods/workflows.updateStep + """ + kwargs.update({"workflow_step_edit_id": workflow_step_edit_id}) + if inputs is not None: + kwargs.update({"inputs": inputs}) + if outputs is not None: + kwargs.update({"outputs": outputs}) + kwargs = _remove_none_values(kwargs) + # NOTE: Intentionally using json for the "inputs" / "outputs" parameters + return self.api_call("workflows.updateStep", json=kwargs) diff --git a/slack_sdk/web/deprecation.py b/slack_sdk/web/deprecation.py new file mode 100644 index 000000000..c81c4b754 --- /dev/null +++ b/slack_sdk/web/deprecation.py @@ -0,0 +1,53 @@ +import os +import warnings + +# https://docs.slack.dev/changelog/2020-01-deprecating-antecedents-to-the-conversations-api/ +deprecated_method_prefixes_2020_01 = [ + "channels.", + "groups.", + "im.", + "mpim.", + "admin.conversations.whitelist.", +] + +deprecated_method_prefixes_2023_07 = ["stars."] + +deprecated_method_prefixes_2024_09 = ["workflows.stepCompleted", "workflows.updateStep", "workflows.stepFailed"] + + +def show_deprecation_warning_if_any(method_name: str): + """Prints a warning if the given method is deprecated""" + + skip_deprecation = os.environ.get("SLACKCLIENT_SKIP_DEPRECATION") # for unit tests etc. + if skip_deprecation: + return + if not method_name: + return + + # 2020/01 conversations API deprecation + matched_prefixes = [prefix for prefix in deprecated_method_prefixes_2020_01 if method_name.startswith(prefix)] + if len(matched_prefixes) > 0: + message = ( + f"{method_name} is deprecated. Please use the Conversations API instead. " + "For more info, go to " + "https://docs.slack.dev/changelog/2020-01-deprecating-antecedents-to-the-conversations-api/" + ) + warnings.warn(message) + + # 2023/07 stars API deprecation + matched_prefixes = [prefix for prefix in deprecated_method_prefixes_2023_07 if method_name.startswith(prefix)] + if len(matched_prefixes) > 0: + message = ( + f"{method_name} is deprecated. For more info, go to " + "https://docs.slack.dev/changelog/2023-07-its-later-already-for-stars-and-reminders/" + ) + warnings.warn(message) + + # 2024/09 workflow steps API deprecation + matched_prefixes = [prefix for prefix in deprecated_method_prefixes_2024_09 if method_name.startswith(prefix)] + if len(matched_prefixes) > 0: + message = ( + f"{method_name} is deprecated. For more info, go to " + "https://docs.slack.dev/changelog/2023-08-workflow-steps-from-apps-step-back/" + ) + warnings.warn(message) diff --git a/slack_sdk/web/file_upload_v2_result.py b/slack_sdk/web/file_upload_v2_result.py new file mode 100644 index 000000000..abacca442 --- /dev/null +++ b/slack_sdk/web/file_upload_v2_result.py @@ -0,0 +1,7 @@ +class FileUploadV2Result: + status: int + body: str + + def __init__(self, status: int, body: str): + self.status = status + self.body = body diff --git a/slack_sdk/web/internal_utils.py b/slack_sdk/web/internal_utils.py new file mode 100644 index 000000000..87139559c --- /dev/null +++ b/slack_sdk/web/internal_utils.py @@ -0,0 +1,438 @@ +import json +import logging +import os +import platform +import sys +import urllib +import warnings +from asyncio import Future +from http.client import HTTPResponse +from io import IOBase +from ssl import SSLContext +from typing import Any, Dict, Optional, Sequence, Union +from urllib.parse import urljoin +from urllib.request import OpenerDirector, ProxyHandler, HTTPSHandler, Request, urlopen + +from slack_sdk import version +from slack_sdk.errors import SlackRequestError +from slack_sdk.models.attachments import Attachment +from slack_sdk.models.blocks import Block +from slack_sdk.models.metadata import Metadata, EventAndEntityMetadata, EntityMetadata + + +def convert_bool_to_0_or_1(params: Optional[Dict[str, Any]]) -> Optional[Dict[str, Any]]: + """Converts all bool values in dict to "0" or "1". + + Slack APIs safely accept "0"/"1" as boolean values. + Using True/False (bool in Python) doesn't work with aiohttp. + This method converts only the bool values in top-level of a given dict. + + Args: + params: params as a dict + + Returns: + Modified dict + """ + if params: + return {k: _to_0_or_1_if_bool(v) for k, v in params.items()} + return None + + +def get_user_agent(prefix: Optional[str] = None, suffix: Optional[str] = None): + """Construct the user-agent header with the package info, + Python version and OS version. + + Returns: + The user agent string. + e.g. 'Python/3.7.17 slackclient/2.0.0 Darwin/17.7.0' + """ + # __name__ returns all classes, we only want the client + client = "{0}/{1}".format("slackclient", version.__version__) + python_version = "Python/{v.major}.{v.minor}.{v.micro}".format(v=sys.version_info) + system_info = "{0}/{1}".format(platform.system(), platform.release()) + user_agent_string = " ".join([python_version, client, system_info]) + prefix = f"{prefix} " if prefix else "" + suffix = f" {suffix}" if suffix else "" + return prefix + user_agent_string + suffix + + +def _get_url(base_url: str, api_method: str) -> str: + """Joins the base Slack URL and an API method to form an absolute URL. + + Args: + base_url (str): The base URL + api_method (str): The Slack Web API method. e.g. 'chat.postMessage' + + Returns: + The absolute API URL. + e.g. 'https://slack.com/api/chat.postMessage' + """ + # Ensure no leading slash in api_method to prevent double slashes + api_method = api_method.lstrip("/") + return urljoin(base_url, api_method) + + +def _get_headers( + *, + headers: dict, + token: Optional[str], + has_json: bool, + has_files: bool, + request_specific_headers: Optional[dict], +) -> Dict[str, str]: + """Constructs the headers need for a request. + Args: + has_json (bool): Whether or not the request has json. + has_files (bool): Whether or not the request has files. + request_specific_headers (dict): Additional headers specified by the user for a specific request. + + Returns: + The headers dictionary. + e.g. { + 'Content-Type': 'application/json;charset=utf-8', + 'Authorization': 'Bearer xoxb-1234-1243', + 'User-Agent': 'Python/3.7.17 slack/2.1.0 Darwin/17.7.0' + } + """ + final_headers = { + "Content-Type": "application/x-www-form-urlencoded", + } + if headers is None or "User-Agent" not in headers: + final_headers["User-Agent"] = get_user_agent() + + if token: + final_headers.update({"Authorization": "Bearer {}".format(token)}) + if headers is None: + headers = {} + + # Merge headers specified at client initialization. + final_headers.update(headers) + + # Merge headers specified for a specific request. e.g. oauth.access + if request_specific_headers: + final_headers.update(request_specific_headers) + + if has_json: + final_headers.update({"Content-Type": "application/json;charset=utf-8"}) + + if has_files: + # These are set automatically by the aiohttp library. + final_headers.pop("Content-Type", None) + + return final_headers + + +def _set_default_params(target: dict, default_params: dict) -> None: + for name, value in default_params.items(): + if name not in target: + target[name] = value + + +def _build_req_args( + *, + token: Optional[str], + http_verb: str, + files: dict, + data: dict, + default_params: dict, + params: dict, + json: dict, + headers: dict, + auth: dict, + ssl: Optional[SSLContext], + proxy: Optional[str], +) -> dict: + has_json = json is not None + has_files = files is not None + if has_json and http_verb != "POST": + msg = "Json data can only be submitted as POST requests. GET requests should use the 'params' argument." + raise SlackRequestError(msg) + + if data is not None and isinstance(data, dict): + data = {k: v for k, v in data.items() if v is not None} + _set_default_params(data, default_params) + if files is not None and isinstance(files, dict): + files = {k: v for k, v in files.items() if v is not None} + # NOTE: We do not need to all #_set_default_params here + # because other parameters in binary data requests can exist + # only in either data or params, not in files. + if params is not None and isinstance(params, dict): + params = {k: v for k, v in params.items() if v is not None} + _set_default_params(params, default_params) + if json is not None and isinstance(json, dict): + _set_default_params(json, default_params) + + token = token + if params is not None and "token" in params: + token = params.pop("token") + if json is not None and "token" in json: + token = json.pop("token") + req_args = { + "headers": _get_headers( + headers=headers, + token=token, + has_json=has_json, + has_files=has_files, + request_specific_headers=headers, + ), + "data": data, + "files": files, + "params": params, + "json": json, + "ssl": ssl, + "proxy": proxy, + "auth": auth, + } + return req_args + + +def _parse_web_class_objects(kwargs) -> None: + def to_dict(obj: Union[Dict, Block, Attachment, Metadata, EventAndEntityMetadata, EntityMetadata]): + if isinstance(obj, Block): + return obj.to_dict() + if isinstance(obj, Attachment): + return obj.to_dict() + if isinstance(obj, Metadata): + return obj.to_dict() + if isinstance(obj, EventAndEntityMetadata): + return obj.to_dict() + if isinstance(obj, EntityMetadata): + return obj.to_dict() + return obj + + for blocks_name in ["blocks", "user_auth_blocks"]: + blocks = kwargs.get(blocks_name, None) + if blocks is not None and isinstance(blocks, Sequence) and (not isinstance(blocks, str)): + dict_blocks = [to_dict(b) for b in blocks] + kwargs.update({blocks_name: dict_blocks}) + + attachments = kwargs.get("attachments", None) + if attachments is not None and isinstance(attachments, Sequence) and (not isinstance(attachments, str)): + dict_attachments = [to_dict(a) for a in attachments] + kwargs.update({"attachments": dict_attachments}) + + metadata = kwargs.get("metadata", None) + if metadata is not None and ( + isinstance(metadata, Metadata) + or isinstance(metadata, EntityMetadata) + or isinstance(metadata, EventAndEntityMetadata) + ): + kwargs.update({"metadata": to_dict(metadata)}) + + +def _update_call_participants(kwargs, users: Union[str, Sequence[Dict[str, str]]]) -> None: + 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 Sequence[Dict[str, str]]") + + +def _next_cursor_is_present(data) -> bool: + """Determine if the response contains 'next_cursor' + and 'next_cursor' is not empty. + + Returns: + A boolean value. + """ + # Only admin.conversations.search returns next_cursor at the top level + present = ("next_cursor" in data and data["next_cursor"] is not None and data["next_cursor"] != "") or ( + "response_metadata" in data + and "next_cursor" in data["response_metadata"] + and data["response_metadata"]["next_cursor"] is not None + and data["response_metadata"]["next_cursor"] != "" + ) + return present + + +def _to_0_or_1_if_bool(v: Any) -> Union[Any, str]: + if isinstance(v, bool): + return "1" if v else "0" + return v + + +def _warn_if_message_text_content_is_missing(endpoint: str, kwargs: Dict[str, Any]) -> None: + text = kwargs.get("text") + if text and len(text.strip()) > 0: + # If a top-level text arg is provided, we are good. This is the recommended accessibility field to always provide. + return + + markdown_text = kwargs.get("markdown_text") + if markdown_text and len(markdown_text.strip()) > 0: + # If a top-level markdown_text arg is provided, we are good. It should not be used in conjunction with text. + return + + # for unit tests etc. + skip_deprecation = os.environ.get("SKIP_SLACK_SDK_WARNING") + if skip_deprecation: + return + + # if text argument is missing, Warn the user about this. + # However, do not warn if a fallback field exists for all attachments, since this can be substituted. + missing_text_message = ( + f"The top-level `text` argument is missing in the request payload for a {endpoint} call - " + f"It's a best practice to always provide a `text` argument when posting a message. " + f"The `text` argument is used in places where content cannot be rendered such as: " + "system push notifications, assistive technology such as screen readers, etc." + ) + + # https://docs.slack.dev/legacy/legacy-messaging/legacy-secondary-message-attachments + # Check if the fallback field exists for all the attachments + # Not all attachments have a fallback property; warn about this too! + missing_fallback_message = ( + f"Additionally, the attachment-level `fallback` argument is missing in the request payload for a {endpoint} call" + " - To avoid this warning, it is recommended to always provide a top-level `text` argument when posting a" + " message. Alternatively you can provide an attachment-level `fallback` argument, though this is now considered" + " a legacy field (see https://docs.slack.dev/legacy/legacy-messaging/legacy-secondary-message-attachments#legacy_fields for more details)." # noqa: E501 + ) + + # Additionally, specifically for attachments, there is a legacy field available at the attachment level called `fallback` + # Even with a missing text, one can provide a `fallback` per attachment. + # More details here: https://docs.slack.dev/legacy/legacy-messaging/legacy-secondary-message-attachments#legacy_fields + attachments = kwargs.get("attachments") + # Note that this method does not verify attachments + # if the value is already serialized as a single str value. + if attachments is not None and isinstance(attachments, list): + if not all( + [isinstance(attachment, dict) and len(attachment.get("fallback", "").strip()) > 0 for attachment in attachments] + ): + warnings.warn(missing_text_message, UserWarning) + warnings.warn(missing_fallback_message, UserWarning) + else: + warnings.warn(missing_text_message, UserWarning) + + +def _build_unexpected_body_error_message(body: str) -> str: + body_for_logging = "".join([line.strip() for line in body.replace("\r", "\n").split("\n")]) + if len(body_for_logging) > 100: + body_for_logging = body_for_logging[:100] + "..." + message = f"Received a response in a non-JSON format: {body_for_logging}" + return message + + +def _remove_none_values(d: dict) -> dict: + # To avoid having null values in JSON (Slack API does not work with null in many situations) + # + # >>> import json + # >>> d = {"a": None, "b":123} + # >>> json.dumps(d) + # '{"a": null, "b": 123}' + # + return {k: v for k, v in d.items() if v is not None} + + +def _to_v2_file_upload_item(upload_file: Dict[str, Any]) -> Dict[str, Optional[Any]]: + file = upload_file.get("file") + content = upload_file.get("content") + data: Optional[bytes] = None + if file is not None: + if isinstance(file, (str, os.PathLike)): # filepath + with open(os.fsencode(file), "rb") as readable: + data = readable.read() + elif isinstance(file, bytes): + data = file + elif isinstance(file, IOBase): + data = file.read() + if isinstance(data, str): + data = data.encode() + else: + raise SlackRequestError("file parameter must be any of filepath, bytes, and io.IOBase") + elif content is not None: + if isinstance(content, str): + data = content.encode("utf-8") + elif isinstance(content, bytes): + data = content + else: + raise SlackRequestError("content for file upload must be 'str' (UTF-8 encoded) or 'bytes' (for data)") + + filename = upload_file.get("filename") + if filename is None: + # use the local filename if filename is missing + if isinstance(file, (str, os.PathLike)): + filename = os.path.basename(os.fspath(file)) + else: + filename = "Uploaded file" + + title = upload_file.get("title") + if data is None: + raise SlackRequestError(f"File content not found for filename: {filename}, title: {title}") + + if title is None: + title = filename # to be consistent with files.upload API + + return { + "filename": filename, + "data": data, + "length": len(data), + "title": title, + "alt_txt": upload_file.get("alt_txt"), + "snippet_type": upload_file.get("snippet_type"), + } + + +def _upload_file_via_v2_url( + url: str, + data: bytes, + timeout: int, + logger: logging.Logger, + proxy: Optional[str] = None, + ssl: Optional[SSLContext] = None, +) -> Dict[str, Any]: + opener: Optional[OpenerDirector] = None + if proxy is not None: + if isinstance(proxy, str): + opener = urllib.request.build_opener( + ProxyHandler({"http": proxy, "https": proxy}), + HTTPSHandler(context=ssl), + ) + else: + raise SlackRequestError(f"Invalid proxy detected: {proxy} must be a str value") + + if logger.level <= logging.DEBUG: + logger.debug(f"Sending a request: POST {url}") + + resp: Optional[HTTPResponse] = None + req: Request = Request(method="POST", url=url, data=data, headers={}) + if opener: + resp = opener.open(req, timeout=timeout) + else: + resp = urlopen(req, context=ssl, timeout=timeout) + + charset = resp.headers.get_content_charset() or "utf-8" + # read the response body here + body: str = resp.read().decode(charset) + if logger.level <= logging.DEBUG: + message = ( + "Received the following response - " + f"status: {resp.status}, " + f"headers: {dict(resp.headers)}, " + f"body: {body}" + ) + logger.debug(message) + + return {"status": resp.status, "headers": resp.headers, "body": body} + + +def _validate_for_legacy_client( + response: Union["SlackResponse", Future], # type: ignore[name-defined] # noqa: F821 +) -> None: + # Only LegacyWebClient can return this union type + if isinstance(response, Future): + message = ( + "Sorry! This SDK does not support run_async=True option for this API calls. " + "Please migrate to AsyncWebClient, which is a new and stable way to go." + ) + raise SlackRequestError(message) + + +def _print_files_upload_v2_suggestion(): + message = ( + "client.files_upload() may cause some issues like timeouts for relatively large files. " + "Our latest recommendation is to use client.files_upload_v2(), " + "which is mostly compatible and much stabler, instead." + ) + warnings.warn(message) diff --git a/slack_sdk/web/legacy_base_client.py b/slack_sdk/web/legacy_base_client.py new file mode 100644 index 000000000..4b6b63027 --- /dev/null +++ b/slack_sdk/web/legacy_base_client.py @@ -0,0 +1,588 @@ +"""A Python module for interacting with Slack's Web API.""" + +# mypy: ignore-errors + +import asyncio +import copy +import hashlib +import hmac +import io +import json +import logging +import mimetypes +import urllib +import uuid +import warnings +from http.client import HTTPResponse +from ssl import SSLContext +from typing import BinaryIO, Dict, List, Any +from typing import Optional, Union +from urllib.error import HTTPError +from urllib.parse import urlencode +from urllib.request import Request, urlopen, OpenerDirector, ProxyHandler, HTTPSHandler + +import aiohttp +from aiohttp import FormData, BasicAuth + +import slack_sdk.errors as err +from slack_sdk.errors import SlackRequestError +from .async_internal_utils import _files_to_data, _get_event_loop, _request_with_session +from .deprecation import show_deprecation_warning_if_any +from .file_upload_v2_result import FileUploadV2Result +from .internal_utils import ( + convert_bool_to_0_or_1, + get_user_agent, + _get_url, + _build_req_args, + _build_unexpected_body_error_message, + _upload_file_via_v2_url, +) +from .legacy_slack_response import LegacySlackResponse as SlackResponse +from ..proxy_env_variable_loader import load_http_proxy_from_env + + +class LegacyBaseClient: + BASE_URL = "https://slack.com/api/" + + def __init__( + self, + token: Optional[str] = None, + base_url: str = BASE_URL, + timeout: int = 30, + loop: Optional[asyncio.AbstractEventLoop] = None, + ssl: Optional[SSLContext] = None, + proxy: Optional[str] = None, + run_async: bool = False, + use_sync_aiohttp: bool = False, + session: Optional[aiohttp.ClientSession] = None, + headers: Optional[dict] = None, + user_agent_prefix: Optional[str] = None, + user_agent_suffix: Optional[str] = None, + # for Org-Wide App installation + team_id: Optional[str] = None, + logger: Optional[logging.Logger] = None, + ): + self.token = None if token is None else token.strip() + """A string specifying an `xoxp-*` or `xoxb-*` token.""" + if not base_url.endswith("/"): + base_url += "/" + self.base_url = base_url + """A string representing the Slack API base URL. + Default is `'https://slack.com/api/'`.""" + self.timeout = timeout + """The maximum number of seconds the client will wait + to connect and receive a response from Slack. + Default is 30 seconds.""" + self.ssl = ssl + """An [`ssl.SSLContext`](https://docs.python.org/3/library/ssl.html#ssl.SSLContext) + instance, helpful for specifying your own custom + certificate chain.""" + self.proxy = proxy + """String representing a fully-qualified URL to a proxy through which + to route all requests to the Slack API. Even if this parameter + is not specified, if any of the following environment variables are + present, they will be loaded into this parameter: `HTTPS_PROXY`, + `https_proxy`, `HTTP_PROXY` or `http_proxy`.""" + self.run_async = run_async + self.use_sync_aiohttp = use_sync_aiohttp + self.session = session + self.headers = headers or {} + """`dict` representing additional request headers to attach to all requests.""" + self.headers["User-Agent"] = get_user_agent(user_agent_prefix, user_agent_suffix) + self.default_params = {} + if team_id is not None: + self.default_params["team_id"] = team_id + self._logger = logger if logger is not None else logging.getLogger(__name__) + if self.proxy is None or len(self.proxy.strip()) == 0: + env_variable = load_http_proxy_from_env(self._logger) + if env_variable is not None: + self.proxy = env_variable + + self._event_loop = loop + + def api_call( + self, + api_method: str, + *, + http_verb: str = "POST", + files: Optional[dict] = None, + data: Union[dict, FormData] = None, + params: Optional[dict] = None, + json: Optional[dict] = None, + headers: Optional[dict] = None, + auth: Optional[dict] = None, + ) -> Union[asyncio.Future, SlackResponse]: + """Create a request and execute the API call to Slack. + Args: + api_method (str): The target Slack API method. + e.g. 'chat.postMessage' + http_verb (str): HTTP Verb. e.g. 'POST' + files (dict): Files to multipart upload. + e.g. {image OR file: file_object OR file_path} + data: The body to attach to the request. If a dictionary is + provided, form-encoding will take place. + e.g. {'key1': 'value1', 'key2': 'value2'} + params (dict): The URL parameters to append to the URL. + e.g. {'key1': 'value1', 'key2': 'value2'} + json (dict): JSON for the body to attach to the request + (if files or data is not specified). + e.g. {'key1': 'value1', 'key2': 'value2'} + headers (dict): Additional request headers + auth (dict): A dictionary that consists of client_id and client_secret + Returns: + (SlackResponse) + The server's response to an HTTP request. Data + from the response can be accessed like a dict. + If the response included 'next_cursor' it can + be iterated on to execute subsequent requests. + Raises: + SlackApiError: The following Slack API call failed: + 'chat.postMessage'. + SlackRequestError: Json data can only be submitted as + POST requests. + """ + + api_url = _get_url(self.base_url, api_method) + + headers = headers or {} + headers.update(self.headers) + + if auth is not None: + if isinstance(auth, dict): + auth = BasicAuth(auth["client_id"], auth["client_secret"]) + elif isinstance(auth, BasicAuth): + headers["Authorization"] = auth.encode() + + req_args = _build_req_args( + token=self.token, + http_verb=http_verb, + files=files, + data=data, + default_params=self.default_params, + params=params, + json=json, + headers=headers, + auth=auth, + ssl=self.ssl, + proxy=self.proxy, + ) + + show_deprecation_warning_if_any(api_method) + + if self.run_async or self.use_sync_aiohttp: + if self._event_loop is None: + self._event_loop = _get_event_loop() + + future = asyncio.ensure_future( + self._send(http_verb=http_verb, api_url=api_url, req_args=req_args), + loop=self._event_loop, + ) + if self.run_async: + return future + if self.use_sync_aiohttp: + # Using this is no longer recommended - just keep this for backward-compatibility + return self._event_loop.run_until_complete(future) + + return self._sync_send(api_url=api_url, req_args=req_args) + + # ================================================================= + # aiohttp based async WebClient + # ================================================================= + + async def _send(self, http_verb: str, api_url: str, req_args: dict) -> SlackResponse: + """Sends the request out for transmission. + Args: + http_verb (str): The HTTP verb. e.g. 'GET' or 'POST'. + api_url (str): The Slack API url. e.g. 'https://slack.com/api/chat.postMessage' + req_args (dict): The request arguments to be attached to the request. + e.g. + { + json: { + 'attachments': [{"pretext": "pre-hello", "text": "text-world"}], + 'channel': '#random' + } + } + Returns: + The response parsed into a SlackResponse object. + """ + open_files = _files_to_data(req_args) + try: + if "params" in req_args: + # True/False -> "1"/"0" + req_args["params"] = convert_bool_to_0_or_1(req_args["params"]) + + res = await self._request(http_verb=http_verb, api_url=api_url, req_args=req_args) + finally: + for f in open_files: + f.close() + + data = { + "client": self, + "http_verb": http_verb, + "api_url": api_url, + "req_args": req_args, + "use_sync_aiohttp": self.use_sync_aiohttp, + } + return SlackResponse(**{**data, **res}).validate() + + async def _request(self, *, http_verb, api_url, req_args) -> Dict[str, Any]: + """Submit the HTTP request with the running session or a new session. + Returns: + A dictionary of the response data. + """ + return await _request_with_session( + current_session=self.session, + timeout=self.timeout, + logger=self._logger, + http_verb=http_verb, + api_url=api_url, + req_args=req_args, + ) + + # ================================================================= + # urllib based WebClient + # ================================================================= + + def _sync_send(self, api_url, req_args) -> SlackResponse: + params = req_args["params"] if "params" in req_args else None + data = req_args["data"] if "data" in req_args else None + files = req_args["files"] if "files" in req_args else None + _json = req_args["json"] if "json" in req_args else None + headers = req_args["headers"] if "headers" in req_args else None + token = params.get("token") if params and "token" in params else None + auth = req_args["auth"] if "auth" in req_args else None # Basic Auth for oauth.v2.access / oauth.access + if auth is not None: + headers = {} + if isinstance(auth, BasicAuth): + headers["Authorization"] = auth.encode() + elif isinstance(auth, str): + headers["Authorization"] = auth + else: + self._logger.warning(f"As the auth: {auth}: {type(auth)} is unsupported, skipped") + + body_params = {} + if params: + body_params.update(params) + if data: + body_params.update(data) + + return self._urllib_api_call( + token=token, + url=api_url, + query_params={}, + body_params=body_params, + files=files, + json_body=_json, + additional_headers=headers, + ) + + def _request_for_pagination(self, api_url: str, req_args: Dict[str, Dict[str, Any]]) -> Dict[str, Any]: + """This method is supposed to be used only for SlackResponse pagination + You can paginate using Python's for iterator as below: + for response in client.conversations_list(limit=100): + # do something with each response here + """ + response = self._perform_urllib_http_request(url=api_url, args=req_args) + return { + "status_code": int(response["status"]), + "headers": dict(response["headers"]), + "data": json.loads(response["body"]), + } + + def _urllib_api_call( + self, + *, + token: Optional[str] = None, + url: str, + query_params: Dict[str, str], + json_body: Dict, + body_params: Dict[str, str], + files: Dict[str, io.BytesIO], + additional_headers: Dict[str, str], + ) -> SlackResponse: + """Performs a Slack API request and returns the result. + + Args: + token: Slack API Token (either bot token or user token) + url: Complete URL (e.g., https://slack.com/api/chat.postMessage) + query_params: Query string + json_body: JSON data structure (it's still a dict at this point), + if you give this argument, body_params and files will be skipped + body_params: Form body params + files: Files to upload + additional_headers: Request headers to append + Returns: + API response + """ + files_to_close: List[BinaryIO] = [] + try: + # True/False -> "1"/"0" + query_params = convert_bool_to_0_or_1(query_params) + body_params = convert_bool_to_0_or_1(body_params) + + if self._logger.level <= logging.DEBUG: + + def convert_params(values: dict) -> dict: + if not values or not isinstance(values, dict): + return {} + return {k: ("(bytes)" if isinstance(v, bytes) else v) for k, v in values.items()} + + headers = {k: "(redacted)" if k.lower() == "authorization" else v for k, v in additional_headers.items()} + self._logger.debug( + f"Sending a request - url: {url}, " + f"query_params: {convert_params(query_params)}, " + f"body_params: {convert_params(body_params)}, " + f"files: {convert_params(files)}, " + f"json_body: {json_body}, " + f"headers: {headers}" + ) + + request_data = {} + if files is not None and isinstance(files, dict) and len(files) > 0: + if body_params: + for k, v in body_params.items(): + request_data.update({k: v}) + + for k, v in files.items(): + if isinstance(v, str): + f: BinaryIO = open(v.encode("utf-8", "ignore"), "rb") + files_to_close.append(f) + request_data.update({k: f}) + elif isinstance(v, (bytearray, bytes)): + request_data.update({k: io.BytesIO(v)}) + else: + request_data.update({k: v}) + + request_headers = self._build_urllib_request_headers( + token=token or self.token, + has_json=json is not None, + has_files=files is not None, + additional_headers=additional_headers, + ) + request_args = { + "headers": request_headers, + "data": request_data, + "params": body_params, + "files": files, + "json": json_body, + } + if query_params: + q = urlencode(query_params) + url = f"{url}&{q}" if "?" in url else f"{url}?{q}" + + response = self._perform_urllib_http_request(url=url, args=request_args) + body = response.get("body", None) + response_body_data: Optional[Union[dict, bytes]] = body + if body is not None and not isinstance(body, bytes): + try: + response_body_data = json.loads(response["body"]) + except json.decoder.JSONDecodeError: + message = _build_unexpected_body_error_message(response.get("body", "")) + raise err.SlackApiError(message, response) + + all_params: Dict[str, Any] = copy.copy(body_params) if body_params is not None else {} + if query_params: + all_params.update(query_params) + request_args["params"] = all_params # for backward-compatibility + + return SlackResponse( + client=self, + http_verb="POST", # you can use POST method for all the Web APIs + api_url=url, + req_args=request_args, + data=response_body_data, + headers=dict(response["headers"]), + status_code=response["status"], + use_sync_aiohttp=False, + ).validate() + finally: + for f in files_to_close: + if not f.closed: + f.close() + + def _perform_urllib_http_request(self, *, url: str, args: Dict[str, Dict[str, Any]]) -> Dict[str, Any]: + """Performs an HTTP request and parses the response. + + Args: + url: Complete URL (e.g., https://slack.com/api/chat.postMessage) + args: args has "headers", "data", "params", and "json" + "headers": Dict[str, str] + "data": Dict[str, Any] + "params": Dict[str, str], + "json": Dict[str, Any], + + Returns: + dict {status: int, headers: Headers, body: str} + """ + headers = args["headers"] + if args["json"]: + body = json.dumps(args["json"]) + headers["Content-Type"] = "application/json;charset=utf-8" + elif args["data"]: + boundary = f"--------------{uuid.uuid4()}" + sep_boundary = b"\r\n--" + boundary.encode("ascii") + end_boundary = sep_boundary + b"--\r\n" + body = io.BytesIO() + data = args["data"] + for key, value in data.items(): + readable = getattr(value, "readable", None) + if readable and value.readable(): + filename = "Uploaded file" + name_attr = getattr(value, "name", None) + if name_attr: + filename = name_attr.decode("utf-8") if isinstance(name_attr, bytes) else name_attr + if "filename" in data: + filename = data["filename"] + mimetype = mimetypes.guess_type(filename)[0] or "application/octet-stream" + title = ( + f'\r\nContent-Disposition: form-data; name="{key}"; filename="{filename}"\r\n' + + f"Content-Type: {mimetype}\r\n" + ) + value = value.read() + else: + title = f'\r\nContent-Disposition: form-data; name="{key}"\r\n' + value = str(value).encode("utf-8") + body.write(sep_boundary) + body.write(title.encode("utf-8")) + body.write(b"\r\n") + body.write(value) + + body.write(end_boundary) + body = body.getvalue() + headers["Content-Type"] = f"multipart/form-data; boundary={boundary}" + headers["Content-Length"] = len(body) + elif args["params"]: + body = urlencode(args["params"]) + headers["Content-Type"] = "application/x-www-form-urlencoded" + else: + body = None + + if isinstance(body, str): + body = body.encode("utf-8") + + # NOTE: Intentionally ignore the `http_verb` here + # Slack APIs accepts any API method requests with POST methods + try: + # urllib not only opens http:// or https:// URLs, but also ftp:// and file://. + # With this it might be possible to open local files on the executing machine + # which might be a security risk if the URL to open can be manipulated by an external user. + # (BAN-B310) + if url.lower().startswith("http"): + req = Request(method="POST", url=url, data=body, headers=headers) + opener: Optional[OpenerDirector] = None + if self.proxy is not None: + if isinstance(self.proxy, str): + opener = urllib.request.build_opener( + ProxyHandler({"http": self.proxy, "https": self.proxy}), + HTTPSHandler(context=self.ssl), + ) + else: + raise SlackRequestError(f"Invalid proxy detected: {self.proxy} must be a str value") + + # NOTE: BAN-B310 is already checked above + resp: Optional[HTTPResponse] = None + if opener: + resp = opener.open(req, timeout=self.timeout) + else: + resp = urlopen(req, context=self.ssl, timeout=self.timeout) + if resp.headers.get_content_type() == "application/gzip": + # admin.analytics.getFile + body: bytes = resp.read() + return {"status": resp.code, "headers": resp.headers, "body": body} + + charset = resp.headers.get_content_charset() or "utf-8" + body: str = resp.read().decode(charset) # read the response body here + return {"status": resp.code, "headers": resp.headers, "body": body} + raise SlackRequestError(f"Invalid URL detected: {url}") + except HTTPError as e: + # As adding new values to HTTPError#headers can be ignored, building a new dict object here + response_headers = dict(e.headers.items()) + resp = {"status": e.code, "headers": response_headers} + if e.code == 429: + # for compatibility with aiohttp + if "retry-after" not in response_headers and "Retry-After" in response_headers: + response_headers["retry-after"] = response_headers["Retry-After"] + if "Retry-After" not in response_headers and "retry-after" in response_headers: + response_headers["Retry-After"] = response_headers["retry-after"] + + # read the response body here + charset = e.headers.get_content_charset() or "utf-8" + body: str = e.read().decode(charset) + resp["body"] = body + return resp + + except Exception as err: + self._logger.error(f"Failed to send a request to Slack API server: {err}") + raise err + + def _build_urllib_request_headers( + self, token: str, has_json: bool, has_files: bool, additional_headers: dict + ) -> Dict[str, str]: + headers = {"Content-Type": "application/x-www-form-urlencoded"} + headers.update(self.headers) + if token: + headers.update({"Authorization": "Bearer {}".format(token)}) + if additional_headers: + headers.update(additional_headers) + if has_json: + headers.update({"Content-Type": "application/json;charset=utf-8"}) + if has_files: + # will be set afterward + headers.pop("Content-Type", None) + return headers + + def _upload_file( + self, + *, + url: str, + data: bytes, + logger: logging.Logger, + timeout: int, + proxy: Optional[str], + ssl: Optional[SSLContext], + ) -> FileUploadV2Result: + result = _upload_file_via_v2_url( + url=url, + data=data, + logger=logger, + timeout=timeout, + proxy=proxy, + ssl=ssl, + ) + return FileUploadV2Result( + status=result.get("status"), + body=result.get("body"), + ) + + # ================================================================= + + @staticmethod + def validate_slack_signature(*, signing_secret: str, data: str, timestamp: str, signature: str) -> bool: + """ + Slack creates a unique string for your app and shares it with you. Verify + requests from Slack with confidence by verifying signatures using your + signing secret. + On each HTTP request that Slack sends, we add an X-Slack-Signature HTTP + header. The signature is created by combining the signing secret with the + body of the request we're sending using a standard HMAC-SHA256 keyed hash. + https://docs.slack.dev/authentication/verifying-requests-from-slack/#how_to_make_a_request_signature_in_4_easy_steps__an_overview + Args: + signing_secret: Your application's signing secret, available in the + Slack API dashboard + data: The raw body of the incoming request - no headers, just the body. + timestamp: from the 'X-Slack-Request-Timestamp' header + signature: from the 'X-Slack-Signature' header - the calculated signature + should match this. + Returns: + True if signatures matches + """ + warnings.warn( + "As this method is deprecated since slackclient 2.6.0, " + "use `from slack.signature import SignatureVerifier` instead", + DeprecationWarning, + ) + format_req = str.encode(f"v0:{timestamp}:{data}") + encoded_secret = str.encode(signing_secret) + request_hash = hmac.new(encoded_secret, format_req, hashlib.sha256).hexdigest() + calculated_signature = f"v0={request_hash}" + return hmac.compare_digest(calculated_signature, signature) diff --git a/slack_sdk/web/legacy_client.py b/slack_sdk/web/legacy_client.py new file mode 100644 index 000000000..df2bcc370 --- /dev/null +++ b/slack_sdk/web/legacy_client.py @@ -0,0 +1,5895 @@ +# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! +# +# *** DO NOT EDIT THIS FILE *** +# +# 1) Modify slack_sdk/web/client.py +# 2) Run `python scripts/codegen.py` +# 3) Run `black slack_sdk/` +# +# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +from asyncio import Future + +"""A Python module for interacting with Slack's Web API.""" + +import json +import os +import warnings +from io import IOBase +from typing import Any, Dict, List, Optional, Sequence, Union + +import slack_sdk.errors as e +from slack_sdk.models.views import View + +from ..models.attachments import Attachment +from ..models.blocks import Block, RichTextBlock +from ..models.metadata import Metadata, EntityMetadata, EventAndEntityMetadata +from .legacy_base_client import LegacyBaseClient, SlackResponse +from .internal_utils import ( + _parse_web_class_objects, + _print_files_upload_v2_suggestion, + _remove_none_values, + _to_v2_file_upload_item, + _update_call_participants, + _validate_for_legacy_client, + _warn_if_message_text_content_is_missing, +) + + +class LegacyWebClient(LegacyBaseClient): + """A WebClient allows apps to communicate with the Slack Platform's Web API. + + https://docs.slack.dev/reference/methods + + The Slack Web API is an interface for querying information from + and enacting change in a Slack workspace. + + This client handles constructing and sending HTTP requests to Slack + as well as parsing any responses received into a `SlackResponse`. + + Attributes: + token (str): A string specifying an `xoxp-*` or `xoxb-*` token. + base_url (str): A string representing the Slack API base URL. + Default is `'https://slack.com/api/'` + timeout (int): The maximum number of seconds the client will wait + to connect and receive a response from Slack. + Default is 30 seconds. + ssl (SSLContext): An [`ssl.SSLContext`][1] instance, helpful for specifying + your own custom certificate chain. + proxy (str): String representing a fully-qualified URL to a proxy through + which to route all requests to the Slack API. Even if this parameter + is not specified, if any of the following environment variables are + present, they will be loaded into this parameter: `HTTPS_PROXY`, + `https_proxy`, `HTTP_PROXY` or `http_proxy`. + headers (dict): Additional request headers to attach to all requests. + + Methods: + `api_call`: Constructs a request and executes the API call to Slack. + + Example of recommended usage: + ```python + import os + from slack_sdk.web.legacy_client import LegacyWebClient + + client = LegacyWebClient(token=os.environ['SLACK_API_TOKEN']) + response = client.chat_postMessage( + channel='#random', + text="Hello world!") + assert response["ok"] + assert response["message"]["text"] == "Hello world!" + ``` + + Example manually creating an API request: + ```python + import os + from slack_sdk.web.legacy_client import LegacyWebClient + + client = LegacyWebClient(token=os.environ['SLACK_API_TOKEN']) + response = client.api_call( + api_method='chat.postMessage', + json={'channel': '#random','text': "Hello world!"} + ) + assert response["ok"] + assert response["message"]["text"] == "Hello world!" + ``` + + Note: + Any attributes or methods prefixed with _underscores are + intended to be "private" internal use only. They may be changed or + removed at anytime. + + [1]: https://docs.python.org/3/library/ssl.html#ssl.SSLContext + """ + + def admin_analytics_getFile( + self, + *, + type: str, + date: Optional[str] = None, + metadata_only: Optional[bool] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Retrieve analytics data for a given date, presented as a compressed JSON file + https://docs.slack.dev/reference/methods/admin.analytics.getFile + """ + kwargs.update({"type": type}) + if date is not None: + kwargs.update({"date": date}) + if metadata_only is not None: + kwargs.update({"metadata_only": metadata_only}) + return self.api_call("admin.analytics.getFile", params=kwargs) + + def admin_apps_approve( + self, + *, + app_id: Optional[str] = None, + request_id: Optional[str] = None, + enterprise_id: Optional[str] = None, + team_id: Optional[str] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Approve an app for installation on a workspace. + Either app_id or request_id is required. + These IDs can be obtained either directly via the app_requested event, + or by the admin.apps.requests.list method. + https://docs.slack.dev/reference/methods/admin.apps.approve + """ + if app_id: + kwargs.update({"app_id": app_id}) + elif request_id: + kwargs.update({"request_id": request_id}) + else: + raise e.SlackRequestError("The app_id or request_id argument must be specified.") + + kwargs.update( + { + "enterprise_id": enterprise_id, + "team_id": team_id, + } + ) + return self.api_call("admin.apps.approve", params=kwargs) + + def admin_apps_approved_list( + self, + *, + cursor: Optional[str] = None, + limit: Optional[int] = None, + enterprise_id: Optional[str] = None, + team_id: Optional[str] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """List approved apps for an org or workspace. + https://docs.slack.dev/reference/methods/admin.apps.approved.list + """ + kwargs.update( + { + "cursor": cursor, + "limit": limit, + "enterprise_id": enterprise_id, + "team_id": team_id, + } + ) + return self.api_call("admin.apps.approved.list", http_verb="GET", params=kwargs) + + def admin_apps_clearResolution( + self, + *, + app_id: str, + enterprise_id: Optional[str] = None, + team_id: Optional[str] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Clear an app resolution + https://docs.slack.dev/reference/methods/admin.apps.clearResolution + """ + kwargs.update( + { + "app_id": app_id, + "enterprise_id": enterprise_id, + "team_id": team_id, + } + ) + return self.api_call("admin.apps.clearResolution", http_verb="POST", params=kwargs) + + def admin_apps_requests_cancel( + self, + *, + request_id: str, + enterprise_id: Optional[str] = None, + team_id: Optional[str] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """List app requests for a team/workspace. + https://docs.slack.dev/reference/methods/admin.apps.requests.cancel + """ + kwargs.update( + { + "request_id": request_id, + "enterprise_id": enterprise_id, + "team_id": team_id, + } + ) + return self.api_call("admin.apps.requests.cancel", http_verb="POST", params=kwargs) + + def admin_apps_requests_list( + self, + *, + cursor: Optional[str] = None, + limit: Optional[int] = None, + team_id: Optional[str] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """List app requests for a team/workspace. + https://docs.slack.dev/reference/methods/admin.apps.requests.list + """ + kwargs.update( + { + "cursor": cursor, + "limit": limit, + "team_id": team_id, + } + ) + return self.api_call("admin.apps.requests.list", http_verb="GET", params=kwargs) + + def admin_apps_restrict( + self, + *, + app_id: Optional[str] = None, + request_id: Optional[str] = None, + enterprise_id: Optional[str] = None, + team_id: Optional[str] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Restrict an app for installation on a workspace. + Exactly one of the team_id or enterprise_id arguments is required, not both. + Either app_id or request_id is required. These IDs can be obtained either directly + via the app_requested event, or by the admin.apps.requests.list method. + https://docs.slack.dev/reference/methods/admin.apps.restrict + """ + if app_id: + kwargs.update({"app_id": app_id}) + elif request_id: + kwargs.update({"request_id": request_id}) + else: + raise e.SlackRequestError("The app_id or request_id argument must be specified.") + + kwargs.update( + { + "enterprise_id": enterprise_id, + "team_id": team_id, + } + ) + return self.api_call("admin.apps.restrict", params=kwargs) + + def admin_apps_restricted_list( + self, + *, + cursor: Optional[str] = None, + limit: Optional[int] = None, + enterprise_id: Optional[str] = None, + team_id: Optional[str] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """List restricted apps for an org or workspace. + https://docs.slack.dev/reference/methods/admin.apps.restricted.list + """ + kwargs.update( + { + "cursor": cursor, + "limit": limit, + "enterprise_id": enterprise_id, + "team_id": team_id, + } + ) + return self.api_call("admin.apps.restricted.list", http_verb="GET", params=kwargs) + + def admin_apps_uninstall( + self, + *, + app_id: str, + enterprise_id: Optional[str] = None, + team_ids: Optional[Union[str, Sequence[str]]] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Uninstall an app from one or many workspaces, or an entire enterprise organization. + With an org-level token, enterprise_id or team_ids is required. + https://docs.slack.dev/reference/methods/admin.apps.uninstall + """ + kwargs.update({"app_id": app_id}) + if enterprise_id is not None: + kwargs.update({"enterprise_id": enterprise_id}) + if team_ids is not None: + if isinstance(team_ids, (list, tuple)): + kwargs.update({"team_ids": ",".join(team_ids)}) + else: + kwargs.update({"team_ids": team_ids}) + return self.api_call("admin.apps.uninstall", http_verb="POST", params=kwargs) + + def admin_apps_activities_list( + self, + *, + app_id: Optional[str] = None, + component_id: Optional[str] = None, + component_type: Optional[str] = None, + log_event_type: Optional[str] = None, + max_date_created: Optional[int] = None, + min_date_created: Optional[int] = None, + min_log_level: Optional[str] = None, + sort_direction: Optional[str] = None, + source: Optional[str] = None, + team_id: Optional[str] = None, + trace_id: Optional[str] = None, + cursor: Optional[str] = None, + limit: Optional[int] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Get logs for a specified team/org + https://docs.slack.dev/reference/methods/admin.apps.activities.list + """ + kwargs.update( + { + "app_id": app_id, + "component_id": component_id, + "component_type": component_type, + "log_event_type": log_event_type, + "max_date_created": max_date_created, + "min_date_created": min_date_created, + "min_log_level": min_log_level, + "sort_direction": sort_direction, + "source": source, + "team_id": team_id, + "trace_id": trace_id, + "cursor": cursor, + "limit": limit, + } + ) + return self.api_call("admin.apps.activities.list", params=kwargs) + + def admin_apps_config_lookup( + self, + *, + app_ids: Union[str, Sequence[str]], + **kwargs, + ) -> Union[Future, SlackResponse]: + """Look up the app config for connectors by their IDs + https://docs.slack.dev/reference/methods/admin.apps.config.lookup + """ + if isinstance(app_ids, (list, tuple)): + kwargs.update({"app_ids": ",".join(app_ids)}) + else: + kwargs.update({"app_ids": app_ids}) + return self.api_call("admin.apps.config.lookup", params=kwargs) + + def admin_apps_config_set( + self, + *, + app_id: str, + domain_restrictions: Optional[Dict[str, Any]] = None, + workflow_auth_strategy: Optional[str] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Set the app config for a connector + https://docs.slack.dev/reference/methods/admin.apps.config.set + """ + kwargs.update( + { + "app_id": app_id, + "workflow_auth_strategy": workflow_auth_strategy, + } + ) + if domain_restrictions is not None: + kwargs.update({"domain_restrictions": json.dumps(domain_restrictions)}) + return self.api_call("admin.apps.config.set", params=kwargs) + + def admin_auth_policy_getEntities( + self, + *, + policy_name: str, + cursor: Optional[str] = None, + entity_type: Optional[str] = None, + limit: Optional[int] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Fetch all the entities assigned to a particular authentication policy by name. + https://docs.slack.dev/reference/methods/admin.auth.policy.getEntities + """ + kwargs.update({"policy_name": policy_name}) + if cursor is not None: + kwargs.update({"cursor": cursor}) + if entity_type is not None: + kwargs.update({"entity_type": entity_type}) + if limit is not None: + kwargs.update({"limit": limit}) + return self.api_call("admin.auth.policy.getEntities", http_verb="POST", params=kwargs) + + def admin_auth_policy_assignEntities( + self, + *, + entity_ids: Union[str, Sequence[str]], + policy_name: str, + entity_type: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Assign entities to a particular authentication policy. + https://docs.slack.dev/reference/methods/admin.auth.policy.assignEntities + """ + if isinstance(entity_ids, (list, tuple)): + kwargs.update({"entity_ids": ",".join(entity_ids)}) + else: + kwargs.update({"entity_ids": entity_ids}) + kwargs.update({"policy_name": policy_name}) + kwargs.update({"entity_type": entity_type}) + return self.api_call("admin.auth.policy.assignEntities", http_verb="POST", params=kwargs) + + def admin_auth_policy_removeEntities( + self, + *, + entity_ids: Union[str, Sequence[str]], + policy_name: str, + entity_type: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Remove specified entities from a specified authentication policy. + https://docs.slack.dev/reference/methods/admin.auth.policy.removeEntities + """ + if isinstance(entity_ids, (list, tuple)): + kwargs.update({"entity_ids": ",".join(entity_ids)}) + else: + kwargs.update({"entity_ids": entity_ids}) + kwargs.update({"policy_name": policy_name}) + kwargs.update({"entity_type": entity_type}) + return self.api_call("admin.auth.policy.removeEntities", http_verb="POST", params=kwargs) + + def admin_conversations_createForObjects( + self, + *, + object_id: str, + salesforce_org_id: str, + invite_object_team: Optional[bool] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Create a Salesforce channel for the corresponding object provided. + https://docs.slack.dev/reference/methods/admin.conversations.createForObjects + """ + kwargs.update( + {"object_id": object_id, "salesforce_org_id": salesforce_org_id, "invite_object_team": invite_object_team} + ) + return self.api_call("admin.conversations.createForObjects", params=kwargs) + + def admin_conversations_linkObjects( + self, + *, + channel: str, + record_id: str, + salesforce_org_id: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Link a Salesforce record to a channel. + https://docs.slack.dev/reference/methods/admin.conversations.linkObjects + """ + kwargs.update( + { + "channel": channel, + "record_id": record_id, + "salesforce_org_id": salesforce_org_id, + } + ) + return self.api_call("admin.conversations.linkObjects", params=kwargs) + + def admin_conversations_unlinkObjects( + self, + *, + channel: str, + new_name: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Unlink a Salesforce record from a channel. + https://docs.slack.dev/reference/methods/admin.conversations.unlinkObjects + """ + kwargs.update( + { + "channel": channel, + "new_name": new_name, + } + ) + return self.api_call("admin.conversations.unlinkObjects", params=kwargs) + + def admin_barriers_create( + self, + *, + barriered_from_usergroup_ids: Union[str, Sequence[str]], + primary_usergroup_id: str, + restricted_subjects: Union[str, Sequence[str]], + **kwargs, + ) -> Union[Future, SlackResponse]: + """Create an Information Barrier + https://docs.slack.dev/reference/methods/admin.barriers.create + """ + kwargs.update({"primary_usergroup_id": primary_usergroup_id}) + if isinstance(barriered_from_usergroup_ids, (list, tuple)): + kwargs.update({"barriered_from_usergroup_ids": ",".join(barriered_from_usergroup_ids)}) + else: + kwargs.update({"barriered_from_usergroup_ids": barriered_from_usergroup_ids}) + if isinstance(restricted_subjects, (list, tuple)): + kwargs.update({"restricted_subjects": ",".join(restricted_subjects)}) + else: + kwargs.update({"restricted_subjects": restricted_subjects}) + return self.api_call("admin.barriers.create", http_verb="POST", params=kwargs) + + def admin_barriers_delete( + self, + *, + barrier_id: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Delete an existing Information Barrier + https://docs.slack.dev/reference/methods/admin.barriers.delete + """ + kwargs.update({"barrier_id": barrier_id}) + return self.api_call("admin.barriers.delete", http_verb="POST", params=kwargs) + + def admin_barriers_update( + self, + *, + barrier_id: str, + barriered_from_usergroup_ids: Union[str, Sequence[str]], + primary_usergroup_id: str, + restricted_subjects: Union[str, Sequence[str]], + **kwargs, + ) -> Union[Future, SlackResponse]: + """Update an existing Information Barrier + https://docs.slack.dev/reference/methods/admin.barriers.update + """ + kwargs.update({"barrier_id": barrier_id, "primary_usergroup_id": primary_usergroup_id}) + if isinstance(barriered_from_usergroup_ids, (list, tuple)): + kwargs.update({"barriered_from_usergroup_ids": ",".join(barriered_from_usergroup_ids)}) + else: + kwargs.update({"barriered_from_usergroup_ids": barriered_from_usergroup_ids}) + if isinstance(restricted_subjects, (list, tuple)): + kwargs.update({"restricted_subjects": ",".join(restricted_subjects)}) + else: + kwargs.update({"restricted_subjects": restricted_subjects}) + return self.api_call("admin.barriers.update", http_verb="POST", params=kwargs) + + def admin_barriers_list( + self, + *, + cursor: Optional[str] = None, + limit: Optional[int] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Get all Information Barriers for your organization + https://docs.slack.dev/reference/methods/admin.barriers.list""" + kwargs.update( + { + "cursor": cursor, + "limit": limit, + } + ) + return self.api_call("admin.barriers.list", http_verb="GET", params=kwargs) + + def admin_conversations_create( + self, + *, + is_private: bool, + name: str, + description: Optional[str] = None, + org_wide: Optional[bool] = None, + team_id: Optional[str] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Create a public or private channel-based conversation. + https://docs.slack.dev/reference/methods/admin.conversations.create + """ + kwargs.update( + { + "is_private": is_private, + "name": name, + "description": description, + "org_wide": org_wide, + "team_id": team_id, + } + ) + return self.api_call("admin.conversations.create", params=kwargs) + + def admin_conversations_delete( + self, + *, + channel_id: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Delete a public or private channel. + https://docs.slack.dev/reference/methods/admin.conversations.delete + """ + kwargs.update({"channel_id": channel_id}) + return self.api_call("admin.conversations.delete", params=kwargs) + + def admin_conversations_invite( + self, + *, + channel_id: str, + user_ids: Union[str, Sequence[str]], + **kwargs, + ) -> Union[Future, SlackResponse]: + """Invite a user to a public or private channel. + https://docs.slack.dev/reference/methods/admin.conversations.invite + """ + kwargs.update({"channel_id": channel_id}) + if isinstance(user_ids, (list, tuple)): + kwargs.update({"user_ids": ",".join(user_ids)}) + else: + kwargs.update({"user_ids": user_ids}) + # NOTE: the endpoint is unable to handle Content-Type: application/json as of Sep 3, 2020. + return self.api_call("admin.conversations.invite", params=kwargs) + + def admin_conversations_archive( + self, + *, + channel_id: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Archive a public or private channel. + https://docs.slack.dev/reference/methods/admin.conversations.archive + """ + kwargs.update({"channel_id": channel_id}) + return self.api_call("admin.conversations.archive", params=kwargs) + + def admin_conversations_unarchive( + self, + *, + channel_id: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Unarchive a public or private channel. + https://docs.slack.dev/reference/methods/admin.conversations.archive + """ + kwargs.update({"channel_id": channel_id}) + return self.api_call("admin.conversations.unarchive", params=kwargs) + + def admin_conversations_rename( + self, + *, + channel_id: str, + name: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Rename a public or private channel. + https://docs.slack.dev/reference/methods/admin.conversations.rename + """ + kwargs.update({"channel_id": channel_id, "name": name}) + return self.api_call("admin.conversations.rename", params=kwargs) + + def admin_conversations_search( + self, + *, + cursor: Optional[str] = None, + limit: Optional[int] = None, + query: Optional[str] = None, + search_channel_types: Optional[Union[str, Sequence[str]]] = None, + sort: Optional[str] = None, + sort_dir: Optional[str] = None, + team_ids: Optional[Union[str, Sequence[str]]] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Search for public or private channels in an Enterprise organization. + https://docs.slack.dev/reference/methods/admin.conversations.search + """ + kwargs.update( + { + "cursor": cursor, + "limit": limit, + "query": query, + "sort": sort, + "sort_dir": sort_dir, + } + ) + + if isinstance(search_channel_types, (list, tuple)): + kwargs.update({"search_channel_types": ",".join(search_channel_types)}) + else: + kwargs.update({"search_channel_types": search_channel_types}) + + if isinstance(team_ids, (list, tuple)): + kwargs.update({"team_ids": ",".join(team_ids)}) + else: + kwargs.update({"team_ids": team_ids}) + + return self.api_call("admin.conversations.search", params=kwargs) + + def admin_conversations_convertToPrivate( + self, + *, + channel_id: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Convert a public channel to a private channel. + https://docs.slack.dev/reference/methods/admin.conversations.convertToPrivate + """ + kwargs.update({"channel_id": channel_id}) + return self.api_call("admin.conversations.convertToPrivate", params=kwargs) + + def admin_conversations_convertToPublic( + self, + *, + channel_id: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Convert a privte channel to a public channel. + https://docs.slack.dev/reference/methods/admin.conversations.convertToPublic + """ + kwargs.update({"channel_id": channel_id}) + return self.api_call("admin.conversations.convertToPublic", params=kwargs) + + def admin_conversations_setConversationPrefs( + self, + *, + channel_id: str, + prefs: Union[str, Dict[str, str]], + **kwargs, + ) -> Union[Future, SlackResponse]: + """Set the posting permissions for a public or private channel. + https://docs.slack.dev/reference/methods/admin.conversations.setConversationPrefs + """ + kwargs.update({"channel_id": channel_id}) + if isinstance(prefs, dict): + kwargs.update({"prefs": json.dumps(prefs)}) + else: + kwargs.update({"prefs": prefs}) + return self.api_call("admin.conversations.setConversationPrefs", params=kwargs) + + def admin_conversations_getConversationPrefs( + self, + *, + channel_id: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Get conversation preferences for a public or private channel. + https://docs.slack.dev/reference/methods/admin.conversations.getConversationPrefs + """ + kwargs.update({"channel_id": channel_id}) + return self.api_call("admin.conversations.getConversationPrefs", params=kwargs) + + def admin_conversations_disconnectShared( + self, + *, + channel_id: str, + leaving_team_ids: Optional[Union[str, Sequence[str]]] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Disconnect a connected channel from one or more workspaces. + https://docs.slack.dev/reference/methods/admin.conversations.disconnectShared + """ + kwargs.update({"channel_id": channel_id}) + if isinstance(leaving_team_ids, (list, tuple)): + kwargs.update({"leaving_team_ids": ",".join(leaving_team_ids)}) + else: + kwargs.update({"leaving_team_ids": leaving_team_ids}) + return self.api_call("admin.conversations.disconnectShared", params=kwargs) + + def admin_conversations_lookup( + self, + *, + last_message_activity_before: int, + team_ids: Union[str, Sequence[str]], + cursor: Optional[str] = None, + limit: Optional[int] = None, + max_member_count: Optional[int] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Returns channels on the given team using the filters. + https://docs.slack.dev/reference/methods/admin.conversations.lookup + """ + kwargs.update( + { + "last_message_activity_before": last_message_activity_before, + "cursor": cursor, + "limit": limit, + "max_member_count": max_member_count, + } + ) + if isinstance(team_ids, (list, tuple)): + kwargs.update({"team_ids": ",".join(team_ids)}) + else: + kwargs.update({"team_ids": team_ids}) + return self.api_call("admin.conversations.lookup", params=kwargs) + + def admin_conversations_ekm_listOriginalConnectedChannelInfo( + self, + *, + channel_ids: Optional[Union[str, Sequence[str]]] = None, + cursor: Optional[str] = None, + limit: Optional[int] = None, + team_ids: Optional[Union[str, Sequence[str]]] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """List all disconnected channels—i.e., + channels that were once connected to other workspaces and then disconnected—and + the corresponding original channel IDs for key revocation with EKM. + https://docs.slack.dev/reference/methods/admin.conversations.ekm.listOriginalConnectedChannelInfo + """ + kwargs.update( + { + "cursor": cursor, + "limit": limit, + } + ) + if isinstance(channel_ids, (list, tuple)): + kwargs.update({"channel_ids": ",".join(channel_ids)}) + else: + kwargs.update({"channel_ids": channel_ids}) + if isinstance(team_ids, (list, tuple)): + kwargs.update({"team_ids": ",".join(team_ids)}) + else: + kwargs.update({"team_ids": team_ids}) + return self.api_call("admin.conversations.ekm.listOriginalConnectedChannelInfo", params=kwargs) + + def admin_conversations_restrictAccess_addGroup( + self, + *, + channel_id: str, + group_id: str, + team_id: Optional[str] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Add an allowlist of IDP groups for accessing a channel. + https://docs.slack.dev/reference/methods/admin.conversations.restrictAccess.addGroup + """ + kwargs.update( + { + "channel_id": channel_id, + "group_id": group_id, + "team_id": team_id, + } + ) + return self.api_call( + "admin.conversations.restrictAccess.addGroup", + http_verb="GET", + params=kwargs, + ) + + def admin_conversations_restrictAccess_listGroups( + self, + *, + channel_id: str, + team_id: Optional[str] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """List all IDP Groups linked to a channel. + https://docs.slack.dev/reference/methods/admin.conversations.restrictAccess.listGroups + """ + kwargs.update( + { + "channel_id": channel_id, + "team_id": team_id, + } + ) + return self.api_call( + "admin.conversations.restrictAccess.listGroups", + http_verb="GET", + params=kwargs, + ) + + def admin_conversations_restrictAccess_removeGroup( + self, + *, + channel_id: str, + group_id: str, + team_id: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Remove a linked IDP group linked from a private channel. + https://docs.slack.dev/reference/methods/admin.conversations.restrictAccess.removeGroup + """ + kwargs.update( + { + "channel_id": channel_id, + "group_id": group_id, + "team_id": team_id, + } + ) + return self.api_call( + "admin.conversations.restrictAccess.removeGroup", + http_verb="GET", + params=kwargs, + ) + + def admin_conversations_setTeams( + self, + *, + channel_id: str, + org_channel: Optional[bool] = None, + target_team_ids: Optional[Union[str, Sequence[str]]] = None, + team_id: Optional[str] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Set the workspaces in an Enterprise grid org that connect to a public or private channel. + https://docs.slack.dev/reference/methods/admin.conversations.setTeams + """ + kwargs.update( + { + "channel_id": channel_id, + "org_channel": org_channel, + "team_id": team_id, + } + ) + if isinstance(target_team_ids, (list, tuple)): + kwargs.update({"target_team_ids": ",".join(target_team_ids)}) + else: + kwargs.update({"target_team_ids": target_team_ids}) + return self.api_call("admin.conversations.setTeams", params=kwargs) + + def admin_conversations_getTeams( + self, + *, + channel_id: str, + cursor: Optional[str] = None, + limit: Optional[int] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Set the workspaces in an Enterprise grid org that connect to a channel. + https://docs.slack.dev/reference/methods/admin.conversations.getTeams + """ + kwargs.update( + { + "channel_id": channel_id, + "cursor": cursor, + "limit": limit, + } + ) + return self.api_call("admin.conversations.getTeams", params=kwargs) + + def admin_conversations_getCustomRetention( + self, + *, + channel_id: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Get a channel's retention policy + https://docs.slack.dev/reference/methods/admin.conversations.getCustomRetention + """ + kwargs.update({"channel_id": channel_id}) + return self.api_call("admin.conversations.getCustomRetention", params=kwargs) + + def admin_conversations_removeCustomRetention( + self, + *, + channel_id: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Remove a channel's retention policy + https://docs.slack.dev/reference/methods/admin.conversations.removeCustomRetention + """ + kwargs.update({"channel_id": channel_id}) + return self.api_call("admin.conversations.removeCustomRetention", params=kwargs) + + def admin_conversations_setCustomRetention( + self, + *, + channel_id: str, + duration_days: int, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Set a channel's retention policy + https://docs.slack.dev/reference/methods/admin.conversations.setCustomRetention + """ + kwargs.update({"channel_id": channel_id, "duration_days": duration_days}) + return self.api_call("admin.conversations.setCustomRetention", params=kwargs) + + def admin_conversations_bulkArchive( + self, + *, + channel_ids: Union[Sequence[str], str], + **kwargs, + ) -> Union[Future, SlackResponse]: + """Archive public or private channels in bulk. + https://docs.slack.dev/reference/methods/admin.conversations.bulkArchive + """ + kwargs.update({"channel_ids": ",".join(channel_ids) if isinstance(channel_ids, (list, tuple)) else channel_ids}) + return self.api_call("admin.conversations.bulkArchive", params=kwargs) + + def admin_conversations_bulkDelete( + self, + *, + channel_ids: Union[Sequence[str], str], + **kwargs, + ) -> Union[Future, SlackResponse]: + """Delete public or private channels in bulk. + https://slack.com/api/admin.conversations.bulkDelete + """ + kwargs.update({"channel_ids": ",".join(channel_ids) if isinstance(channel_ids, (list, tuple)) else channel_ids}) + return self.api_call("admin.conversations.bulkDelete", params=kwargs) + + def admin_conversations_bulkMove( + self, + *, + channel_ids: Union[Sequence[str], str], + target_team_id: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Move public or private channels in bulk. + https://docs.slack.dev/reference/methods/admin.conversations.bulkMove + """ + kwargs.update( + { + "target_team_id": target_team_id, + "channel_ids": ",".join(channel_ids) if isinstance(channel_ids, (list, tuple)) else channel_ids, + } + ) + return self.api_call("admin.conversations.bulkMove", params=kwargs) + + def admin_emoji_add( + self, + *, + name: str, + url: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Add an emoji. + https://docs.slack.dev/reference/methods/admin.emoji.add + """ + kwargs.update({"name": name, "url": url}) + return self.api_call("admin.emoji.add", http_verb="GET", params=kwargs) + + def admin_emoji_addAlias( + self, + *, + alias_for: str, + name: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Add an emoji alias. + https://docs.slack.dev/reference/methods/admin.emoji.addAlias + """ + kwargs.update({"alias_for": alias_for, "name": name}) + return self.api_call("admin.emoji.addAlias", http_verb="GET", params=kwargs) + + def admin_emoji_list( + self, + *, + cursor: Optional[str] = None, + limit: Optional[int] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """List emoji for an Enterprise Grid organization. + https://docs.slack.dev/reference/methods/admin.emoji.list + """ + kwargs.update({"cursor": cursor, "limit": limit}) + return self.api_call("admin.emoji.list", http_verb="GET", params=kwargs) + + def admin_emoji_remove( + self, + *, + name: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Remove an emoji across an Enterprise Grid organization. + https://docs.slack.dev/reference/methods/admin.emoji.remove + """ + kwargs.update({"name": name}) + return self.api_call("admin.emoji.remove", http_verb="GET", params=kwargs) + + def admin_emoji_rename( + self, + *, + name: str, + new_name: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Rename an emoji. + https://docs.slack.dev/reference/methods/admin.emoji.rename + """ + kwargs.update({"name": name, "new_name": new_name}) + return self.api_call("admin.emoji.rename", http_verb="GET", params=kwargs) + + def admin_functions_list( + self, + *, + app_ids: Union[str, Sequence[str]], + team_id: Optional[str] = None, + cursor: Optional[str] = None, + limit: Optional[int] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Look up functions by a set of apps + https://docs.slack.dev/reference/methods/admin.functions.list + """ + if isinstance(app_ids, (list, tuple)): + kwargs.update({"app_ids": ",".join(app_ids)}) + else: + kwargs.update({"app_ids": app_ids}) + kwargs.update( + { + "team_id": team_id, + "cursor": cursor, + "limit": limit, + } + ) + return self.api_call("admin.functions.list", params=kwargs) + + def admin_functions_permissions_lookup( + self, + *, + function_ids: Union[str, Sequence[str]], + **kwargs, + ) -> Union[Future, SlackResponse]: + """Lookup the visibility of multiple Slack functions + and include the users if it is limited to particular named entities. + https://docs.slack.dev/reference/methods/admin.functions.permissions.lookup + """ + if isinstance(function_ids, (list, tuple)): + kwargs.update({"function_ids": ",".join(function_ids)}) + else: + kwargs.update({"function_ids": function_ids}) + return self.api_call("admin.functions.permissions.lookup", params=kwargs) + + def admin_functions_permissions_set( + self, + *, + function_id: str, + visibility: str, + user_ids: Optional[Union[str, Sequence[str]]] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Set the visibility of a Slack function + and define the users or workspaces if it is set to named_entities + https://docs.slack.dev/reference/methods/admin.functions.permissions.set + """ + kwargs.update( + { + "function_id": function_id, + "visibility": visibility, + } + ) + if user_ids is not None: + if isinstance(user_ids, (list, tuple)): + kwargs.update({"user_ids": ",".join(user_ids)}) + else: + kwargs.update({"user_ids": user_ids}) + return self.api_call("admin.functions.permissions.set", params=kwargs) + + def admin_roles_addAssignments( + self, + *, + role_id: str, + entity_ids: Union[str, Sequence[str]], + user_ids: Union[str, Sequence[str]], + **kwargs, + ) -> Union[Future, SlackResponse]: + """Adds members to the specified role with the specified scopes + https://docs.slack.dev/reference/methods/admin.roles.addAssignments + """ + kwargs.update({"role_id": role_id}) + if isinstance(entity_ids, (list, tuple)): + kwargs.update({"entity_ids": ",".join(entity_ids)}) + else: + kwargs.update({"entity_ids": entity_ids}) + if isinstance(user_ids, (list, tuple)): + kwargs.update({"user_ids": ",".join(user_ids)}) + else: + kwargs.update({"user_ids": user_ids}) + return self.api_call("admin.roles.addAssignments", params=kwargs) + + def admin_roles_listAssignments( + self, + *, + role_ids: Optional[Union[str, Sequence[str]]] = None, + entity_ids: Optional[Union[str, Sequence[str]]] = None, + cursor: Optional[str] = None, + limit: Optional[Union[str, int]] = None, + sort_dir: Optional[str] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Lists assignments for all roles across entities. + Options to scope results by any combination of roles or entities + https://docs.slack.dev/reference/methods/admin.roles.listAssignments + """ + kwargs.update({"cursor": cursor, "limit": limit, "sort_dir": sort_dir}) + if isinstance(entity_ids, (list, tuple)): + kwargs.update({"entity_ids": ",".join(entity_ids)}) + else: + kwargs.update({"entity_ids": entity_ids}) + if isinstance(role_ids, (list, tuple)): + kwargs.update({"role_ids": ",".join(role_ids)}) + else: + kwargs.update({"role_ids": role_ids}) + return self.api_call("admin.roles.listAssignments", params=kwargs) + + def admin_roles_removeAssignments( + self, + *, + role_id: str, + entity_ids: Union[str, Sequence[str]], + user_ids: Union[str, Sequence[str]], + **kwargs, + ) -> Union[Future, SlackResponse]: + """Removes a set of users from a role for the given scopes and entities + https://docs.slack.dev/reference/methods/admin.roles.removeAssignments + """ + kwargs.update({"role_id": role_id}) + if isinstance(entity_ids, (list, tuple)): + kwargs.update({"entity_ids": ",".join(entity_ids)}) + else: + kwargs.update({"entity_ids": entity_ids}) + if isinstance(user_ids, (list, tuple)): + kwargs.update({"user_ids": ",".join(user_ids)}) + else: + kwargs.update({"user_ids": user_ids}) + return self.api_call("admin.roles.removeAssignments", params=kwargs) + + def admin_users_session_reset( + self, + *, + user_id: str, + mobile_only: Optional[bool] = None, + web_only: Optional[bool] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Wipes all valid sessions on all devices for a given user. + https://docs.slack.dev/reference/methods/admin.users.session.reset + """ + kwargs.update( + { + "user_id": user_id, + "mobile_only": mobile_only, + "web_only": web_only, + } + ) + return self.api_call("admin.users.session.reset", params=kwargs) + + def admin_users_session_resetBulk( + self, + *, + user_ids: Union[str, Sequence[str]], + mobile_only: Optional[bool] = None, + web_only: Optional[bool] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Enqueues an asynchronous job to wipe all valid sessions on all devices for a given list of users + https://docs.slack.dev/reference/methods/admin.users.session.resetBulk + """ + if isinstance(user_ids, (list, tuple)): + kwargs.update({"user_ids": ",".join(user_ids)}) + else: + kwargs.update({"user_ids": user_ids}) + kwargs.update( + { + "mobile_only": mobile_only, + "web_only": web_only, + } + ) + return self.api_call("admin.users.session.resetBulk", params=kwargs) + + def admin_users_session_invalidate( + self, + *, + session_id: str, + team_id: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Invalidate a single session for a user by session_id. + https://docs.slack.dev/reference/methods/admin.users.session.invalidate + """ + kwargs.update({"session_id": session_id, "team_id": team_id}) + return self.api_call("admin.users.session.invalidate", params=kwargs) + + def admin_users_session_list( + self, + *, + cursor: Optional[str] = None, + limit: Optional[int] = None, + team_id: Optional[str] = None, + user_id: Optional[str] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Lists all active user sessions for an organization + https://docs.slack.dev/reference/methods/admin.users.session.list + """ + kwargs.update( + { + "cursor": cursor, + "limit": limit, + "team_id": team_id, + "user_id": user_id, + } + ) + return self.api_call("admin.users.session.list", params=kwargs) + + def admin_teams_settings_setDefaultChannels( + self, + *, + team_id: str, + channel_ids: Union[str, Sequence[str]], + **kwargs, + ) -> Union[Future, SlackResponse]: + """Set the default channels of a workspace. + https://docs.slack.dev/reference/methods/admin.teams.settings.setDefaultChannels + """ + kwargs.update({"team_id": team_id}) + if isinstance(channel_ids, (list, tuple)): + kwargs.update({"channel_ids": ",".join(channel_ids)}) + else: + kwargs.update({"channel_ids": channel_ids}) + return self.api_call("admin.teams.settings.setDefaultChannels", http_verb="GET", params=kwargs) + + def admin_users_session_getSettings( + self, + *, + user_ids: Union[str, Sequence[str]], + **kwargs, + ) -> Union[Future, SlackResponse]: + """Get user-specific session settings—the session duration + and what happens when the client closes—given a list of users. + https://docs.slack.dev/reference/methods/admin.users.session.getSettings + """ + if isinstance(user_ids, (list, tuple)): + kwargs.update({"user_ids": ",".join(user_ids)}) + else: + kwargs.update({"user_ids": user_ids}) + return self.api_call("admin.users.session.getSettings", params=kwargs) + + def admin_users_session_setSettings( + self, + *, + user_ids: Union[str, Sequence[str]], + desktop_app_browser_quit: Optional[bool] = None, + duration: Optional[int] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Configure the user-level session settings—the session duration + and what happens when the client closes—for one or more users. + https://docs.slack.dev/reference/methods/admin.users.session.setSettings + """ + if isinstance(user_ids, (list, tuple)): + kwargs.update({"user_ids": ",".join(user_ids)}) + else: + kwargs.update({"user_ids": user_ids}) + kwargs.update( + { + "desktop_app_browser_quit": desktop_app_browser_quit, + "duration": duration, + } + ) + return self.api_call("admin.users.session.setSettings", params=kwargs) + + def admin_users_session_clearSettings( + self, + *, + user_ids: Union[str, Sequence[str]], + **kwargs, + ) -> Union[Future, SlackResponse]: + """Clear user-specific session settings—the session duration + and what happens when the client closes—for a list of users. + https://docs.slack.dev/reference/methods/admin.users.session.clearSettings + """ + if isinstance(user_ids, (list, tuple)): + kwargs.update({"user_ids": ",".join(user_ids)}) + else: + kwargs.update({"user_ids": user_ids}) + return self.api_call("admin.users.session.clearSettings", params=kwargs) + + def admin_users_unsupportedVersions_export( + self, + *, + date_end_of_support: Optional[Union[str, int]] = None, + date_sessions_started: Optional[Union[str, int]] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Ask Slackbot to send you an export listing all workspace members using unsupported software, + presented as a zipped CSV file. + https://docs.slack.dev/reference/methods/admin.users.unsupportedVersions.export + """ + kwargs.update( + { + "date_end_of_support": date_end_of_support, + "date_sessions_started": date_sessions_started, + } + ) + return self.api_call("admin.users.unsupportedVersions.export", params=kwargs) + + def admin_inviteRequests_approve( + self, + *, + invite_request_id: str, + team_id: Optional[str] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Approve a workspace invite request. + https://docs.slack.dev/reference/methods/admin.inviteRequests.approve + """ + kwargs.update({"invite_request_id": invite_request_id, "team_id": team_id}) + return self.api_call("admin.inviteRequests.approve", params=kwargs) + + def admin_inviteRequests_approved_list( + self, + *, + cursor: Optional[str] = None, + limit: Optional[int] = None, + team_id: Optional[str] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """List all approved workspace invite requests. + https://docs.slack.dev/reference/methods/admin.inviteRequests.approved.list + """ + kwargs.update( + { + "cursor": cursor, + "limit": limit, + "team_id": team_id, + } + ) + return self.api_call("admin.inviteRequests.approved.list", params=kwargs) + + def admin_inviteRequests_denied_list( + self, + *, + cursor: Optional[str] = None, + limit: Optional[int] = None, + team_id: Optional[str] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """List all denied workspace invite requests. + https://docs.slack.dev/reference/methods/admin.inviteRequests.denied.list + """ + kwargs.update( + { + "cursor": cursor, + "limit": limit, + "team_id": team_id, + } + ) + return self.api_call("admin.inviteRequests.denied.list", params=kwargs) + + def admin_inviteRequests_deny( + self, + *, + invite_request_id: str, + team_id: Optional[str] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Deny a workspace invite request. + https://docs.slack.dev/reference/methods/admin.inviteRequests.deny + """ + kwargs.update({"invite_request_id": invite_request_id, "team_id": team_id}) + return self.api_call("admin.inviteRequests.deny", params=kwargs) + + def admin_inviteRequests_list( + self, + **kwargs, + ) -> Union[Future, SlackResponse]: + """List all pending workspace invite requests.""" + return self.api_call("admin.inviteRequests.list", params=kwargs) + + def admin_teams_admins_list( + self, + *, + team_id: str, + cursor: Optional[str] = None, + limit: Optional[int] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """List all of the admins on a given workspace. + https://docs.slack.dev/reference/methods/admin.inviteRequests.list + """ + kwargs.update( + { + "cursor": cursor, + "limit": limit, + "team_id": team_id, + } + ) + return self.api_call("admin.teams.admins.list", http_verb="GET", params=kwargs) + + def admin_teams_create( + self, + *, + team_domain: str, + team_name: str, + team_description: Optional[str] = None, + team_discoverability: Optional[str] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Create an Enterprise team. + https://docs.slack.dev/reference/methods/admin.teams.create + """ + kwargs.update( + { + "team_domain": team_domain, + "team_name": team_name, + "team_description": team_description, + "team_discoverability": team_discoverability, + } + ) + return self.api_call("admin.teams.create", params=kwargs) + + def admin_teams_list( + self, + *, + cursor: Optional[str] = None, + limit: Optional[int] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """List all teams on an Enterprise organization. + https://docs.slack.dev/reference/methods/admin.teams.list + """ + kwargs.update({"cursor": cursor, "limit": limit}) + return self.api_call("admin.teams.list", params=kwargs) + + def admin_teams_owners_list( + self, + *, + team_id: str, + cursor: Optional[str] = None, + limit: Optional[int] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """List all of the admins on a given workspace. + https://docs.slack.dev/reference/methods/admin.teams.owners.list + """ + kwargs.update({"team_id": team_id, "cursor": cursor, "limit": limit}) + return self.api_call("admin.teams.owners.list", http_verb="GET", params=kwargs) + + def admin_teams_settings_info( + self, + *, + team_id: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Fetch information about settings in a workspace + https://docs.slack.dev/reference/methods/admin.teams.settings.info + """ + kwargs.update({"team_id": team_id}) + return self.api_call("admin.teams.settings.info", params=kwargs) + + def admin_teams_settings_setDescription( + self, + *, + team_id: str, + description: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Set the description of a given workspace. + https://docs.slack.dev/reference/methods/admin.teams.settings.setDescription + """ + kwargs.update({"team_id": team_id, "description": description}) + return self.api_call("admin.teams.settings.setDescription", params=kwargs) + + def admin_teams_settings_setDiscoverability( + self, + *, + team_id: str, + discoverability: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Sets the icon of a workspace. + https://docs.slack.dev/reference/methods/admin.teams.settings.setDiscoverability + """ + kwargs.update({"team_id": team_id, "discoverability": discoverability}) + return self.api_call("admin.teams.settings.setDiscoverability", params=kwargs) + + def admin_teams_settings_setIcon( + self, + *, + team_id: str, + image_url: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Sets the icon of a workspace. + https://docs.slack.dev/reference/methods/admin.teams.settings.setIcon + """ + kwargs.update({"team_id": team_id, "image_url": image_url}) + return self.api_call("admin.teams.settings.setIcon", http_verb="GET", params=kwargs) + + def admin_teams_settings_setName( + self, + *, + team_id: str, + name: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Sets the icon of a workspace. + https://docs.slack.dev/reference/methods/admin.teams.settings.setName + """ + kwargs.update({"team_id": team_id, "name": name}) + return self.api_call("admin.teams.settings.setName", params=kwargs) + + def admin_usergroups_addChannels( + self, + *, + channel_ids: Union[str, Sequence[str]], + usergroup_id: str, + team_id: Optional[str] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Add one or more default channels to an IDP group. + https://docs.slack.dev/reference/methods/admin.usergroups.addChannels + """ + kwargs.update({"team_id": team_id, "usergroup_id": usergroup_id}) + if isinstance(channel_ids, (list, tuple)): + kwargs.update({"channel_ids": ",".join(channel_ids)}) + else: + kwargs.update({"channel_ids": channel_ids}) + return self.api_call("admin.usergroups.addChannels", params=kwargs) + + def admin_usergroups_addTeams( + self, + *, + usergroup_id: str, + team_ids: Union[str, Sequence[str]], + auto_provision: Optional[bool] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Associate one or more default workspaces with an organization-wide IDP group. + https://docs.slack.dev/reference/methods/admin.usergroups.addTeams + """ + kwargs.update({"usergroup_id": usergroup_id, "auto_provision": auto_provision}) + if isinstance(team_ids, (list, tuple)): + kwargs.update({"team_ids": ",".join(team_ids)}) + else: + kwargs.update({"team_ids": team_ids}) + return self.api_call("admin.usergroups.addTeams", params=kwargs) + + def admin_usergroups_listChannels( + self, + *, + usergroup_id: str, + include_num_members: Optional[bool] = None, + team_id: Optional[bool] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Add one or more default channels to an IDP group. + https://docs.slack.dev/reference/methods/admin.usergroups.listChannels + """ + kwargs.update( + { + "usergroup_id": usergroup_id, + "include_num_members": include_num_members, + "team_id": team_id, + } + ) + return self.api_call("admin.usergroups.listChannels", params=kwargs) + + def admin_usergroups_removeChannels( + self, + *, + usergroup_id: str, + channel_ids: Union[str, Sequence[str]], + **kwargs, + ) -> Union[Future, SlackResponse]: + """Add one or more default channels to an IDP group. + https://docs.slack.dev/reference/methods/admin.usergroups.removeChannels + """ + kwargs.update({"usergroup_id": usergroup_id}) + if isinstance(channel_ids, (list, tuple)): + kwargs.update({"channel_ids": ",".join(channel_ids)}) + else: + kwargs.update({"channel_ids": channel_ids}) + return self.api_call("admin.usergroups.removeChannels", params=kwargs) + + def admin_users_assign( + self, + *, + team_id: str, + user_id: str, + channel_ids: Optional[Union[str, Sequence[str]]] = None, + is_restricted: Optional[bool] = None, + is_ultra_restricted: Optional[bool] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Add an Enterprise user to a workspace. + https://docs.slack.dev/reference/methods/admin.users.assign + """ + kwargs.update( + { + "team_id": team_id, + "user_id": user_id, + "is_restricted": is_restricted, + "is_ultra_restricted": is_ultra_restricted, + } + ) + if isinstance(channel_ids, (list, tuple)): + kwargs.update({"channel_ids": ",".join(channel_ids)}) + else: + kwargs.update({"channel_ids": channel_ids}) + return self.api_call("admin.users.assign", params=kwargs) + + def admin_users_invite( + self, + *, + team_id: str, + email: str, + channel_ids: Union[str, Sequence[str]], + custom_message: Optional[str] = None, + email_password_policy_enabled: Optional[bool] = None, + guest_expiration_ts: Optional[Union[str, float]] = None, + is_restricted: Optional[bool] = None, + is_ultra_restricted: Optional[bool] = None, + real_name: Optional[str] = None, + resend: Optional[bool] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Invite a user to a workspace. + https://docs.slack.dev/reference/methods/admin.users.invite + """ + kwargs.update( + { + "team_id": team_id, + "email": email, + "custom_message": custom_message, + "email_password_policy_enabled": email_password_policy_enabled, + "guest_expiration_ts": str(guest_expiration_ts) if guest_expiration_ts is not None else None, + "is_restricted": is_restricted, + "is_ultra_restricted": is_ultra_restricted, + "real_name": real_name, + "resend": resend, + } + ) + if isinstance(channel_ids, (list, tuple)): + kwargs.update({"channel_ids": ",".join(channel_ids)}) + else: + kwargs.update({"channel_ids": channel_ids}) + return self.api_call("admin.users.invite", params=kwargs) + + def admin_users_list( + self, + *, + team_id: Optional[str] = None, + include_deactivated_user_workspaces: Optional[bool] = None, + is_active: Optional[bool] = None, + cursor: Optional[str] = None, + limit: Optional[int] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """List users on a workspace + https://docs.slack.dev/reference/methods/admin.users.list + """ + kwargs.update( + { + "team_id": team_id, + "include_deactivated_user_workspaces": include_deactivated_user_workspaces, + "is_active": is_active, + "cursor": cursor, + "limit": limit, + } + ) + return self.api_call("admin.users.list", params=kwargs) + + def admin_users_remove( + self, + *, + team_id: str, + user_id: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Remove a user from a workspace. + https://docs.slack.dev/reference/methods/admin.users.remove + """ + kwargs.update({"team_id": team_id, "user_id": user_id}) + return self.api_call("admin.users.remove", params=kwargs) + + def admin_users_setAdmin( + self, + *, + team_id: str, + user_id: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Set an existing guest, regular user, or owner to be an admin user. + https://docs.slack.dev/reference/methods/admin.users.setAdmin + """ + kwargs.update({"team_id": team_id, "user_id": user_id}) + return self.api_call("admin.users.setAdmin", params=kwargs) + + def admin_users_setExpiration( + self, + *, + expiration_ts: int, + user_id: str, + team_id: Optional[str] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Set an expiration for a guest user. + https://docs.slack.dev/reference/methods/admin.users.setExpiration + """ + kwargs.update({"expiration_ts": expiration_ts, "team_id": team_id, "user_id": user_id}) + return self.api_call("admin.users.setExpiration", params=kwargs) + + def admin_users_setOwner( + self, + *, + team_id: str, + user_id: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Set an existing guest, regular user, or admin user to be a workspace owner. + https://docs.slack.dev/reference/methods/admin.users.setOwner + """ + kwargs.update({"team_id": team_id, "user_id": user_id}) + return self.api_call("admin.users.setOwner", params=kwargs) + + def admin_users_setRegular( + self, + *, + team_id: str, + user_id: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Set an existing guest user, admin user, or owner to be a regular user. + https://docs.slack.dev/reference/methods/admin.users.setRegular + """ + kwargs.update({"team_id": team_id, "user_id": user_id}) + return self.api_call("admin.users.setRegular", params=kwargs) + + def admin_workflows_search( + self, + *, + app_id: Optional[str] = None, + collaborator_ids: Optional[Union[str, Sequence[str]]] = None, + cursor: Optional[str] = None, + limit: Optional[int] = None, + no_collaborators: Optional[bool] = None, + num_trigger_ids: Optional[int] = None, + query: Optional[str] = None, + sort: Optional[str] = None, + sort_dir: Optional[str] = None, + source: Optional[str] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Search workflows within the team or enterprise + https://docs.slack.dev/reference/methods/admin.workflows.search + """ + if collaborator_ids is not None: + if isinstance(collaborator_ids, (list, tuple)): + kwargs.update({"collaborator_ids": ",".join(collaborator_ids)}) + else: + kwargs.update({"collaborator_ids": collaborator_ids}) + kwargs.update( + { + "app_id": app_id, + "cursor": cursor, + "limit": limit, + "no_collaborators": no_collaborators, + "num_trigger_ids": num_trigger_ids, + "query": query, + "sort": sort, + "sort_dir": sort_dir, + "source": source, + } + ) + return self.api_call("admin.workflows.search", params=kwargs) + + def admin_workflows_permissions_lookup( + self, + *, + workflow_ids: Union[str, Sequence[str]], + max_workflow_triggers: Optional[int] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Look up the permissions for a set of workflows + https://docs.slack.dev/reference/methods/admin.workflows.permissions.lookup + """ + if isinstance(workflow_ids, (list, tuple)): + kwargs.update({"workflow_ids": ",".join(workflow_ids)}) + else: + kwargs.update({"workflow_ids": workflow_ids}) + kwargs.update( + { + "max_workflow_triggers": max_workflow_triggers, + } + ) + return self.api_call("admin.workflows.permissions.lookup", params=kwargs) + + def admin_workflows_collaborators_add( + self, + *, + collaborator_ids: Union[str, Sequence[str]], + workflow_ids: Union[str, Sequence[str]], + **kwargs, + ) -> Union[Future, SlackResponse]: + """Add collaborators to workflows within the team or enterprise + https://docs.slack.dev/reference/methods/admin.workflows.collaborators.add + """ + if isinstance(collaborator_ids, (list, tuple)): + kwargs.update({"collaborator_ids": ",".join(collaborator_ids)}) + else: + kwargs.update({"collaborator_ids": collaborator_ids}) + if isinstance(workflow_ids, (list, tuple)): + kwargs.update({"workflow_ids": ",".join(workflow_ids)}) + else: + kwargs.update({"workflow_ids": workflow_ids}) + return self.api_call("admin.workflows.collaborators.add", params=kwargs) + + def admin_workflows_collaborators_remove( + self, + *, + collaborator_ids: Union[str, Sequence[str]], + workflow_ids: Union[str, Sequence[str]], + **kwargs, + ) -> Union[Future, SlackResponse]: + """Remove collaborators from workflows within the team or enterprise + https://docs.slack.dev/reference/methods/admin.workflows.collaborators.remove + """ + if isinstance(collaborator_ids, (list, tuple)): + kwargs.update({"collaborator_ids": ",".join(collaborator_ids)}) + else: + kwargs.update({"collaborator_ids": collaborator_ids}) + if isinstance(workflow_ids, (list, tuple)): + kwargs.update({"workflow_ids": ",".join(workflow_ids)}) + else: + kwargs.update({"workflow_ids": workflow_ids}) + return self.api_call("admin.workflows.collaborators.remove", params=kwargs) + + def admin_workflows_unpublish( + self, + *, + workflow_ids: Union[str, Sequence[str]], + **kwargs, + ) -> Union[Future, SlackResponse]: + """Unpublish workflows within the team or enterprise + https://docs.slack.dev/reference/methods/admin.workflows.unpublish + """ + if isinstance(workflow_ids, (list, tuple)): + kwargs.update({"workflow_ids": ",".join(workflow_ids)}) + else: + kwargs.update({"workflow_ids": workflow_ids}) + return self.api_call("admin.workflows.unpublish", params=kwargs) + + def api_test( + self, + *, + error: Optional[str] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Checks API calling code. + https://docs.slack.dev/reference/methods/api.test + """ + kwargs.update({"error": error}) + return self.api_call("api.test", params=kwargs) + + def apps_connections_open( + self, + *, + app_token: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Generate a temporary Socket Mode WebSocket URL that your app can connect to + in order to receive events and interactive payloads + https://docs.slack.dev/reference/methods/apps.connections.open + """ + kwargs.update({"token": app_token}) + return self.api_call("apps.connections.open", http_verb="POST", params=kwargs) + + def apps_event_authorizations_list( + self, + *, + event_context: str, + cursor: Optional[str] = None, + limit: Optional[int] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Get a list of authorizations for the given event context. + Each authorization represents an app installation that the event is visible to. + https://docs.slack.dev/reference/methods/apps.event.authorizations.list + """ + kwargs.update({"event_context": event_context, "cursor": cursor, "limit": limit}) + return self.api_call("apps.event.authorizations.list", params=kwargs) + + def apps_uninstall( + self, + *, + client_id: str, + client_secret: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Uninstalls your app from a workspace. + https://docs.slack.dev/reference/methods/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://docs.slack.dev/reference/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://docs.slack.dev/reference/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://docs.slack.dev/reference/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://docs.slack.dev/reference/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://docs.slack.dev/reference/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://docs.slack.dev/reference/methods/tooling.tokens.rotate + """ + kwargs.update({"refresh_token": refresh_token}) + return self.api_call("tooling.tokens.rotate", params=kwargs) + + def assistant_threads_setStatus( + self, + *, + channel_id: str, + thread_ts: str, + status: str, + loading_messages: Optional[List[str]] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Set the status for an AI assistant thread. + https://docs.slack.dev/reference/methods/assistant.threads.setStatus + """ + kwargs.update( + {"channel_id": channel_id, "thread_ts": thread_ts, "status": status, "loading_messages": loading_messages} + ) + kwargs = _remove_none_values(kwargs) + return self.api_call("assistant.threads.setStatus", json=kwargs) + + def assistant_threads_setTitle( + self, + *, + channel_id: str, + thread_ts: str, + title: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Set the title for the given assistant thread. + https://docs.slack.dev/reference/methods/assistant.threads.setTitle + """ + kwargs.update({"channel_id": channel_id, "thread_ts": thread_ts, "title": title}) + return self.api_call("assistant.threads.setTitle", params=kwargs) + + def assistant_threads_setSuggestedPrompts( + self, + *, + channel_id: str, + thread_ts: str, + title: Optional[str] = None, + prompts: List[Dict[str, str]], + **kwargs, + ) -> Union[Future, SlackResponse]: + """Set suggested prompts for the given assistant thread. + https://docs.slack.dev/reference/methods/assistant.threads.setSuggestedPrompts + """ + kwargs.update({"channel_id": channel_id, "thread_ts": thread_ts, "prompts": prompts}) + if title is not None: + kwargs.update({"title": title}) + return self.api_call("assistant.threads.setSuggestedPrompts", json=kwargs) + + def auth_revoke( + self, + *, + test: Optional[bool] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Revokes a token. + https://docs.slack.dev/reference/methods/auth.revoke + """ + kwargs.update({"test": test}) + return self.api_call("auth.revoke", http_verb="GET", params=kwargs) + + def auth_test( + self, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Checks authentication & identity. + https://docs.slack.dev/reference/methods/auth.test + """ + return self.api_call("auth.test", params=kwargs) + + def auth_teams_list( + self, + cursor: Optional[str] = None, + limit: Optional[int] = None, + include_icon: Optional[bool] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """List the workspaces a token can access. + https://docs.slack.dev/reference/methods/auth.teams.list + """ + kwargs.update({"cursor": cursor, "limit": limit, "include_icon": include_icon}) + return self.api_call("auth.teams.list", params=kwargs) + + def bookmarks_add( + self, + *, + channel_id: str, + title: str, + type: str, + emoji: Optional[str] = None, + entity_id: Optional[str] = None, + link: Optional[str] = None, # include when type is 'link' + parent_id: Optional[str] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Add bookmark to a channel. + https://docs.slack.dev/reference/methods/bookmarks.add + """ + kwargs.update( + { + "channel_id": channel_id, + "title": title, + "type": type, + "emoji": emoji, + "entity_id": entity_id, + "link": link, + "parent_id": parent_id, + } + ) + return self.api_call("bookmarks.add", http_verb="POST", params=kwargs) + + def bookmarks_edit( + self, + *, + bookmark_id: str, + channel_id: str, + emoji: Optional[str] = None, + link: Optional[str] = None, + title: Optional[str] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Edit bookmark. + https://docs.slack.dev/reference/methods/bookmarks.edit + """ + kwargs.update( + { + "bookmark_id": bookmark_id, + "channel_id": channel_id, + "emoji": emoji, + "link": link, + "title": title, + } + ) + return self.api_call("bookmarks.edit", http_verb="POST", params=kwargs) + + def bookmarks_list( + self, + *, + channel_id: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """List bookmark for the channel. + https://docs.slack.dev/reference/methods/bookmarks.list + """ + kwargs.update({"channel_id": channel_id}) + return self.api_call("bookmarks.list", http_verb="POST", params=kwargs) + + def bookmarks_remove( + self, + *, + bookmark_id: str, + channel_id: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Remove bookmark from the channel. + https://docs.slack.dev/reference/methods/bookmarks.remove + """ + kwargs.update({"bookmark_id": bookmark_id, "channel_id": channel_id}) + return self.api_call("bookmarks.remove", http_verb="POST", params=kwargs) + + def bots_info( + self, + *, + bot: Optional[str] = None, + team_id: Optional[str] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Gets information about a bot user. + https://docs.slack.dev/reference/methods/bots.info + """ + kwargs.update({"bot": bot, "team_id": team_id}) + return self.api_call("bots.info", http_verb="GET", params=kwargs) + + def calls_add( + self, + *, + external_unique_id: str, + join_url: str, + created_by: Optional[str] = None, + date_start: Optional[int] = None, + desktop_app_join_url: Optional[str] = None, + external_display_id: Optional[str] = None, + title: Optional[str] = None, + users: Optional[Union[str, Sequence[Dict[str, str]]]] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Registers a new Call. + https://docs.slack.dev/reference/methods/calls.add + """ + kwargs.update( + { + "external_unique_id": external_unique_id, + "join_url": join_url, + "created_by": created_by, + "date_start": date_start, + "desktop_app_join_url": desktop_app_join_url, + "external_display_id": external_display_id, + "title": title, + } + ) + _update_call_participants( + kwargs, + users if users is not None else kwargs.get("users"), # type: ignore[arg-type] + ) + return self.api_call("calls.add", http_verb="POST", params=kwargs) + + def calls_end( + self, + *, + id: str, + duration: Optional[int] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Ends a Call. + https://docs.slack.dev/reference/methods/calls.end + """ + kwargs.update({"id": id, "duration": duration}) + 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. + https://docs.slack.dev/reference/methods/calls.info + """ + 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, Sequence[Dict[str, str]]], + **kwargs, + ) -> Union[Future, SlackResponse]: + """Registers new participants added to a Call. + https://docs.slack.dev/reference/methods/calls.participants.add + """ + kwargs.update({"id": id}) + _update_call_participants(kwargs, users) + return self.api_call("calls.participants.add", http_verb="POST", params=kwargs) + + def calls_participants_remove( + self, + *, + id: str, + users: Union[str, Sequence[Dict[str, str]]], + **kwargs, + ) -> Union[Future, SlackResponse]: + """Registers participants removed from a Call. + https://docs.slack.dev/reference/methods/calls.participants.remove + """ + kwargs.update({"id": id}) + _update_call_participants(kwargs, users) + return self.api_call("calls.participants.remove", http_verb="POST", params=kwargs) + + def calls_update( + self, + *, + id: str, + desktop_app_join_url: Optional[str] = None, + join_url: Optional[str] = None, + title: Optional[str] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Updates information about a Call. + https://docs.slack.dev/reference/methods/calls.update + """ + kwargs.update( + { + "id": id, + "desktop_app_join_url": desktop_app_join_url, + "join_url": join_url, + "title": title, + } + ) + return self.api_call("calls.update", http_verb="POST", params=kwargs) + + def canvases_create( + self, + *, + title: Optional[str] = None, + document_content: Dict[str, str], + **kwargs, + ) -> Union[Future, SlackResponse]: + """Create Canvas for a user + https://docs.slack.dev/reference/methods/canvases.create + """ + kwargs.update({"title": title, "document_content": document_content}) + return self.api_call("canvases.create", json=kwargs) + + def canvases_edit( + self, + *, + canvas_id: str, + changes: Sequence[Dict[str, Any]], + **kwargs, + ) -> Union[Future, SlackResponse]: + """Update an existing canvas + https://docs.slack.dev/reference/methods/canvases.edit + """ + kwargs.update({"canvas_id": canvas_id, "changes": changes}) + return self.api_call("canvases.edit", json=kwargs) + + def canvases_delete( + self, + *, + canvas_id: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Deletes a canvas + https://docs.slack.dev/reference/methods/canvases.delete + """ + kwargs.update({"canvas_id": canvas_id}) + return self.api_call("canvases.delete", params=kwargs) + + def canvases_access_set( + self, + *, + canvas_id: str, + access_level: str, + channel_ids: Optional[Union[Sequence[str], str]] = None, + user_ids: Optional[Union[Sequence[str], str]] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Sets the access level to a canvas for specified entities + https://docs.slack.dev/reference/methods/canvases.access.set + """ + kwargs.update({"canvas_id": canvas_id, "access_level": access_level}) + if channel_ids is not None: + if isinstance(channel_ids, (list, tuple)): + kwargs.update({"channel_ids": ",".join(channel_ids)}) + else: + kwargs.update({"channel_ids": channel_ids}) + if user_ids is not None: + if isinstance(user_ids, (list, tuple)): + kwargs.update({"user_ids": ",".join(user_ids)}) + else: + kwargs.update({"user_ids": user_ids}) + + return self.api_call("canvases.access.set", params=kwargs) + + def canvases_access_delete( + self, + *, + canvas_id: str, + channel_ids: Optional[Union[Sequence[str], str]] = None, + user_ids: Optional[Union[Sequence[str], str]] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Create a Channel Canvas for a channel + https://docs.slack.dev/reference/methods/canvases.access.delete + """ + kwargs.update({"canvas_id": canvas_id}) + if channel_ids is not None: + if isinstance(channel_ids, (list, tuple)): + kwargs.update({"channel_ids": ",".join(channel_ids)}) + else: + kwargs.update({"channel_ids": channel_ids}) + if user_ids is not None: + if isinstance(user_ids, (list, tuple)): + kwargs.update({"user_ids": ",".join(user_ids)}) + else: + kwargs.update({"user_ids": user_ids}) + return self.api_call("canvases.access.delete", params=kwargs) + + def canvases_sections_lookup( + self, + *, + canvas_id: str, + criteria: Dict[str, Any], + **kwargs, + ) -> Union[Future, SlackResponse]: + """Find sections matching the provided criteria + https://docs.slack.dev/reference/methods/canvases.sections.lookup + """ + kwargs.update({"canvas_id": canvas_id, "criteria": json.dumps(criteria)}) + return self.api_call("canvases.sections.lookup", params=kwargs) + + # -------------------------- + # Deprecated: channels.* + # You can use conversations.* APIs instead. + # https://docs.slack.dev/changelog/2020-01-deprecating-antecedents-to-the-conversations-api/ + # -------------------------- + + def channels_archive( + self, + *, + channel: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Archives a channel.""" + kwargs.update({"channel": channel}) + kwargs = _remove_none_values(kwargs) + return self.api_call("channels.archive", json=kwargs) + + def channels_create( + self, + *, + name: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Creates a channel.""" + kwargs.update({"name": name}) + kwargs = _remove_none_values(kwargs) + return self.api_call("channels.create", json=kwargs) + + def channels_history( + self, + *, + channel: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Fetches history of messages and events from a channel.""" + kwargs.update({"channel": channel}) + return self.api_call("channels.history", http_verb="GET", params=kwargs) + + def channels_info( + self, + *, + channel: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Gets information about a channel.""" + kwargs.update({"channel": channel}) + return self.api_call("channels.info", http_verb="GET", params=kwargs) + + def channels_invite( + self, + *, + channel: str, + user: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Invites a user to a channel.""" + kwargs.update({"channel": channel, "user": user}) + kwargs = _remove_none_values(kwargs) + return self.api_call("channels.invite", json=kwargs) + + def channels_join( + self, + *, + name: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Joins a channel, creating it if needed.""" + kwargs.update({"name": name}) + kwargs = _remove_none_values(kwargs) + return self.api_call("channels.join", json=kwargs) + + def channels_kick( + self, + *, + channel: str, + user: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Removes a user from a channel.""" + kwargs.update({"channel": channel, "user": user}) + kwargs = _remove_none_values(kwargs) + return self.api_call("channels.kick", json=kwargs) + + def channels_leave( + self, + *, + channel: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Leaves a channel.""" + kwargs.update({"channel": channel}) + kwargs = _remove_none_values(kwargs) + return self.api_call("channels.leave", json=kwargs) + + def channels_list( + self, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Lists all channels in a Slack team.""" + return self.api_call("channels.list", http_verb="GET", params=kwargs) + + def channels_mark( + self, + *, + channel: str, + ts: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Sets the read cursor in a channel.""" + kwargs.update({"channel": channel, "ts": ts}) + kwargs = _remove_none_values(kwargs) + return self.api_call("channels.mark", json=kwargs) + + def channels_rename( + self, + *, + channel: str, + name: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Renames a channel.""" + kwargs.update({"channel": channel, "name": name}) + kwargs = _remove_none_values(kwargs) + return self.api_call("channels.rename", json=kwargs) + + def channels_replies( + self, + *, + channel: str, + thread_ts: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Retrieve a thread of messages posted to a channel""" + kwargs.update({"channel": channel, "thread_ts": thread_ts}) + return self.api_call("channels.replies", http_verb="GET", params=kwargs) + + def channels_setPurpose( + self, + *, + channel: str, + purpose: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Sets the purpose for a channel.""" + kwargs.update({"channel": channel, "purpose": purpose}) + kwargs = _remove_none_values(kwargs) + return self.api_call("channels.setPurpose", json=kwargs) + + def channels_setTopic( + self, + *, + channel: str, + topic: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Sets the topic for a channel.""" + kwargs.update({"channel": channel, "topic": topic}) + kwargs = _remove_none_values(kwargs) + return self.api_call("channels.setTopic", json=kwargs) + + def channels_unarchive( + self, + *, + channel: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Unarchives a channel.""" + kwargs.update({"channel": channel}) + kwargs = _remove_none_values(kwargs) + return self.api_call("channels.unarchive", json=kwargs) + + # -------------------------- + + def chat_appendStream( + self, + *, + channel: str, + ts: str, + markdown_text: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Appends text to an existing streaming conversation. + https://docs.slack.dev/reference/methods/chat.appendStream + """ + kwargs.update( + { + "channel": channel, + "ts": ts, + "markdown_text": markdown_text, + } + ) + kwargs = _remove_none_values(kwargs) + return self.api_call("chat.appendStream", json=kwargs) + + def chat_delete( + self, + *, + channel: str, + ts: str, + as_user: Optional[bool] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Deletes a message. + https://docs.slack.dev/reference/methods/chat.delete + """ + kwargs.update({"channel": channel, "ts": ts, "as_user": as_user}) + return self.api_call("chat.delete", params=kwargs) + + def chat_deleteScheduledMessage( + self, + *, + channel: str, + scheduled_message_id: str, + as_user: Optional[bool] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Deletes a scheduled message. + https://docs.slack.dev/reference/methods/chat.deleteScheduledMessage + """ + kwargs.update( + { + "channel": channel, + "scheduled_message_id": scheduled_message_id, + "as_user": as_user, + } + ) + return self.api_call("chat.deleteScheduledMessage", params=kwargs) + + def chat_getPermalink( + self, + *, + channel: str, + message_ts: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Retrieve a permalink URL for a specific extant message + https://docs.slack.dev/reference/methods/chat.getPermalink + """ + kwargs.update({"channel": channel, "message_ts": message_ts}) + return self.api_call("chat.getPermalink", http_verb="GET", params=kwargs) + + def chat_meMessage( + self, + *, + channel: str, + text: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Share a me message into a channel. + https://docs.slack.dev/reference/methods/chat.meMessage + """ + kwargs.update({"channel": channel, "text": text}) + return self.api_call("chat.meMessage", params=kwargs) + + def chat_postEphemeral( + self, + *, + channel: str, + user: str, + text: Optional[str] = None, + as_user: Optional[bool] = None, + attachments: Optional[Union[str, Sequence[Union[Dict, Attachment]]]] = None, + blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None, + thread_ts: Optional[str] = None, + icon_emoji: Optional[str] = None, + icon_url: Optional[str] = None, + link_names: Optional[bool] = None, + username: Optional[str] = None, + parse: Optional[str] = None, + markdown_text: Optional[str] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Sends an ephemeral message to a user in a channel. + https://docs.slack.dev/reference/methods/chat.postEphemeral + """ + kwargs.update( + { + "channel": channel, + "user": user, + "text": text, + "as_user": as_user, + "attachments": attachments, + "blocks": blocks, + "thread_ts": thread_ts, + "icon_emoji": icon_emoji, + "icon_url": icon_url, + "link_names": link_names, + "username": username, + "parse": parse, + "markdown_text": markdown_text, + } + ) + _parse_web_class_objects(kwargs) + kwargs = _remove_none_values(kwargs) + _warn_if_message_text_content_is_missing("chat.postEphemeral", kwargs) + # NOTE: intentionally using json over params for the API methods using blocks/attachments + return self.api_call("chat.postEphemeral", json=kwargs) + + def chat_postMessage( + self, + *, + channel: str, + text: Optional[str] = None, + as_user: Optional[bool] = None, + attachments: Optional[Union[str, Sequence[Union[Dict, Attachment]]]] = None, + blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None, + thread_ts: Optional[str] = None, + reply_broadcast: Optional[bool] = None, + unfurl_links: Optional[bool] = None, + unfurl_media: Optional[bool] = None, + container_id: Optional[str] = None, + icon_emoji: Optional[str] = None, + icon_url: Optional[str] = None, + mrkdwn: Optional[bool] = None, + link_names: Optional[bool] = None, + username: Optional[str] = None, + parse: Optional[str] = None, # none, full + metadata: Optional[Union[Dict, Metadata, EventAndEntityMetadata]] = None, + markdown_text: Optional[str] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Sends a message to a channel. + https://docs.slack.dev/reference/methods/chat.postMessage + """ + kwargs.update( + { + "channel": channel, + "text": text, + "as_user": as_user, + "attachments": attachments, + "blocks": blocks, + "thread_ts": thread_ts, + "reply_broadcast": reply_broadcast, + "unfurl_links": unfurl_links, + "unfurl_media": unfurl_media, + "container_id": container_id, + "icon_emoji": icon_emoji, + "icon_url": icon_url, + "mrkdwn": mrkdwn, + "link_names": link_names, + "username": username, + "parse": parse, + "metadata": metadata, + "markdown_text": markdown_text, + } + ) + _parse_web_class_objects(kwargs) + kwargs = _remove_none_values(kwargs) + _warn_if_message_text_content_is_missing("chat.postMessage", kwargs) + # NOTE: intentionally using json over params for the API methods using blocks/attachments + return self.api_call("chat.postMessage", json=kwargs) + + def chat_scheduleMessage( + self, + *, + channel: str, + post_at: Union[str, int], + text: Optional[str] = None, + as_user: Optional[bool] = None, + attachments: Optional[Union[str, Sequence[Union[Dict, Attachment]]]] = None, + blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None, + thread_ts: Optional[str] = None, + parse: Optional[str] = None, + reply_broadcast: Optional[bool] = None, + unfurl_links: Optional[bool] = None, + unfurl_media: Optional[bool] = None, + link_names: Optional[bool] = None, + metadata: Optional[Union[Dict, Metadata]] = None, + markdown_text: Optional[str] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Schedules a message. + https://docs.slack.dev/reference/methods/chat.scheduleMessage + """ + kwargs.update( + { + "channel": channel, + "post_at": post_at, + "text": text, + "as_user": as_user, + "attachments": attachments, + "blocks": blocks, + "thread_ts": thread_ts, + "reply_broadcast": reply_broadcast, + "parse": parse, + "unfurl_links": unfurl_links, + "unfurl_media": unfurl_media, + "link_names": link_names, + "metadata": metadata, + "markdown_text": markdown_text, + } + ) + _parse_web_class_objects(kwargs) + kwargs = _remove_none_values(kwargs) + _warn_if_message_text_content_is_missing("chat.scheduleMessage", kwargs) + # NOTE: intentionally using json over params for the API methods using blocks/attachments + return self.api_call("chat.scheduleMessage", json=kwargs) + + def chat_scheduledMessages_list( + self, + *, + channel: Optional[str] = None, + cursor: Optional[str] = None, + latest: Optional[str] = None, + limit: Optional[int] = None, + oldest: Optional[str] = None, + team_id: Optional[str] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Lists all scheduled messages. + https://docs.slack.dev/reference/methods/chat.scheduledMessages.list + """ + kwargs.update( + { + "channel": channel, + "cursor": cursor, + "latest": latest, + "limit": limit, + "oldest": oldest, + "team_id": team_id, + } + ) + return self.api_call("chat.scheduledMessages.list", params=kwargs) + + def chat_startStream( + self, + *, + channel: str, + thread_ts: str, + markdown_text: Optional[str] = None, + recipient_team_id: Optional[str] = None, + recipient_user_id: Optional[str] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Starts a new streaming conversation. + https://docs.slack.dev/reference/methods/chat.startStream + """ + kwargs.update( + { + "channel": channel, + "thread_ts": thread_ts, + "markdown_text": markdown_text, + "recipient_team_id": recipient_team_id, + "recipient_user_id": recipient_user_id, + } + ) + kwargs = _remove_none_values(kwargs) + return self.api_call("chat.startStream", json=kwargs) + + def chat_stopStream( + self, + *, + channel: str, + ts: str, + markdown_text: Optional[str] = None, + blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None, + metadata: Optional[Union[Dict, Metadata]] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Stops a streaming conversation. + https://docs.slack.dev/reference/methods/chat.stopStream + """ + kwargs.update( + { + "channel": channel, + "ts": ts, + "markdown_text": markdown_text, + "blocks": blocks, + "metadata": metadata, + } + ) + _parse_web_class_objects(kwargs) + kwargs = _remove_none_values(kwargs) + return self.api_call("chat.stopStream", json=kwargs) + + def chat_unfurl( + self, + *, + channel: Optional[str] = None, + ts: Optional[str] = None, + source: Optional[str] = None, + unfurl_id: Optional[str] = None, + unfurls: Optional[Dict[str, Dict]] = None, # or user_auth_* + metadata: Optional[Union[Dict, EventAndEntityMetadata]] = None, + user_auth_blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None, + user_auth_message: Optional[str] = None, + user_auth_required: Optional[bool] = None, + user_auth_url: Optional[str] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Provide custom unfurl behavior for user-posted URLs. + https://docs.slack.dev/reference/methods/chat.unfurl + """ + kwargs.update( + { + "channel": channel, + "ts": ts, + "source": source, + "unfurl_id": unfurl_id, + "unfurls": unfurls, + "metadata": metadata, + "user_auth_blocks": user_auth_blocks, + "user_auth_message": user_auth_message, + "user_auth_required": user_auth_required, + "user_auth_url": user_auth_url, + } + ) + _parse_web_class_objects(kwargs) # for user_auth_blocks + kwargs = _remove_none_values(kwargs) + # NOTE: intentionally using json over params for API methods using blocks/attachments + return self.api_call("chat.unfurl", json=kwargs) + + def chat_update( + self, + *, + channel: str, + ts: str, + text: Optional[str] = None, + attachments: Optional[Union[str, Sequence[Union[Dict, Attachment]]]] = None, + blocks: Optional[Union[str, Sequence[Union[Dict, Block]]]] = None, + as_user: Optional[bool] = None, + file_ids: Optional[Union[str, Sequence[str]]] = None, + link_names: Optional[bool] = None, + parse: Optional[str] = None, # none, full + reply_broadcast: Optional[bool] = None, + metadata: Optional[Union[Dict, Metadata]] = None, + markdown_text: Optional[str] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Updates a message in a channel. + https://docs.slack.dev/reference/methods/chat.update + """ + kwargs.update( + { + "channel": channel, + "ts": ts, + "text": text, + "attachments": attachments, + "blocks": blocks, + "as_user": as_user, + "link_names": link_names, + "parse": parse, + "reply_broadcast": reply_broadcast, + "metadata": metadata, + "markdown_text": markdown_text, + } + ) + if isinstance(file_ids, (list, tuple)): + kwargs.update({"file_ids": ",".join(file_ids)}) + else: + kwargs.update({"file_ids": file_ids}) + _parse_web_class_objects(kwargs) + kwargs = _remove_none_values(kwargs) + _warn_if_message_text_content_is_missing("chat.update", kwargs) + # NOTE: intentionally using json over params for API methods using blocks/attachments + return self.api_call("chat.update", json=kwargs) + + def conversations_acceptSharedInvite( + self, + *, + channel_name: str, + channel_id: Optional[str] = None, + invite_id: Optional[str] = None, + free_trial_accepted: Optional[bool] = None, + is_private: Optional[bool] = None, + team_id: Optional[str] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Accepts an invitation to a Slack Connect channel. + https://docs.slack.dev/reference/methods/conversations.acceptSharedInvite + """ + if channel_id is None and invite_id is None: + raise e.SlackRequestError("Either channel_id or invite_id must be provided.") + kwargs.update( + { + "channel_name": channel_name, + "channel_id": channel_id, + "invite_id": invite_id, + "free_trial_accepted": free_trial_accepted, + "is_private": is_private, + "team_id": team_id, + } + ) + return self.api_call("conversations.acceptSharedInvite", http_verb="POST", params=kwargs) + + def conversations_approveSharedInvite( + self, + *, + invite_id: str, + target_team: Optional[str] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Approves an invitation to a Slack Connect channel. + https://docs.slack.dev/reference/methods/conversations.approveSharedInvite + """ + kwargs.update({"invite_id": invite_id, "target_team": target_team}) + return self.api_call("conversations.approveSharedInvite", http_verb="POST", params=kwargs) + + def conversations_archive( + self, + *, + channel: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Archives a conversation. + https://docs.slack.dev/reference/methods/conversations.archive + """ + kwargs.update({"channel": channel}) + return self.api_call("conversations.archive", params=kwargs) + + def conversations_close( + self, + *, + channel: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Closes a direct message or multi-person direct message. + https://docs.slack.dev/reference/methods/conversations.close + """ + kwargs.update({"channel": channel}) + return self.api_call("conversations.close", params=kwargs) + + def conversations_create( + self, + *, + name: str, + is_private: Optional[bool] = None, + team_id: Optional[str] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Initiates a public or private channel-based conversation + https://docs.slack.dev/reference/methods/conversations.create + """ + kwargs.update({"name": name, "is_private": is_private, "team_id": team_id}) + return self.api_call("conversations.create", params=kwargs) + + def conversations_declineSharedInvite( + self, + *, + invite_id: str, + target_team: Optional[str] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Declines a Slack Connect channel invite. + https://docs.slack.dev/reference/methods/conversations.declineSharedInvite + """ + kwargs.update({"invite_id": invite_id, "target_team": target_team}) + return self.api_call("conversations.declineSharedInvite", http_verb="GET", params=kwargs) + + def conversations_externalInvitePermissions_set( + self, *, action: str, channel: str, target_team: str, **kwargs + ) -> Union[Future, SlackResponse]: + """Sets a team in a shared External Limited channel to a shared Slack Connect channel or vice versa. + https://docs.slack.dev/reference/methods/conversations.externalInvitePermissions.set + """ + kwargs.update( + { + "action": action, + "channel": channel, + "target_team": target_team, + } + ) + return self.api_call("conversations.externalInvitePermissions.set", params=kwargs) + + def conversations_history( + self, + *, + channel: str, + cursor: Optional[str] = None, + inclusive: Optional[bool] = None, + include_all_metadata: Optional[bool] = None, + latest: Optional[str] = None, + limit: Optional[int] = None, + oldest: Optional[str] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Fetches a conversation's history of messages and events. + https://docs.slack.dev/reference/methods/conversations.history + """ + kwargs.update( + { + "channel": channel, + "cursor": cursor, + "inclusive": inclusive, + "include_all_metadata": include_all_metadata, + "limit": limit, + "latest": latest, + "oldest": oldest, + } + ) + return self.api_call("conversations.history", http_verb="GET", params=kwargs) + + def conversations_info( + self, + *, + channel: str, + include_locale: Optional[bool] = None, + include_num_members: Optional[bool] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Retrieve information about a conversation. + https://docs.slack.dev/reference/methods/conversations.info + """ + kwargs.update( + { + "channel": channel, + "include_locale": include_locale, + "include_num_members": include_num_members, + } + ) + return self.api_call("conversations.info", http_verb="GET", params=kwargs) + + def conversations_invite( + self, + *, + channel: str, + users: Union[str, Sequence[str]], + force: Optional[bool] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Invites users to a channel. + https://docs.slack.dev/reference/methods/conversations.invite + """ + kwargs.update( + { + "channel": channel, + "force": force, + } + ) + if isinstance(users, (list, tuple)): + kwargs.update({"users": ",".join(users)}) + else: + kwargs.update({"users": users}) + return self.api_call("conversations.invite", params=kwargs) + + def conversations_inviteShared( + self, + *, + channel: str, + emails: Optional[Union[str, Sequence[str]]] = None, + user_ids: Optional[Union[str, Sequence[str]]] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Sends an invitation to a Slack Connect channel. + https://docs.slack.dev/reference/methods/conversations.inviteShared + """ + if emails is None and user_ids is None: + raise e.SlackRequestError("Either emails or user ids must be provided.") + kwargs.update({"channel": channel}) + if isinstance(emails, (list, tuple)): + kwargs.update({"emails": ",".join(emails)}) + else: + kwargs.update({"emails": emails}) + if isinstance(user_ids, (list, tuple)): + kwargs.update({"user_ids": ",".join(user_ids)}) + else: + kwargs.update({"user_ids": user_ids}) + return self.api_call("conversations.inviteShared", http_verb="GET", params=kwargs) + + def conversations_join( + self, + *, + channel: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Joins an existing conversation. + https://docs.slack.dev/reference/methods/conversations.join + """ + kwargs.update({"channel": channel}) + return self.api_call("conversations.join", params=kwargs) + + def conversations_kick( + self, + *, + channel: str, + user: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Removes a user from a conversation. + https://docs.slack.dev/reference/methods/conversations.kick + """ + kwargs.update({"channel": channel, "user": user}) + return self.api_call("conversations.kick", params=kwargs) + + def conversations_leave( + self, + *, + channel: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Leaves a conversation. + https://docs.slack.dev/reference/methods/conversations.leave + """ + kwargs.update({"channel": channel}) + return self.api_call("conversations.leave", params=kwargs) + + def conversations_list( + self, + *, + cursor: Optional[str] = None, + exclude_archived: Optional[bool] = None, + limit: Optional[int] = None, + team_id: Optional[str] = None, + types: Optional[Union[str, Sequence[str]]] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Lists all channels in a Slack team. + https://docs.slack.dev/reference/methods/conversations.list + """ + kwargs.update( + { + "cursor": cursor, + "exclude_archived": exclude_archived, + "limit": limit, + "team_id": team_id, + } + ) + if isinstance(types, (list, tuple)): + kwargs.update({"types": ",".join(types)}) + else: + kwargs.update({"types": types}) + return self.api_call("conversations.list", http_verb="GET", params=kwargs) + + def conversations_listConnectInvites( + self, + *, + count: Optional[int] = None, + cursor: Optional[str] = None, + team_id: Optional[str] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """List shared channel invites that have been generated + or received but have not yet been approved by all parties. + https://docs.slack.dev/reference/methods/conversations.listConnectInvites + """ + kwargs.update({"count": count, "cursor": cursor, "team_id": team_id}) + return self.api_call("conversations.listConnectInvites", params=kwargs) + + def conversations_mark( + self, + *, + channel: str, + ts: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Sets the read cursor in a channel. + https://docs.slack.dev/reference/methods/conversations.mark + """ + kwargs.update({"channel": channel, "ts": ts}) + return self.api_call("conversations.mark", params=kwargs) + + def conversations_members( + self, + *, + channel: str, + cursor: Optional[str] = None, + limit: Optional[int] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Retrieve members of a conversation. + https://docs.slack.dev/reference/methods/conversations.members + """ + kwargs.update({"channel": channel, "cursor": cursor, "limit": limit}) + return self.api_call("conversations.members", http_verb="GET", params=kwargs) + + def conversations_open( + self, + *, + channel: Optional[str] = None, + return_im: Optional[bool] = None, + users: Optional[Union[str, Sequence[str]]] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Opens or resumes a direct message or multi-person direct message. + https://docs.slack.dev/reference/methods/conversations.open + """ + if channel is None and users is None: + raise e.SlackRequestError("Either channel or users must be provided.") + kwargs.update({"channel": channel, "return_im": return_im}) + if isinstance(users, (list, tuple)): + kwargs.update({"users": ",".join(users)}) + else: + kwargs.update({"users": users}) + return self.api_call("conversations.open", params=kwargs) + + def conversations_rename( + self, + *, + channel: str, + name: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Renames a conversation. + https://docs.slack.dev/reference/methods/conversations.rename + """ + kwargs.update({"channel": channel, "name": name}) + return self.api_call("conversations.rename", params=kwargs) + + def conversations_replies( + self, + *, + channel: str, + ts: str, + cursor: Optional[str] = None, + inclusive: Optional[bool] = None, + include_all_metadata: Optional[bool] = None, + latest: Optional[str] = None, + limit: Optional[int] = None, + oldest: Optional[str] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Retrieve a thread of messages posted to a conversation + https://docs.slack.dev/reference/methods/conversations.replies + """ + kwargs.update( + { + "channel": channel, + "ts": ts, + "cursor": cursor, + "inclusive": inclusive, + "include_all_metadata": include_all_metadata, + "limit": limit, + "latest": latest, + "oldest": oldest, + } + ) + return self.api_call("conversations.replies", http_verb="GET", params=kwargs) + + def conversations_requestSharedInvite_approve( + self, + *, + invite_id: str, + channel_id: Optional[str] = None, + is_external_limited: Optional[str] = None, + message: Optional[Dict[str, Any]] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Approve a request to add an external user to a channel. This also sends them a Slack Connect invite. + https://docs.slack.dev/reference/methods/conversations.requestSharedInvite.approve + """ + kwargs.update( + { + "invite_id": invite_id, + "channel_id": channel_id, + "is_external_limited": is_external_limited, + } + ) + if message is not None: + kwargs.update({"message": json.dumps(message)}) + return self.api_call("conversations.requestSharedInvite.approve", params=kwargs) + + def conversations_requestSharedInvite_deny( + self, + *, + invite_id: str, + message: Optional[str] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Deny a request to invite an external user to a channel. + https://docs.slack.dev/reference/methods/conversations.requestSharedInvite.deny + """ + kwargs.update({"invite_id": invite_id, "message": message}) + return self.api_call("conversations.requestSharedInvite.deny", params=kwargs) + + def conversations_requestSharedInvite_list( + self, + *, + cursor: Optional[str] = None, + include_approved: Optional[bool] = None, + include_denied: Optional[bool] = None, + include_expired: Optional[bool] = None, + invite_ids: Optional[Union[str, Sequence[str]]] = None, + limit: Optional[int] = None, + user_id: Optional[str] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Lists requests to add external users to channels with ability to filter. + https://docs.slack.dev/reference/methods/conversations.requestSharedInvite.list + """ + kwargs.update( + { + "cursor": cursor, + "include_approved": include_approved, + "include_denied": include_denied, + "include_expired": include_expired, + "limit": limit, + "user_id": user_id, + } + ) + if invite_ids is not None: + if isinstance(invite_ids, (list, tuple)): + kwargs.update({"invite_ids": ",".join(invite_ids)}) + else: + kwargs.update({"invite_ids": invite_ids}) + return self.api_call("conversations.requestSharedInvite.list", params=kwargs) + + def conversations_setPurpose( + self, + *, + channel: str, + purpose: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Sets the purpose for a conversation. + https://docs.slack.dev/reference/methods/conversations.setPurpose + """ + kwargs.update({"channel": channel, "purpose": purpose}) + return self.api_call("conversations.setPurpose", params=kwargs) + + def conversations_setTopic( + self, + *, + channel: str, + topic: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Sets the topic for a conversation. + https://docs.slack.dev/reference/methods/conversations.setTopic + """ + kwargs.update({"channel": channel, "topic": topic}) + return self.api_call("conversations.setTopic", params=kwargs) + + def conversations_unarchive( + self, + *, + channel: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Reverses conversation archival. + https://docs.slack.dev/reference/methods/conversations.unarchive + """ + kwargs.update({"channel": channel}) + return self.api_call("conversations.unarchive", params=kwargs) + + def conversations_canvases_create( + self, + *, + channel_id: str, + document_content: Dict[str, str], + **kwargs, + ) -> Union[Future, SlackResponse]: + """Create a Channel Canvas for a channel + https://docs.slack.dev/reference/methods/conversations.canvases.create + """ + kwargs.update({"channel_id": channel_id, "document_content": document_content}) + return self.api_call("conversations.canvases.create", json=kwargs) + + def dialog_open( + self, + *, + dialog: Dict[str, Any], + trigger_id: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Open a dialog with a user. + https://docs.slack.dev/reference/methods/dialog.open + """ + kwargs.update({"dialog": dialog, "trigger_id": trigger_id}) + kwargs = _remove_none_values(kwargs) + # NOTE: As the dialog can be a dict, this API call works only with json format. + return self.api_call("dialog.open", json=kwargs) + + def dnd_endDnd( + self, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Ends the current user's Do Not Disturb session immediately. + https://docs.slack.dev/reference/methods/dnd.endDnd + """ + return self.api_call("dnd.endDnd", params=kwargs) + + def dnd_endSnooze( + self, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Ends the current user's snooze mode immediately. + https://docs.slack.dev/reference/methods/dnd.endSnooze + """ + return self.api_call("dnd.endSnooze", params=kwargs) + + def dnd_info( + self, + *, + team_id: Optional[str] = None, + user: Optional[str] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Retrieves a user's current Do Not Disturb status. + https://docs.slack.dev/reference/methods/dnd.info + """ + kwargs.update({"team_id": team_id, "user": user}) + return self.api_call("dnd.info", http_verb="GET", params=kwargs) + + def dnd_setSnooze( + self, + *, + num_minutes: Union[int, str], + **kwargs, + ) -> Union[Future, SlackResponse]: + """Turns on Do Not Disturb mode for the current user, or changes its duration. + https://docs.slack.dev/reference/methods/dnd.setSnooze + """ + kwargs.update({"num_minutes": num_minutes}) + return self.api_call("dnd.setSnooze", http_verb="GET", params=kwargs) + + def dnd_teamInfo( + self, + users: Union[str, Sequence[str]], + team_id: Optional[str] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Retrieves the Do Not Disturb status for users on a team. + https://docs.slack.dev/reference/methods/dnd.teamInfo + """ + if isinstance(users, (list, tuple)): + kwargs.update({"users": ",".join(users)}) + else: + kwargs.update({"users": users}) + kwargs.update({"team_id": team_id}) + return self.api_call("dnd.teamInfo", http_verb="GET", params=kwargs) + + def emoji_list( + self, + include_categories: Optional[bool] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Lists custom emoji for a team. + https://docs.slack.dev/reference/methods/emoji.list + """ + kwargs.update({"include_categories": include_categories}) + return self.api_call("emoji.list", http_verb="GET", params=kwargs) + + def entity_presentDetails( + self, + trigger_id: str, + metadata: Optional[Union[Dict, EntityMetadata]] = None, + user_auth_required: Optional[bool] = None, + user_auth_url: Optional[str] = None, + error: Optional[Dict[str, Any]] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Provides entity details for the flexpane. + https://docs.slack.dev/reference/methods/entity.presentDetails/ + """ + kwargs.update({"trigger_id": trigger_id}) + if metadata is not None: + kwargs.update({"metadata": metadata}) + if user_auth_required is not None: + kwargs.update({"user_auth_required": user_auth_required}) + if user_auth_url is not None: + kwargs.update({"user_auth_url": user_auth_url}) + if error is not None: + kwargs.update({"error": error}) + _parse_web_class_objects(kwargs) + return self.api_call("entity.presentDetails", json=kwargs) + + def files_comments_delete( + self, + *, + file: str, + id: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Deletes an existing comment on a file. + https://docs.slack.dev/reference/methods/files.comments.delete + """ + kwargs.update({"file": file, "id": id}) + return self.api_call("files.comments.delete", params=kwargs) + + def files_delete( + self, + *, + file: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Deletes a file. + https://docs.slack.dev/reference/methods/files.delete + """ + kwargs.update({"file": file}) + return self.api_call("files.delete", params=kwargs) + + def files_info( + self, + *, + file: str, + count: Optional[int] = None, + cursor: Optional[str] = None, + limit: Optional[int] = None, + page: Optional[int] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Gets information about a team file. + https://docs.slack.dev/reference/methods/files.info + """ + kwargs.update( + { + "file": file, + "count": count, + "cursor": cursor, + "limit": limit, + "page": page, + } + ) + return self.api_call("files.info", http_verb="GET", params=kwargs) + + def files_list( + self, + *, + channel: Optional[str] = None, + count: Optional[int] = None, + page: Optional[int] = None, + show_files_hidden_by_limit: Optional[bool] = None, + team_id: Optional[str] = None, + ts_from: Optional[str] = None, + ts_to: Optional[str] = None, + types: Optional[Union[str, Sequence[str]]] = None, + user: Optional[str] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Lists & filters team files. + https://docs.slack.dev/reference/methods/files.list + """ + kwargs.update( + { + "channel": channel, + "count": count, + "page": page, + "show_files_hidden_by_limit": show_files_hidden_by_limit, + "team_id": team_id, + "ts_from": ts_from, + "ts_to": ts_to, + "user": user, + } + ) + if isinstance(types, (list, tuple)): + kwargs.update({"types": ",".join(types)}) + else: + kwargs.update({"types": types}) + return self.api_call("files.list", http_verb="GET", params=kwargs) + + def files_remote_info( + self, + *, + external_id: Optional[str] = None, + file: Optional[str] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Retrieve information about a remote file added to Slack. + https://docs.slack.dev/reference/methods/files.remote.info + """ + kwargs.update({"external_id": external_id, "file": file}) + return self.api_call("files.remote.info", http_verb="GET", params=kwargs) + + def files_remote_list( + self, + *, + channel: Optional[str] = None, + cursor: Optional[str] = None, + limit: Optional[int] = None, + ts_from: Optional[str] = None, + ts_to: Optional[str] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Retrieve information about a remote file added to Slack. + https://docs.slack.dev/reference/methods/files.remote.list + """ + kwargs.update( + { + "channel": channel, + "cursor": cursor, + "limit": limit, + "ts_from": ts_from, + "ts_to": ts_to, + } + ) + return self.api_call("files.remote.list", http_verb="GET", params=kwargs) + + def files_remote_add( + self, + *, + external_id: str, + external_url: str, + title: str, + filetype: Optional[str] = None, + indexable_file_contents: Optional[Union[str, bytes, IOBase]] = None, + preview_image: Optional[Union[str, bytes, IOBase]] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Adds a file from a remote service. + https://docs.slack.dev/reference/methods/files.remote.add + """ + kwargs.update( + { + "external_id": external_id, + "external_url": external_url, + "title": title, + "filetype": filetype, + } + ) + files = None + # preview_image (file): Preview of the document via multipart/form-data. + if preview_image is not None or indexable_file_contents is not None: + files = { + "preview_image": preview_image, + "indexable_file_contents": indexable_file_contents, + } + + return self.api_call( + # Intentionally using "POST" method over "GET" here + "files.remote.add", + http_verb="POST", + data=kwargs, + files=files, + ) + + def files_remote_update( + self, + *, + external_id: Optional[str] = None, + external_url: Optional[str] = None, + file: Optional[str] = None, + title: Optional[str] = None, + filetype: Optional[str] = None, + indexable_file_contents: Optional[str] = None, + preview_image: Optional[str] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Updates an existing remote file. + https://docs.slack.dev/reference/methods/files.remote.update + """ + kwargs.update( + { + "external_id": external_id, + "external_url": external_url, + "file": file, + "title": title, + "filetype": filetype, + } + ) + files = None + # preview_image (file): Preview of the document via multipart/form-data. + if preview_image is not None or indexable_file_contents is not None: + files = { + "preview_image": preview_image, + "indexable_file_contents": indexable_file_contents, + } + + return self.api_call( + # Intentionally using "POST" method over "GET" here + "files.remote.update", + http_verb="POST", + data=kwargs, + files=files, + ) + + def files_remote_remove( + self, + *, + external_id: Optional[str] = None, + file: Optional[str] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Remove a remote file. + https://docs.slack.dev/reference/methods/files.remote.remove + """ + kwargs.update({"external_id": external_id, "file": file}) + return self.api_call("files.remote.remove", http_verb="POST", params=kwargs) + + def files_remote_share( + self, + *, + channels: Union[str, Sequence[str]], + external_id: Optional[str] = None, + file: Optional[str] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Share a remote file into a channel. + https://docs.slack.dev/reference/methods/files.remote.share + """ + if external_id is None and file is None: + raise e.SlackRequestError("Either external_id or file must be provided.") + if isinstance(channels, (list, tuple)): + kwargs.update({"channels": ",".join(channels)}) + else: + kwargs.update({"channels": channels}) + kwargs.update({"external_id": external_id, "file": file}) + return self.api_call("files.remote.share", http_verb="GET", params=kwargs) + + def files_revokePublicURL( + self, + *, + file: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Revokes public/external sharing access for a file + https://docs.slack.dev/reference/methods/files.revokePublicURL + """ + kwargs.update({"file": file}) + return self.api_call("files.revokePublicURL", params=kwargs) + + def files_sharedPublicURL( + self, + *, + file: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Enables a file for public/external sharing. + https://docs.slack.dev/reference/methods/files.sharedPublicURL + """ + kwargs.update({"file": file}) + return self.api_call("files.sharedPublicURL", params=kwargs) + + def files_upload( + self, + *, + file: Optional[Union[str, bytes, IOBase]] = None, + content: Optional[Union[str, bytes]] = None, + filename: Optional[str] = None, + filetype: Optional[str] = None, + initial_comment: Optional[str] = None, + thread_ts: Optional[str] = None, + title: Optional[str] = None, + channels: Optional[Union[str, Sequence[str]]] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Uploads or creates a file. + https://docs.slack.dev/reference/methods/files.upload + """ + _print_files_upload_v2_suggestion() + + if file is None and content is None: + raise e.SlackRequestError("The file or content argument must be specified.") + if file is not None and content is not None: + raise e.SlackRequestError("You cannot specify both the file and the content argument.") + + if isinstance(channels, (list, tuple)): + kwargs.update({"channels": ",".join(channels)}) + else: + kwargs.update({"channels": channels}) + kwargs.update( + { + "filename": filename, + "filetype": filetype, + "initial_comment": initial_comment, + "thread_ts": thread_ts, + "title": title, + } + ) + if file: + if kwargs.get("filename") is None and isinstance(file, str): + # use the local filename if filename is missing + if kwargs.get("filename") is None: + kwargs["filename"] = file.split(os.path.sep)[-1] + return self.api_call("files.upload", files={"file": file}, data=kwargs) + else: + kwargs["content"] = content + return self.api_call("files.upload", data=kwargs) + + def files_upload_v2( + self, + *, + # for sending a single file + filename: Optional[str] = None, # you can skip this only when sending along with content parameter + file: Optional[Union[str, bytes, IOBase, os.PathLike]] = None, + content: Optional[Union[str, bytes]] = None, + title: Optional[str] = None, + alt_txt: Optional[str] = None, + snippet_type: Optional[str] = None, + # To upload multiple files at a time + file_uploads: Optional[List[Dict[str, Any]]] = None, + channel: Optional[str] = None, + channels: Optional[List[str]] = None, + initial_comment: Optional[str] = None, + thread_ts: Optional[str] = None, + request_file_info: bool = True, # since v3.23, this flag is no longer necessary + **kwargs, + ) -> Union[Future, SlackResponse]: + """This wrapper method provides an easy way to upload files using the following endpoints: + + - step1: https://docs.slack.dev/reference/methods/files.getUploadURLExternal + + - step2: "https://files.slack.com/upload/v1/..." URLs returned from files.getUploadURLExternal API + + - step3: https://docs.slack.dev/reference/methods/files.completeUploadExternal + and https://docs.slack.dev/reference/methods/files.info + + """ + if file is None and content is None and file_uploads is None: + raise e.SlackRequestError("Any of file, content, and file_uploads must be specified.") + if file is not None and content is not None: + raise e.SlackRequestError("You cannot specify both the file and the content argument.") + + # deprecated arguments: + filetype = kwargs.get("filetype") + + if filetype is not None: + warnings.warn("The filetype parameter is no longer supported. Please remove it from the arguments.") + + # step1: files.getUploadURLExternal per file + files: List[Dict[str, Any]] = [] + if file_uploads is not None: + for f in file_uploads: + files.append(_to_v2_file_upload_item(f)) + else: + f = _to_v2_file_upload_item( + { + "filename": filename, + "file": file, + "content": content, + "title": title, + "alt_txt": alt_txt, + "snippet_type": snippet_type, + } + ) + files.append(f) + + for f in files: + url_response = self.files_getUploadURLExternal( + filename=f.get("filename"), # type: ignore[arg-type] + length=f.get("length"), # type: ignore[arg-type] + alt_txt=f.get("alt_txt"), + snippet_type=f.get("snippet_type"), + token=kwargs.get("token"), + ) + _validate_for_legacy_client(url_response) + f["file_id"] = url_response.get("file_id") # type: ignore[union-attr, unused-ignore] + f["upload_url"] = url_response.get("upload_url") # type: ignore[union-attr, unused-ignore] + + # step2: "https://files.slack.com/upload/v1/..." per file + for f in files: + upload_result = self._upload_file( + url=f["upload_url"], + data=f["data"], + logger=self._logger, + timeout=self.timeout, + proxy=self.proxy, + ssl=self.ssl, + ) + if upload_result.status != 200: + status = upload_result.status + body = upload_result.body + message = ( + "Failed to upload a file " + f"(status: {status}, body: {body}, filename: {f.get('filename')}, title: {f.get('title')})" + ) + raise e.SlackRequestError(message) + + # step3: files.completeUploadExternal with all the sets of (file_id + title) + completion = self.files_completeUploadExternal( + files=[{"id": f["file_id"], "title": f["title"]} for f in files], + channel_id=channel, + channels=channels, + initial_comment=initial_comment, + thread_ts=thread_ts, + **kwargs, + ) + if len(completion.get("files")) == 1: # type: ignore[arg-type, union-attr, unused-ignore] + completion.data["file"] = completion.get("files")[0] # type: ignore[index, union-attr, unused-ignore] + return completion + + def files_getUploadURLExternal( + self, + *, + filename: str, + length: int, + alt_txt: Optional[str] = None, + snippet_type: Optional[str] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Gets a URL for an edge external upload. + https://docs.slack.dev/reference/methods/files.getUploadURLExternal + """ + kwargs.update( + { + "filename": filename, + "length": length, + "alt_txt": alt_txt, + "snippet_type": snippet_type, + } + ) + return self.api_call("files.getUploadURLExternal", params=kwargs) + + def files_completeUploadExternal( + self, + *, + files: List[Dict[str, str]], + channel_id: Optional[str] = None, + channels: Optional[List[str]] = None, + initial_comment: Optional[str] = None, + thread_ts: Optional[str] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Finishes an upload started with files.getUploadURLExternal. + https://docs.slack.dev/reference/methods/files.completeUploadExternal + """ + _files = [{k: v for k, v in f.items() if v is not None} for f in files] + kwargs.update( + { + "files": json.dumps(_files), + "channel_id": channel_id, + "initial_comment": initial_comment, + "thread_ts": thread_ts, + } + ) + if channels: + kwargs["channels"] = ",".join(channels) + return self.api_call("files.completeUploadExternal", params=kwargs) + + def functions_completeSuccess( + self, + *, + function_execution_id: str, + outputs: Dict[str, Any], + **kwargs, + ) -> Union[Future, SlackResponse]: + """Signal the successful completion of a function + https://docs.slack.dev/reference/methods/functions.completeSuccess + """ + kwargs.update({"function_execution_id": function_execution_id, "outputs": json.dumps(outputs)}) + return self.api_call("functions.completeSuccess", params=kwargs) + + def functions_completeError( + self, + *, + function_execution_id: str, + error: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Signal the failure to execute a function + https://docs.slack.dev/reference/methods/functions.completeError + """ + kwargs.update({"function_execution_id": function_execution_id, "error": error}) + return self.api_call("functions.completeError", params=kwargs) + + # -------------------------- + # Deprecated: groups.* + # You can use conversations.* APIs instead. + # https://docs.slack.dev/changelog/2020-01-deprecating-antecedents-to-the-conversations-api/ + # -------------------------- + + def groups_archive( + self, + *, + channel: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Archives a private channel.""" + kwargs.update({"channel": channel}) + kwargs = _remove_none_values(kwargs) + return self.api_call("groups.archive", json=kwargs) + + def groups_create( + self, + *, + name: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Creates a private channel.""" + kwargs.update({"name": name}) + kwargs = _remove_none_values(kwargs) + return self.api_call("groups.create", json=kwargs) + + def groups_createChild( + self, + *, + channel: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Clones and archives a private channel.""" + kwargs.update({"channel": channel}) + return self.api_call("groups.createChild", http_verb="GET", params=kwargs) + + def groups_history( + self, + *, + channel: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Fetches history of messages and events from a private channel.""" + kwargs.update({"channel": channel}) + return self.api_call("groups.history", http_verb="GET", params=kwargs) + + def groups_info( + self, + *, + channel: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Gets information about a private channel.""" + kwargs.update({"channel": channel}) + return self.api_call("groups.info", http_verb="GET", params=kwargs) + + def groups_invite( + self, + *, + channel: str, + user: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Invites a user to a private channel.""" + kwargs.update({"channel": channel, "user": user}) + kwargs = _remove_none_values(kwargs) + return self.api_call("groups.invite", json=kwargs) + + def groups_kick( + self, + *, + channel: str, + user: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Removes a user from a private channel.""" + kwargs.update({"channel": channel, "user": user}) + kwargs = _remove_none_values(kwargs) + return self.api_call("groups.kick", json=kwargs) + + def groups_leave( + self, + *, + channel: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Leaves a private channel.""" + kwargs.update({"channel": channel}) + kwargs = _remove_none_values(kwargs) + return self.api_call("groups.leave", json=kwargs) + + def groups_list( + self, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Lists private channels that the calling user has access to.""" + return self.api_call("groups.list", http_verb="GET", params=kwargs) + + def groups_mark( + self, + *, + channel: str, + ts: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Sets the read cursor in a private channel.""" + kwargs.update({"channel": channel, "ts": ts}) + kwargs = _remove_none_values(kwargs) + return self.api_call("groups.mark", json=kwargs) + + def groups_open( + self, + *, + channel: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Opens a private channel.""" + kwargs.update({"channel": channel}) + kwargs = _remove_none_values(kwargs) + return self.api_call("groups.open", json=kwargs) + + def groups_rename( + self, + *, + channel: str, + name: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Renames a private channel.""" + kwargs.update({"channel": channel, "name": name}) + kwargs = _remove_none_values(kwargs) + return self.api_call("groups.rename", json=kwargs) + + def groups_replies( + self, + *, + channel: str, + thread_ts: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Retrieve a thread of messages posted to a private channel""" + kwargs.update({"channel": channel, "thread_ts": thread_ts}) + return self.api_call("groups.replies", http_verb="GET", params=kwargs) + + def groups_setPurpose( + self, + *, + channel: str, + purpose: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Sets the purpose for a private channel.""" + kwargs.update({"channel": channel, "purpose": purpose}) + kwargs = _remove_none_values(kwargs) + return self.api_call("groups.setPurpose", json=kwargs) + + def groups_setTopic( + self, + *, + channel: str, + topic: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Sets the topic for a private channel.""" + kwargs.update({"channel": channel, "topic": topic}) + kwargs = _remove_none_values(kwargs) + return self.api_call("groups.setTopic", json=kwargs) + + def groups_unarchive( + self, + *, + channel: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Unarchives a private channel.""" + kwargs.update({"channel": channel}) + kwargs = _remove_none_values(kwargs) + return self.api_call("groups.unarchive", json=kwargs) + + # -------------------------- + # Deprecated: im.* + # You can use conversations.* APIs instead. + # https://docs.slack.dev/changelog/2020-01-deprecating-antecedents-to-the-conversations-api/ + # -------------------------- + + def im_close( + self, + *, + channel: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Close a direct message channel.""" + kwargs.update({"channel": channel}) + kwargs = _remove_none_values(kwargs) + return self.api_call("im.close", json=kwargs) + + def im_history( + self, + *, + channel: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Fetches history of messages and events from direct message channel.""" + kwargs.update({"channel": channel}) + return self.api_call("im.history", http_verb="GET", params=kwargs) + + def im_list( + self, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Lists direct message channels for the calling user.""" + return self.api_call("im.list", http_verb="GET", params=kwargs) + + def im_mark( + self, + *, + channel: str, + ts: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Sets the read cursor in a direct message channel.""" + kwargs.update({"channel": channel, "ts": ts}) + kwargs = _remove_none_values(kwargs) + return self.api_call("im.mark", json=kwargs) + + def im_open( + self, + *, + user: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Opens a direct message channel.""" + kwargs.update({"user": user}) + kwargs = _remove_none_values(kwargs) + return self.api_call("im.open", json=kwargs) + + def im_replies( + self, + *, + channel: str, + thread_ts: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Retrieve a thread of messages posted to a direct message conversation""" + kwargs.update({"channel": channel, "thread_ts": thread_ts}) + return self.api_call("im.replies", http_verb="GET", params=kwargs) + + # -------------------------- + + def migration_exchange( + self, + *, + users: Union[str, Sequence[str]], + team_id: Optional[str] = None, + to_old: Optional[bool] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """For Enterprise Grid workspaces, map local user IDs to global user IDs + https://docs.slack.dev/reference/methods/migration.exchange + """ + if isinstance(users, (list, tuple)): + kwargs.update({"users": ",".join(users)}) + else: + kwargs.update({"users": users}) + kwargs.update({"team_id": team_id, "to_old": to_old}) + return self.api_call("migration.exchange", http_verb="GET", params=kwargs) + + # -------------------------- + # Deprecated: mpim.* + # You can use conversations.* APIs instead. + # https://docs.slack.dev/changelog/2020-01-deprecating-antecedents-to-the-conversations-api/ + # -------------------------- + + def mpim_close( + self, + *, + channel: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Closes a multiparty direct message channel.""" + kwargs.update({"channel": channel}) + kwargs = _remove_none_values(kwargs) + return self.api_call("mpim.close", json=kwargs) + + def mpim_history( + self, + *, + channel: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Fetches history of messages and events from a multiparty direct message.""" + kwargs.update({"channel": channel}) + return self.api_call("mpim.history", http_verb="GET", params=kwargs) + + def mpim_list( + self, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Lists multiparty direct message channels for the calling user.""" + return self.api_call("mpim.list", http_verb="GET", params=kwargs) + + def mpim_mark( + self, + *, + channel: str, + ts: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Sets the read cursor in a multiparty direct message channel.""" + kwargs.update({"channel": channel, "ts": ts}) + kwargs = _remove_none_values(kwargs) + return self.api_call("mpim.mark", json=kwargs) + + def mpim_open( + self, + *, + users: Union[str, Sequence[str]], + **kwargs, + ) -> Union[Future, SlackResponse]: + """This method opens a multiparty direct message.""" + if isinstance(users, (list, tuple)): + kwargs.update({"users": ",".join(users)}) + else: + kwargs.update({"users": users}) + return self.api_call("mpim.open", params=kwargs) + + def mpim_replies( + self, + *, + channel: str, + thread_ts: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Retrieve a thread of messages posted to a direct message conversation from a + multiparty direct message. + """ + kwargs.update({"channel": channel, "thread_ts": thread_ts}) + return self.api_call("mpim.replies", http_verb="GET", params=kwargs) + + # -------------------------- + + def oauth_v2_access( + self, + *, + client_id: str, + client_secret: str, + # This field is required when processing the OAuth redirect URL requests + # while it's absent for token rotation + code: Optional[str] = None, + redirect_uri: Optional[str] = None, + # This field is required for token rotation + grant_type: Optional[str] = None, + # This field is required for token rotation + refresh_token: Optional[str] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Exchanges a temporary OAuth verifier code for an access token. + https://docs.slack.dev/reference/methods/oauth.v2.access + """ + if redirect_uri is not None: + kwargs.update({"redirect_uri": redirect_uri}) + if code is not None: + kwargs.update({"code": code}) + if grant_type is not None: + kwargs.update({"grant_type": grant_type}) + if refresh_token is not None: + kwargs.update({"refresh_token": refresh_token}) + return self.api_call( + "oauth.v2.access", + data=kwargs, + auth={"client_id": client_id, "client_secret": client_secret}, + ) + + def oauth_access( + self, + *, + client_id: str, + client_secret: str, + code: str, + redirect_uri: Optional[str] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Exchanges a temporary OAuth verifier code for an access token. + https://docs.slack.dev/reference/methods/oauth.access + """ + if redirect_uri is not None: + kwargs.update({"redirect_uri": redirect_uri}) + kwargs.update({"code": code}) + return self.api_call( + "oauth.access", + data=kwargs, + auth={"client_id": client_id, "client_secret": client_secret}, + ) + + def oauth_v2_exchange( + self, + *, + token: str, + client_id: str, + client_secret: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Exchanges a legacy access token for a new expiring access token and refresh token + https://docs.slack.dev/reference/methods/oauth.v2.exchange + """ + kwargs.update({"client_id": client_id, "client_secret": client_secret, "token": token}) + return self.api_call("oauth.v2.exchange", params=kwargs) + + def openid_connect_token( + self, + client_id: str, + client_secret: str, + code: Optional[str] = None, + redirect_uri: Optional[str] = None, + grant_type: Optional[str] = None, + refresh_token: Optional[str] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Exchanges a temporary OAuth verifier code for an access token for Sign in with Slack. + https://docs.slack.dev/reference/methods/openid.connect.token + """ + if redirect_uri is not None: + kwargs.update({"redirect_uri": redirect_uri}) + if code is not None: + kwargs.update({"code": code}) + if grant_type is not None: + kwargs.update({"grant_type": grant_type}) + if refresh_token is not None: + kwargs.update({"refresh_token": refresh_token}) + return self.api_call( + "openid.connect.token", + data=kwargs, + auth={"client_id": client_id, "client_secret": client_secret}, + ) + + def openid_connect_userInfo( + self, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Get the identity of a user who has authorized Sign in with Slack. + https://docs.slack.dev/reference/methods/openid.connect.userInfo + """ + return self.api_call("openid.connect.userInfo", params=kwargs) + + def pins_add( + self, + *, + channel: str, + timestamp: Optional[str] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Pins an item to a channel. + https://docs.slack.dev/reference/methods/pins.add + """ + kwargs.update({"channel": channel, "timestamp": timestamp}) + return self.api_call("pins.add", params=kwargs) + + def pins_list( + self, + *, + channel: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Lists items pinned to a channel. + https://docs.slack.dev/reference/methods/pins.list + """ + kwargs.update({"channel": channel}) + return self.api_call("pins.list", http_verb="GET", params=kwargs) + + def pins_remove( + self, + *, + channel: str, + timestamp: Optional[str] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Un-pins an item from a channel. + https://docs.slack.dev/reference/methods/pins.remove + """ + kwargs.update({"channel": channel, "timestamp": timestamp}) + return self.api_call("pins.remove", params=kwargs) + + def reactions_add( + self, + *, + channel: str, + name: str, + timestamp: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Adds a reaction to an item. + https://docs.slack.dev/reference/methods/reactions.add + """ + kwargs.update({"channel": channel, "name": name, "timestamp": timestamp}) + return self.api_call("reactions.add", params=kwargs) + + def reactions_get( + self, + *, + channel: Optional[str] = None, + file: Optional[str] = None, + file_comment: Optional[str] = None, + full: Optional[bool] = None, + timestamp: Optional[str] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Gets reactions for an item. + https://docs.slack.dev/reference/methods/reactions.get + """ + kwargs.update( + { + "channel": channel, + "file": file, + "file_comment": file_comment, + "full": full, + "timestamp": timestamp, + } + ) + return self.api_call("reactions.get", http_verb="GET", params=kwargs) + + def reactions_list( + self, + *, + count: Optional[int] = None, + cursor: Optional[str] = None, + full: Optional[bool] = None, + limit: Optional[int] = None, + page: Optional[int] = None, + team_id: Optional[str] = None, + user: Optional[str] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Lists reactions made by a user. + https://docs.slack.dev/reference/methods/reactions.list + """ + kwargs.update( + { + "count": count, + "cursor": cursor, + "full": full, + "limit": limit, + "page": page, + "team_id": team_id, + "user": user, + } + ) + return self.api_call("reactions.list", http_verb="GET", params=kwargs) + + def reactions_remove( + self, + *, + name: str, + channel: Optional[str] = None, + file: Optional[str] = None, + file_comment: Optional[str] = None, + timestamp: Optional[str] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Removes a reaction from an item. + https://docs.slack.dev/reference/methods/reactions.remove + """ + kwargs.update( + { + "name": name, + "channel": channel, + "file": file, + "file_comment": file_comment, + "timestamp": timestamp, + } + ) + return self.api_call("reactions.remove", params=kwargs) + + def reminders_add( + self, + *, + text: str, + time: str, + team_id: Optional[str] = None, + user: Optional[str] = None, + recurrence: Optional[str] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Creates a reminder. + https://docs.slack.dev/reference/methods/reminders.add + """ + kwargs.update( + { + "text": text, + "time": time, + "team_id": team_id, + "user": user, + "recurrence": recurrence, + } + ) + return self.api_call("reminders.add", params=kwargs) + + def reminders_complete( + self, + *, + reminder: str, + team_id: Optional[str] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Marks a reminder as complete. + https://docs.slack.dev/reference/methods/reminders.complete + """ + kwargs.update({"reminder": reminder, "team_id": team_id}) + return self.api_call("reminders.complete", params=kwargs) + + def reminders_delete( + self, + *, + reminder: str, + team_id: Optional[str] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Deletes a reminder. + https://docs.slack.dev/reference/methods/reminders.delete + """ + kwargs.update({"reminder": reminder, "team_id": team_id}) + return self.api_call("reminders.delete", params=kwargs) + + def reminders_info( + self, + *, + reminder: str, + team_id: Optional[str] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Gets information about a reminder. + https://docs.slack.dev/reference/methods/reminders.info + """ + kwargs.update({"reminder": reminder, "team_id": team_id}) + return self.api_call("reminders.info", http_verb="GET", params=kwargs) + + def reminders_list( + self, + *, + team_id: Optional[str] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Lists all reminders created by or for a given user. + https://docs.slack.dev/reference/methods/reminders.list + """ + kwargs.update({"team_id": team_id}) + return self.api_call("reminders.list", http_verb="GET", params=kwargs) + + def rtm_connect( + self, + *, + batch_presence_aware: Optional[bool] = None, + presence_sub: Optional[bool] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Starts a Real Time Messaging session. + https://docs.slack.dev/reference/methods/rtm.connect + """ + kwargs.update({"batch_presence_aware": batch_presence_aware, "presence_sub": presence_sub}) + return self.api_call("rtm.connect", http_verb="GET", params=kwargs) + + def rtm_start( + self, + *, + batch_presence_aware: Optional[bool] = None, + include_locale: Optional[bool] = None, + mpim_aware: Optional[bool] = None, + no_latest: Optional[bool] = None, + no_unreads: Optional[bool] = None, + presence_sub: Optional[bool] = None, + simple_latest: Optional[bool] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Starts a Real Time Messaging session. + https://docs.slack.dev/reference/methods/rtm.start + """ + kwargs.update( + { + "batch_presence_aware": batch_presence_aware, + "include_locale": include_locale, + "mpim_aware": mpim_aware, + "no_latest": no_latest, + "no_unreads": no_unreads, + "presence_sub": presence_sub, + "simple_latest": simple_latest, + } + ) + return self.api_call("rtm.start", http_verb="GET", params=kwargs) + + def search_all( + self, + *, + query: str, + count: Optional[int] = None, + highlight: Optional[bool] = None, + page: Optional[int] = None, + sort: Optional[str] = None, + sort_dir: Optional[str] = None, + team_id: Optional[str] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Searches for messages and files matching a query. + https://docs.slack.dev/reference/methods/search.all + """ + kwargs.update( + { + "query": query, + "count": count, + "highlight": highlight, + "page": page, + "sort": sort, + "sort_dir": sort_dir, + "team_id": team_id, + } + ) + return self.api_call("search.all", http_verb="GET", params=kwargs) + + def search_files( + self, + *, + query: str, + count: Optional[int] = None, + highlight: Optional[bool] = None, + page: Optional[int] = None, + sort: Optional[str] = None, + sort_dir: Optional[str] = None, + team_id: Optional[str] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Searches for files matching a query. + https://docs.slack.dev/reference/methods/search.files + """ + kwargs.update( + { + "query": query, + "count": count, + "highlight": highlight, + "page": page, + "sort": sort, + "sort_dir": sort_dir, + "team_id": team_id, + } + ) + return self.api_call("search.files", http_verb="GET", params=kwargs) + + def search_messages( + self, + *, + query: str, + count: Optional[int] = None, + cursor: Optional[str] = None, + highlight: Optional[bool] = None, + page: Optional[int] = None, + sort: Optional[str] = None, + sort_dir: Optional[str] = None, + team_id: Optional[str] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Searches for messages matching a query. + https://docs.slack.dev/reference/methods/search.messages + """ + kwargs.update( + { + "query": query, + "count": count, + "cursor": cursor, + "highlight": highlight, + "page": page, + "sort": sort, + "sort_dir": sort_dir, + "team_id": team_id, + } + ) + return self.api_call("search.messages", http_verb="GET", params=kwargs) + + def slackLists_access_delete( + self, + *, + list_id: str, + channel_ids: Optional[List[str]] = None, + user_ids: Optional[List[str]] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Revoke access to a List for specified entities. + https://docs.slack.dev/reference/methods/slackLists.access.delete + """ + kwargs.update({"list_id": list_id, "channel_ids": channel_ids, "user_ids": user_ids}) + kwargs = _remove_none_values(kwargs) + return self.api_call("slackLists.access.delete", json=kwargs) + + def slackLists_access_set( + self, + *, + list_id: str, + access_level: str, + channel_ids: Optional[List[str]] = None, + user_ids: Optional[List[str]] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Set the access level to a List for specified entities. + https://docs.slack.dev/reference/methods/slackLists.access.set + """ + kwargs.update({"list_id": list_id, "access_level": access_level, "channel_ids": channel_ids, "user_ids": user_ids}) + kwargs = _remove_none_values(kwargs) + return self.api_call("slackLists.access.set", json=kwargs) + + def slackLists_create( + self, + *, + name: str, + description_blocks: Optional[Union[str, Sequence[Union[Dict, RichTextBlock]]]] = None, + schema: Optional[List[Dict[str, Any]]] = None, + copy_from_list_id: Optional[str] = None, + include_copied_list_records: Optional[bool] = None, + todo_mode: Optional[bool] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Creates a List. + https://docs.slack.dev/reference/methods/slackLists.create + """ + kwargs.update( + { + "name": name, + "description_blocks": description_blocks, + "schema": schema, + "copy_from_list_id": copy_from_list_id, + "include_copied_list_records": include_copied_list_records, + "todo_mode": todo_mode, + } + ) + kwargs = _remove_none_values(kwargs) + return self.api_call("slackLists.create", json=kwargs) + + def slackLists_download_get( + self, + *, + list_id: str, + job_id: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Retrieve List download URL from an export job to download List contents. + https://docs.slack.dev/reference/methods/slackLists.download.get + """ + kwargs.update( + { + "list_id": list_id, + "job_id": job_id, + } + ) + kwargs = _remove_none_values(kwargs) + return self.api_call("slackLists.download.get", json=kwargs) + + def slackLists_download_start( + self, + *, + list_id: str, + include_archived: Optional[bool] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Initiate a job to export List contents. + https://docs.slack.dev/reference/methods/slackLists.download.start + """ + kwargs.update( + { + "list_id": list_id, + "include_archived": include_archived, + } + ) + kwargs = _remove_none_values(kwargs) + return self.api_call("slackLists.download.start", json=kwargs) + + def slackLists_items_create( + self, + *, + list_id: str, + duplicated_item_id: Optional[str] = None, + parent_item_id: Optional[str] = None, + initial_fields: Optional[List[Dict[str, Any]]] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Add a new item to an existing List. + https://docs.slack.dev/reference/methods/slackLists.items.create + """ + kwargs.update( + { + "list_id": list_id, + "duplicated_item_id": duplicated_item_id, + "parent_item_id": parent_item_id, + "initial_fields": initial_fields, + } + ) + kwargs = _remove_none_values(kwargs) + return self.api_call("slackLists.items.create", json=kwargs) + + def slackLists_items_delete( + self, + *, + list_id: str, + id: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Deletes an item from an existing List. + https://docs.slack.dev/reference/methods/slackLists.items.delete + """ + kwargs.update( + { + "list_id": list_id, + "id": id, + } + ) + kwargs = _remove_none_values(kwargs) + return self.api_call("slackLists.items.delete", json=kwargs) + + def slackLists_items_deleteMultiple( + self, + *, + list_id: str, + ids: List[str], + **kwargs, + ) -> Union[Future, SlackResponse]: + """Deletes multiple items from an existing List. + https://docs.slack.dev/reference/methods/slackLists.items.deleteMultiple + """ + kwargs.update( + { + "list_id": list_id, + "ids": ids, + } + ) + kwargs = _remove_none_values(kwargs) + return self.api_call("slackLists.items.deleteMultiple", json=kwargs) + + def slackLists_items_info( + self, + *, + list_id: str, + id: str, + include_is_subscribed: Optional[bool] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Get a row from a List. + https://docs.slack.dev/reference/methods/slackLists.items.info + """ + kwargs.update( + { + "list_id": list_id, + "id": id, + "include_is_subscribed": include_is_subscribed, + } + ) + kwargs = _remove_none_values(kwargs) + return self.api_call("slackLists.items.info", json=kwargs) + + def slackLists_items_list( + self, + *, + list_id: str, + limit: Optional[int] = None, + cursor: Optional[str] = None, + archived: Optional[bool] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Get records from a List. + https://docs.slack.dev/reference/methods/slackLists.items.list + """ + kwargs.update( + { + "list_id": list_id, + "limit": limit, + "cursor": cursor, + "archived": archived, + } + ) + kwargs = _remove_none_values(kwargs) + return self.api_call("slackLists.items.list", json=kwargs) + + def slackLists_items_update( + self, + *, + list_id: str, + cells: List[Dict[str, Any]], + **kwargs, + ) -> Union[Future, SlackResponse]: + """Updates cells in a List. + https://docs.slack.dev/reference/methods/slackLists.items.update + """ + kwargs.update( + { + "list_id": list_id, + "cells": cells, + } + ) + kwargs = _remove_none_values(kwargs) + return self.api_call("slackLists.items.update", json=kwargs) + + def slackLists_update( + self, + *, + id: str, + name: Optional[str] = None, + description_blocks: Optional[Union[str, Sequence[Union[Dict, RichTextBlock]]]] = None, + todo_mode: Optional[bool] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Update a List. + https://docs.slack.dev/reference/methods/slackLists.update + """ + kwargs.update( + { + "id": id, + "name": name, + "description_blocks": description_blocks, + "todo_mode": todo_mode, + } + ) + kwargs = _remove_none_values(kwargs) + return self.api_call("slackLists.update", json=kwargs) + + def stars_add( + self, + *, + channel: Optional[str] = None, + file: Optional[str] = None, + file_comment: Optional[str] = None, + timestamp: Optional[str] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Adds a star to an item. + https://docs.slack.dev/reference/methods/stars.add + """ + kwargs.update( + { + "channel": channel, + "file": file, + "file_comment": file_comment, + "timestamp": timestamp, + } + ) + return self.api_call("stars.add", params=kwargs) + + def stars_list( + self, + *, + count: Optional[int] = None, + cursor: Optional[str] = None, + limit: Optional[int] = None, + page: Optional[int] = None, + team_id: Optional[str] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Lists stars for a user. + https://docs.slack.dev/reference/methods/stars.list + """ + kwargs.update( + { + "count": count, + "cursor": cursor, + "limit": limit, + "page": page, + "team_id": team_id, + } + ) + return self.api_call("stars.list", http_verb="GET", params=kwargs) + + def stars_remove( + self, + *, + channel: Optional[str] = None, + file: Optional[str] = None, + file_comment: Optional[str] = None, + timestamp: Optional[str] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Removes a star from an item. + https://docs.slack.dev/reference/methods/stars.remove + """ + kwargs.update( + { + "channel": channel, + "file": file, + "file_comment": file_comment, + "timestamp": timestamp, + } + ) + return self.api_call("stars.remove", params=kwargs) + + def team_accessLogs( + self, + *, + before: Optional[Union[int, str]] = None, + count: Optional[Union[int, str]] = None, + page: Optional[Union[int, str]] = None, + team_id: Optional[str] = None, + cursor: Optional[str] = None, + limit: Optional[int] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Gets the access logs for the current team. + https://docs.slack.dev/reference/methods/team.accessLogs + """ + kwargs.update( + { + "before": before, + "count": count, + "page": page, + "team_id": team_id, + "cursor": cursor, + "limit": limit, + } + ) + return self.api_call("team.accessLogs", http_verb="GET", params=kwargs) + + def team_billableInfo( + self, + *, + team_id: Optional[str] = None, + user: Optional[str] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Gets billable users information for the current team. + https://docs.slack.dev/reference/methods/team.billableInfo + """ + kwargs.update({"team_id": team_id, "user": user}) + return self.api_call("team.billableInfo", http_verb="GET", params=kwargs) + + def team_billing_info( + self, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Reads a workspace's billing plan information. + https://docs.slack.dev/reference/methods/team.billing.info + """ + return self.api_call("team.billing.info", params=kwargs) + + def team_externalTeams_disconnect( + self, + *, + target_team: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Disconnects an external organization. + https://docs.slack.dev/reference/methods/team.externalTeams.disconnect + """ + kwargs.update( + { + "target_team": target_team, + } + ) + return self.api_call("team.externalTeams.disconnect", params=kwargs) + + def team_externalTeams_list( + self, + *, + connection_status_filter: Optional[str] = None, + slack_connect_pref_filter: Optional[Sequence[str]] = None, + sort_direction: Optional[str] = None, + sort_field: Optional[str] = None, + workspace_filter: Optional[Sequence[str]] = None, + cursor: Optional[str] = None, + limit: Optional[int] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Returns a list of all the external teams connected and details about the connection. + https://docs.slack.dev/reference/methods/team.externalTeams.list + """ + kwargs.update( + { + "connection_status_filter": connection_status_filter, + "sort_direction": sort_direction, + "sort_field": sort_field, + "cursor": cursor, + "limit": limit, + } + ) + if slack_connect_pref_filter is not None: + if isinstance(slack_connect_pref_filter, (list, tuple)): + kwargs.update({"slack_connect_pref_filter": ",".join(slack_connect_pref_filter)}) + else: + kwargs.update({"slack_connect_pref_filter": slack_connect_pref_filter}) + if workspace_filter is not None: + if isinstance(workspace_filter, (list, tuple)): + kwargs.update({"workspace_filter": ",".join(workspace_filter)}) + else: + kwargs.update({"workspace_filter": workspace_filter}) + return self.api_call("team.externalTeams.list", http_verb="GET", params=kwargs) + + def team_info( + self, + *, + team: Optional[str] = None, + domain: Optional[str] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Gets information about the current team. + https://docs.slack.dev/reference/methods/team.info + """ + kwargs.update({"team": team, "domain": domain}) + return self.api_call("team.info", http_verb="GET", params=kwargs) + + def team_integrationLogs( + self, + *, + app_id: Optional[str] = None, + change_type: Optional[str] = None, + count: Optional[Union[int, str]] = None, + page: Optional[Union[int, str]] = None, + service_id: Optional[str] = None, + team_id: Optional[str] = None, + user: Optional[str] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Gets the integration logs for the current team. + https://docs.slack.dev/reference/methods/team.integrationLogs + """ + kwargs.update( + { + "app_id": app_id, + "change_type": change_type, + "count": count, + "page": page, + "service_id": service_id, + "team_id": team_id, + "user": user, + } + ) + return self.api_call("team.integrationLogs", http_verb="GET", params=kwargs) + + def team_profile_get( + self, + *, + visibility: Optional[str] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Retrieve a team's profile. + https://docs.slack.dev/reference/methods/team.profile.get + """ + kwargs.update({"visibility": visibility}) + return self.api_call("team.profile.get", http_verb="GET", params=kwargs) + + def team_preferences_list( + self, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Retrieve a list of a workspace's team preferences. + https://docs.slack.dev/reference/methods/team.preferences.list + """ + return self.api_call("team.preferences.list", params=kwargs) + + def usergroups_create( + self, + *, + name: str, + channels: Optional[Union[str, Sequence[str]]] = None, + description: Optional[str] = None, + handle: Optional[str] = None, + include_count: Optional[bool] = None, + team_id: Optional[str] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Create a User Group + https://docs.slack.dev/reference/methods/usergroups.create + """ + kwargs.update( + { + "name": name, + "description": description, + "handle": handle, + "include_count": include_count, + "team_id": team_id, + } + ) + if isinstance(channels, (list, tuple)): + kwargs.update({"channels": ",".join(channels)}) + else: + kwargs.update({"channels": channels}) + return self.api_call("usergroups.create", params=kwargs) + + def usergroups_disable( + self, + *, + usergroup: str, + include_count: Optional[bool] = None, + team_id: Optional[str] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Disable an existing User Group + https://docs.slack.dev/reference/methods/usergroups.disable + """ + kwargs.update({"usergroup": usergroup, "include_count": include_count, "team_id": team_id}) + return self.api_call("usergroups.disable", params=kwargs) + + def usergroups_enable( + self, + *, + usergroup: str, + include_count: Optional[bool] = None, + team_id: Optional[str] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Enable a User Group + https://docs.slack.dev/reference/methods/usergroups.enable + """ + kwargs.update({"usergroup": usergroup, "include_count": include_count, "team_id": team_id}) + return self.api_call("usergroups.enable", params=kwargs) + + def usergroups_list( + self, + *, + include_count: Optional[bool] = None, + include_disabled: Optional[bool] = None, + include_users: Optional[bool] = None, + team_id: Optional[str] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """List all User Groups for a team + https://docs.slack.dev/reference/methods/usergroups.list + """ + kwargs.update( + { + "include_count": include_count, + "include_disabled": include_disabled, + "include_users": include_users, + "team_id": team_id, + } + ) + return self.api_call("usergroups.list", http_verb="GET", params=kwargs) + + def usergroups_update( + self, + *, + usergroup: str, + channels: Optional[Union[str, Sequence[str]]] = None, + description: Optional[str] = None, + handle: Optional[str] = None, + include_count: Optional[bool] = None, + name: Optional[str] = None, + team_id: Optional[str] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Update an existing User Group + https://docs.slack.dev/reference/methods/usergroups.update + """ + kwargs.update( + { + "usergroup": usergroup, + "description": description, + "handle": handle, + "include_count": include_count, + "name": name, + "team_id": team_id, + } + ) + if isinstance(channels, (list, tuple)): + kwargs.update({"channels": ",".join(channels)}) + else: + kwargs.update({"channels": channels}) + return self.api_call("usergroups.update", params=kwargs) + + def usergroups_users_list( + self, + *, + usergroup: str, + include_disabled: Optional[bool] = None, + team_id: Optional[str] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """List all users in a User Group + https://docs.slack.dev/reference/methods/usergroups.users.list + """ + kwargs.update( + { + "usergroup": usergroup, + "include_disabled": include_disabled, + "team_id": team_id, + } + ) + return self.api_call("usergroups.users.list", http_verb="GET", params=kwargs) + + def usergroups_users_update( + self, + *, + usergroup: str, + users: Union[str, Sequence[str]], + include_count: Optional[bool] = None, + team_id: Optional[str] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Update the list of users for a User Group + https://docs.slack.dev/reference/methods/usergroups.users.update + """ + kwargs.update( + { + "usergroup": usergroup, + "include_count": include_count, + "team_id": team_id, + } + ) + if isinstance(users, (list, tuple)): + kwargs.update({"users": ",".join(users)}) + else: + kwargs.update({"users": users}) + return self.api_call("usergroups.users.update", params=kwargs) + + def users_conversations( + self, + *, + cursor: Optional[str] = None, + exclude_archived: Optional[bool] = None, + limit: Optional[int] = None, + team_id: Optional[str] = None, + types: Optional[Union[str, Sequence[str]]] = None, + user: Optional[str] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """List conversations the calling user may access. + https://docs.slack.dev/reference/methods/users.conversations + """ + kwargs.update( + { + "cursor": cursor, + "exclude_archived": exclude_archived, + "limit": limit, + "team_id": team_id, + "user": user, + } + ) + if isinstance(types, (list, tuple)): + kwargs.update({"types": ",".join(types)}) + else: + kwargs.update({"types": types}) + return self.api_call("users.conversations", http_verb="GET", params=kwargs) + + def users_deletePhoto( + self, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Delete the user profile photo + https://docs.slack.dev/reference/methods/users.deletePhoto + """ + return self.api_call("users.deletePhoto", http_verb="GET", params=kwargs) + + def users_getPresence( + self, + *, + user: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Gets user presence information. + https://docs.slack.dev/reference/methods/users.getPresence + """ + kwargs.update({"user": user}) + return self.api_call("users.getPresence", http_verb="GET", params=kwargs) + + def users_identity( + self, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Get a user's identity. + https://docs.slack.dev/reference/methods/users.identity + """ + return self.api_call("users.identity", http_verb="GET", params=kwargs) + + def users_info( + self, + *, + user: str, + include_locale: Optional[bool] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Gets information about a user. + https://docs.slack.dev/reference/methods/users.info + """ + kwargs.update({"user": user, "include_locale": include_locale}) + return self.api_call("users.info", http_verb="GET", params=kwargs) + + def users_list( + self, + *, + cursor: Optional[str] = None, + include_locale: Optional[bool] = None, + limit: Optional[int] = None, + team_id: Optional[str] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Lists all users in a Slack team. + https://docs.slack.dev/reference/methods/users.list + """ + kwargs.update( + { + "cursor": cursor, + "include_locale": include_locale, + "limit": limit, + "team_id": team_id, + } + ) + return self.api_call("users.list", http_verb="GET", params=kwargs) + + def users_lookupByEmail( + self, + *, + email: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Find a user with an email address. + https://docs.slack.dev/reference/methods/users.lookupByEmail + """ + kwargs.update({"email": email}) + return self.api_call("users.lookupByEmail", http_verb="GET", params=kwargs) + + def users_setPhoto( + self, + *, + image: Union[str, IOBase], + crop_w: Optional[Union[int, str]] = None, + crop_x: Optional[Union[int, str]] = None, + crop_y: Optional[Union[int, str]] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Set the user profile photo + https://docs.slack.dev/reference/methods/users.setPhoto + """ + kwargs.update({"crop_w": crop_w, "crop_x": crop_x, "crop_y": crop_y}) + return self.api_call("users.setPhoto", files={"image": image}, data=kwargs) + + def users_setPresence( + self, + *, + presence: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Manually sets user presence. + https://docs.slack.dev/reference/methods/users.setPresence + """ + kwargs.update({"presence": presence}) + return self.api_call("users.setPresence", params=kwargs) + + def users_discoverableContacts_lookup( + self, + email: str, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Lookup an email address to see if someone is on Slack + https://docs.slack.dev/reference/methods/users.discoverableContacts.lookup + """ + kwargs.update({"email": email}) + return self.api_call("users.discoverableContacts.lookup", params=kwargs) + + def users_profile_get( + self, + *, + user: Optional[str] = None, + include_labels: Optional[bool] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Retrieves a user's profile information. + https://docs.slack.dev/reference/methods/users.profile.get + """ + kwargs.update({"user": user, "include_labels": include_labels}) + return self.api_call("users.profile.get", http_verb="GET", params=kwargs) + + def users_profile_set( + self, + *, + name: Optional[str] = None, + value: Optional[str] = None, + user: Optional[str] = None, + profile: Optional[Dict] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Set the profile information for a user. + https://docs.slack.dev/reference/methods/users.profile.set + """ + kwargs.update( + { + "name": name, + "profile": profile, + "user": user, + "value": value, + } + ) + kwargs = _remove_none_values(kwargs) + # NOTE: Intentionally using json for the "profile" parameter + return self.api_call("users.profile.set", json=kwargs) + + def views_open( + self, + *, + trigger_id: Optional[str] = None, + interactivity_pointer: Optional[str] = None, + view: Union[dict, View], + **kwargs, + ) -> Union[Future, SlackResponse]: + """Open a view for a user. + https://docs.slack.dev/reference/methods/views.open + See https://docs.slack.dev/surfaces/modals/ for details. + """ + kwargs.update({"trigger_id": trigger_id, "interactivity_pointer": interactivity_pointer}) + if isinstance(view, View): + kwargs.update({"view": view.to_dict()}) + else: + kwargs.update({"view": view}) + kwargs = _remove_none_values(kwargs) + # NOTE: Intentionally using json for the "view" parameter + return self.api_call("views.open", json=kwargs) + + def views_push( + self, + *, + trigger_id: Optional[str] = None, + interactivity_pointer: Optional[str] = None, + view: Union[dict, View], + **kwargs, + ) -> Union[Future, SlackResponse]: + """Push a view onto the stack of a root view. + Push a new view onto the existing view stack by passing a view + payload and a valid trigger_id generated from an interaction + within the existing modal. + Read the modals documentation (https://docs.slack.dev/surfaces/modals/) + to learn more about the lifecycle and intricacies of views. + https://docs.slack.dev/reference/methods/views.push + """ + kwargs.update({"trigger_id": trigger_id, "interactivity_pointer": interactivity_pointer}) + if isinstance(view, View): + kwargs.update({"view": view.to_dict()}) + else: + kwargs.update({"view": view}) + kwargs = _remove_none_values(kwargs) + # NOTE: Intentionally using json for the "view" parameter + return self.api_call("views.push", json=kwargs) + + def views_update( + self, + *, + view: Union[dict, View], + external_id: Optional[str] = None, + view_id: Optional[str] = None, + hash: Optional[str] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Update an existing view. + Update a view by passing a new view definition along with the + view_id returned in views.open or the external_id. + See the modals documentation (https://docs.slack.dev/surfaces/modals/#updating_views) + to learn more about updating views and avoiding race conditions with the hash argument. + https://docs.slack.dev/reference/methods/views.update + """ + if isinstance(view, View): + kwargs.update({"view": view.to_dict()}) + else: + kwargs.update({"view": view}) + if external_id: + kwargs.update({"external_id": external_id}) + elif view_id: + kwargs.update({"view_id": view_id}) + else: + raise e.SlackRequestError("Either view_id or external_id is required.") + kwargs.update({"hash": hash}) + kwargs = _remove_none_values(kwargs) + # NOTE: Intentionally using json for the "view" parameter + return self.api_call("views.update", json=kwargs) + + def views_publish( + self, + *, + user_id: str, + view: Union[dict, View], + hash: Optional[str] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Publish a static view for a User. + Create or update the view that comprises an + app's Home tab (https://docs.slack.dev/surfaces/app-home/) + https://docs.slack.dev/reference/methods/views.publish + """ + kwargs.update({"user_id": user_id, "hash": hash}) + if isinstance(view, View): + kwargs.update({"view": view.to_dict()}) + else: + kwargs.update({"view": view}) + kwargs = _remove_none_values(kwargs) + # NOTE: Intentionally using json for the "view" parameter + return self.api_call("views.publish", json=kwargs) + + def workflows_featured_add( + self, + *, + channel_id: str, + trigger_ids: Union[str, Sequence[str]], + **kwargs, + ) -> Union[Future, SlackResponse]: + """Add featured workflows to a channel. + https://docs.slack.dev/reference/methods/workflows.featured.add + """ + kwargs.update({"channel_id": channel_id}) + if isinstance(trigger_ids, (list, tuple)): + kwargs.update({"trigger_ids": ",".join(trigger_ids)}) + else: + kwargs.update({"trigger_ids": trigger_ids}) + return self.api_call("workflows.featured.add", params=kwargs) + + def workflows_featured_list( + self, + *, + channel_ids: Union[str, Sequence[str]], + **kwargs, + ) -> Union[Future, SlackResponse]: + """List the featured workflows for specified channels. + https://docs.slack.dev/reference/methods/workflows.featured.list + """ + if isinstance(channel_ids, (list, tuple)): + kwargs.update({"channel_ids": ",".join(channel_ids)}) + else: + kwargs.update({"channel_ids": channel_ids}) + return self.api_call("workflows.featured.list", params=kwargs) + + def workflows_featured_remove( + self, + *, + channel_id: str, + trigger_ids: Union[str, Sequence[str]], + **kwargs, + ) -> Union[Future, SlackResponse]: + """Remove featured workflows from a channel. + https://docs.slack.dev/reference/methods/workflows.featured.remove + """ + kwargs.update({"channel_id": channel_id}) + if isinstance(trigger_ids, (list, tuple)): + kwargs.update({"trigger_ids": ",".join(trigger_ids)}) + else: + kwargs.update({"trigger_ids": trigger_ids}) + return self.api_call("workflows.featured.remove", params=kwargs) + + def workflows_featured_set( + self, + *, + channel_id: str, + trigger_ids: Union[str, Sequence[str]], + **kwargs, + ) -> Union[Future, SlackResponse]: + """Set featured workflows for a channel. + https://docs.slack.dev/reference/methods/workflows.featured.set + """ + kwargs.update({"channel_id": channel_id}) + if isinstance(trigger_ids, (list, tuple)): + kwargs.update({"trigger_ids": ",".join(trigger_ids)}) + else: + kwargs.update({"trigger_ids": trigger_ids}) + return self.api_call("workflows.featured.set", params=kwargs) + + def workflows_stepCompleted( + self, + *, + workflow_step_execute_id: str, + outputs: Optional[dict] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Indicate a successful outcome of a workflow step's execution. + https://docs.slack.dev/reference/methods/workflows.stepCompleted + """ + kwargs.update({"workflow_step_execute_id": workflow_step_execute_id}) + if outputs is not None: + kwargs.update({"outputs": outputs}) + kwargs = _remove_none_values(kwargs) + # NOTE: Intentionally using json for the "outputs" parameter + return self.api_call("workflows.stepCompleted", json=kwargs) + + def workflows_stepFailed( + self, + *, + workflow_step_execute_id: str, + error: Dict[str, str], + **kwargs, + ) -> Union[Future, SlackResponse]: + """Indicate an unsuccessful outcome of a workflow step's execution. + https://docs.slack.dev/reference/methods/workflows.stepFailed + """ + kwargs.update( + { + "workflow_step_execute_id": workflow_step_execute_id, + "error": error, + } + ) + kwargs = _remove_none_values(kwargs) + # NOTE: Intentionally using json for the "error" parameter + return self.api_call("workflows.stepFailed", json=kwargs) + + def workflows_updateStep( + self, + *, + workflow_step_edit_id: str, + inputs: Optional[Dict[str, Any]] = None, + outputs: Optional[List[Dict[str, str]]] = None, + **kwargs, + ) -> Union[Future, SlackResponse]: + """Update the configuration for a workflow extension step. + https://docs.slack.dev/reference/methods/workflows.updateStep + """ + kwargs.update({"workflow_step_edit_id": workflow_step_edit_id}) + if inputs is not None: + kwargs.update({"inputs": inputs}) + if outputs is not None: + kwargs.update({"outputs": outputs}) + kwargs = _remove_none_values(kwargs) + # NOTE: Intentionally using json for the "inputs" / "outputs" parameters + return self.api_call("workflows.updateStep", json=kwargs) diff --git a/slack_sdk/web/legacy_slack_response.py b/slack_sdk/web/legacy_slack_response.py new file mode 100644 index 000000000..d665a5050 --- /dev/null +++ b/slack_sdk/web/legacy_slack_response.py @@ -0,0 +1,221 @@ +"""A Python module for interacting and consuming responses from Slack.""" + +import asyncio + +# Standard Imports +import logging + +# Internal Imports +from typing import Union + +import slack_sdk.errors as e + + +class LegacySlackResponse(object): + """An iterable container of response data. + + Attributes: + data (dict): The json-encoded content of the response. Along + with the headers and status code information. + + Methods: + validate: Check if the response from Slack was successful. + get: Retrieves any key from the response data. + next: Retrieves the next portion of results, + if 'next_cursor' is present. + + Example: + ```python + import os + import slack + + client = slack.WebClient(token=os.environ['SLACK_API_TOKEN']) + + response1 = client.auth_revoke(test='true') + assert not response1['revoked'] + + response2 = client.auth_test() + assert response2.get('ok', False) + + users = [] + for page in client.users_list(limit=2): + TODO: This example should specify when to break. + users = users + page['members'] + ``` + + Note: + Some responses return collections of information + like channel and user lists. If they do it's likely + that you'll only receive a portion of results. This + object allows you to iterate over the response which + makes subsequent API requests until your code hits + 'break' or there are no more results to be found. + + Any attributes or methods prefixed with _underscores are + intended to be "private" internal use only. They may be changed or + removed at anytime. + """ + + def __init__( + self, + *, + client, + http_verb: str, + api_url: str, + req_args: dict, + data: Union[dict, bytes], # data can be binary data + headers: dict, + status_code: int, + use_sync_aiohttp: bool = True, # True for backward-compatibility + ): + self.http_verb = http_verb + self.api_url = api_url + self.req_args = req_args + self.data = data + self.headers = headers + self.status_code = status_code + self._initial_data = data + self._client = client # LegacyWebClient + self._use_sync_aiohttp = use_sync_aiohttp + self._logger = logging.getLogger(__name__) + + def __str__(self): + """Return the Response data if object is converted to a string.""" + if isinstance(self.data, bytes): + raise ValueError("As the response.data is binary data, this operation is unsupported") + return f"{self.data}" + + def __getitem__(self, key): + """Retrieves any key from the data store. + + Note: + This is implemented so users can reference the + SlackResponse object like a dictionary. + e.g. response["ok"] + + Returns: + The value from data or None. + """ + if isinstance(self.data, bytes): + raise ValueError("As the response.data is binary data, this operation is unsupported") + return self.data.get(key, None) + + def __iter__(self): + """Enables the ability to iterate over the response. + It's required for the iterator protocol. + + Note: + This enables Slack cursor-based pagination. + + Returns: + (SlackResponse) self + """ + if isinstance(self.data, bytes): + raise ValueError("As the response.data is binary data, this operation is unsupported") + self._iteration = 0 + self.data = self._initial_data + return self + + def __next__(self): + """Retrieves the next portion of results, if 'next_cursor' is present. + + Note: + Some responses return collections of information + like channel and user lists. If they do it's likely + that you'll only receive a portion of results. This + method allows you to iterate over the response until + your code hits 'break' or there are no more results + to be found. + + Returns: + (SlackResponse) self + With the new response data now attached to this object. + + Raises: + SlackApiError: If the request to the Slack API failed. + StopIteration: If 'next_cursor' is not present or empty. + """ + if isinstance(self.data, bytes): + raise ValueError("As the response.data is binary data, this operation is unsupported") + self._iteration += 1 + if self._iteration == 1: + return self + if self._next_cursor_is_present(self.data): + params = self.req_args.get("params", {}) + if params is None: + params = {} + params.update({"cursor": self.data["response_metadata"]["next_cursor"]}) + self.req_args.update({"params": params}) + + if self._use_sync_aiohttp: + # We no longer recommend going with this way + response = asyncio.get_event_loop().run_until_complete( + self._client._request( + http_verb=self.http_verb, + api_url=self.api_url, + req_args=self.req_args, + ) + ) + else: + # This method sends a request in a synchronous way + response = self._client._request_for_pagination(api_url=self.api_url, req_args=self.req_args) + + self.data = response["data"] + self.headers = response["headers"] + self.status_code = response["status_code"] + return self.validate() + else: + raise StopIteration + + def get(self, key, default=None): + """Retrieves any key from the response data. + + Note: + This is implemented so users can reference the + SlackResponse object like a dictionary. + e.g. response.get("ok", False) + + Returns: + The value from data or the specified default. + """ + if isinstance(self.data, bytes): + raise ValueError("As the response.data is binary data, this operation is unsupported") + return self.data.get(key, default) + + def validate(self): + """Check if the response from Slack was successful. + + Returns: + (SlackResponse) + This method returns it's own object. e.g. 'self' + + Raises: + SlackApiError: The request to the Slack API failed. + """ + if self._logger.level <= logging.DEBUG: + body = self.data if isinstance(self.data, dict) else "(binary)" + self._logger.debug( + "Received the following response - " + f"status: {self.status_code}, " + f"headers: {dict(self.headers)}, " + f"body: {body}" + ) + if self.status_code == 200 and self.data and (isinstance(self.data, bytes) or self.data.get("ok", False)): + return self + msg = "The request to the Slack API failed." + raise e.SlackApiError(message=msg, response=self) + + @staticmethod + def _next_cursor_is_present(data): + """Determine if the response contains 'next_cursor' + and 'next_cursor' is not empty. + + Returns: + A boolean value. + """ + present = ( + "response_metadata" in data + and "next_cursor" in data["response_metadata"] + and data["response_metadata"]["next_cursor"] != "" + ) + return present diff --git a/slack_sdk/web/slack_response.py b/slack_sdk/web/slack_response.py new file mode 100644 index 000000000..37fb266f5 --- /dev/null +++ b/slack_sdk/web/slack_response.py @@ -0,0 +1,197 @@ +"""A Python module for interacting and consuming responses from Slack.""" + +import logging +from typing import Any, Optional, TypeVar, Union, overload + +import slack_sdk.errors as e +from .internal_utils import _next_cursor_is_present + +T = TypeVar("T") + + +class SlackResponse: + """An iterable container of response data. + + Attributes: + data (dict): The json-encoded content of the response. Along + with the headers and status code information. + + Methods: + validate: Check if the response from Slack was successful. + get: Retrieves any key from the response data. + next: Retrieves the next portion of results, + if 'next_cursor' is present. + + Example: + ```python + import os + import slack + + client = slack.WebClient(token=os.environ['SLACK_API_TOKEN']) + + response1 = client.auth_revoke(test='true') + assert not response1['revoked'] + + response2 = client.auth_test() + assert response2.get('ok', False) + + users = [] + for page in client.users_list(limit=2): + users = users + page['members'] + ``` + + Note: + Some responses return collections of information + like channel and user lists. If they do it's likely + that you'll only receive a portion of results. This + object allows you to iterate over the response which + makes subsequent API requests until your code hits + 'break' or there are no more results to be found. + + Any attributes or methods prefixed with _underscores are + intended to be "private" internal use only. They may be changed or + removed at anytime. + """ + + def __init__( + self, + *, + client, + http_verb: str, + api_url: str, + req_args: dict, + data: Union[dict, bytes], # data can be binary data + headers: dict, + status_code: int, + ): + self.http_verb = http_verb + self.api_url = api_url + self.req_args = req_args + self.data = data + self.headers = headers + self.status_code = status_code + self._initial_data = data + self._iteration = None # for __iter__ & __next__ + self._client = client + self._logger = logging.getLogger(__name__) + + def __str__(self): + """Return the Response data if object is converted to a string.""" + if isinstance(self.data, bytes): + raise ValueError("As the response.data is binary data, this operation is unsupported") + return f"{self.data}" + + def __contains__(self, key: str) -> bool: + return self.get(key) is not None + + def __getitem__(self, key): + """Retrieves any key from the data store. + + Note: + This is implemented so users can reference the + SlackResponse object like a dictionary. + e.g. response["ok"] + + Returns: + The value from data or None. + """ + if isinstance(self.data, bytes): + raise ValueError("As the response.data is binary data, this operation is unsupported") + if self.data is None: + raise ValueError("As the response.data is empty, this operation is unsupported") + return self.data.get(key, None) + + def __iter__(self): + """Enables the ability to iterate over the response. + It's required for the iterator protocol. + + Note: + This enables Slack cursor-based pagination. + + Returns: + (SlackResponse) self + """ + self._iteration = 0 + self.data = self._initial_data + return self + + def __next__(self): + """Retrieves the next portion of results, if 'next_cursor' is present. + + Note: + Some responses return collections of information + like channel and user lists. If they do it's likely + that you'll only receive a portion of results. This + method allows you to iterate over the response until + your code hits 'break' or there are no more results + to be found. + + Returns: + (SlackResponse) self + With the new response data now attached to this object. + + Raises: + SlackApiError: If the request to the Slack API failed. + StopIteration: If 'next_cursor' is not present or empty. + """ + if isinstance(self.data, bytes): + raise ValueError("As the response.data is binary data, this operation is unsupported") + self._iteration += 1 + if self._iteration == 1: + return self + if _next_cursor_is_present(self.data): + params = self.req_args.get("params", {}) + if params is None: + params = {} + next_cursor = self.data.get("response_metadata", {}).get("next_cursor") or self.data.get("next_cursor") + params.update({"cursor": next_cursor}) + self.req_args.update({"params": params}) + + # This method sends a request in a synchronous way + response = self._client._request_for_pagination(api_url=self.api_url, req_args=self.req_args) + self.data = response["data"] + self.headers = response["headers"] + self.status_code = response["status_code"] + return self.validate() + else: + raise StopIteration + + @overload + def get(self, key: str, default: None = None) -> Optional[Any]: + ... + + @overload + def get(self, key: str, default: T) -> T: + ... + + def get(self, key, default=None): + """Retrieves any key from the response data. + + Note: + This is implemented so users can reference the + SlackResponse object like a dictionary. + e.g. response.get("ok", False) + + Returns: + The value from data or the specified default. + """ + if isinstance(self.data, bytes): + raise ValueError("As the response.data is binary data, this operation is unsupported") + if self.data is None: + return None + return self.data.get(key, default) + + def validate(self): + """Check if the response from Slack was successful. + + Returns: + (SlackResponse) + This method returns it's own object. e.g. 'self' + + Raises: + SlackApiError: The request to the Slack API failed. + """ + if self.status_code == 200 and self.data and (isinstance(self.data, bytes) or self.data.get("ok", False)): + return self + msg = f"The request to the Slack API failed. (url: {self.api_url})" + raise e.SlackApiError(message=msg, response=self) diff --git a/slack_sdk/webhook/__init__.py b/slack_sdk/webhook/__init__.py new file mode 100644 index 000000000..09e2292ab --- /dev/null +++ b/slack_sdk/webhook/__init__.py @@ -0,0 +1,12 @@ +"""You can use slack_sdk.webhook.WebhookClient for Incoming Webhooks +and message responses using response_url in payloads. +""" + +# from .async_client import AsyncWebhookClient +from .client import WebhookClient +from .webhook_response import WebhookResponse + +__all__ = [ + "WebhookClient", + "WebhookResponse", +] diff --git a/slack_sdk/webhook/async_client.py b/slack_sdk/webhook/async_client.py new file mode 100644 index 000000000..dded0491f --- /dev/null +++ b/slack_sdk/webhook/async_client.py @@ -0,0 +1,271 @@ +import json +import logging +from ssl import SSLContext +from typing import Dict, Union, Optional, Any, Sequence, List + +import aiohttp +from aiohttp import BasicAuth, ClientSession + +from slack_sdk.models.attachments import Attachment +from slack_sdk.models.blocks import Block +from .internal_utils import ( + _debug_log_response, + _build_request_headers, + _build_body, + get_user_agent, +) +from .webhook_response import WebhookResponse +from ..proxy_env_variable_loader import load_http_proxy_from_env + +from slack_sdk.http_retry.async_handler import AsyncRetryHandler +from slack_sdk.http_retry.builtin_async_handlers import async_default_handlers +from slack_sdk.http_retry.request import HttpRequest as RetryHttpRequest +from slack_sdk.http_retry.response import HttpResponse as RetryHttpResponse +from slack_sdk.http_retry.state import RetryState + + +class AsyncWebhookClient: + url: str + timeout: int + ssl: Optional[SSLContext] + proxy: Optional[str] + session: Optional[ClientSession] + trust_env_in_session: bool + auth: Optional[BasicAuth] + default_headers: Dict[str, str] + logger: logging.Logger + retry_handlers: List[AsyncRetryHandler] + + def __init__( + self, + url: str, + timeout: int = 30, + ssl: Optional[SSLContext] = None, + proxy: Optional[str] = None, + session: Optional[ClientSession] = None, + trust_env_in_session: bool = False, + auth: Optional[BasicAuth] = None, + default_headers: Optional[Dict[str, str]] = None, + user_agent_prefix: Optional[str] = None, + user_agent_suffix: Optional[str] = None, + logger: Optional[logging.Logger] = None, + retry_handlers: Optional[List[AsyncRetryHandler]] = None, + ): + """API client for Incoming Webhooks and `response_url` + + https://docs.slack.dev/messaging/sending-messages-using-incoming-webhooks/ + + Args: + url: Complete URL to send data (e.g., `https://hooks.slack.com/XXX`) + timeout: Request timeout (in seconds) + ssl: `ssl.SSLContext` to use for requests + proxy: Proxy URL (e.g., `localhost:9000`, `http://localhost:9000`) + session: `aiohttp.ClientSession` instance + trust_env_in_session: True/False for `aiohttp.ClientSession` + auth: Basic auth info for `aiohttp.ClientSession` + default_headers: Request headers to add to all requests + user_agent_prefix: Prefix for User-Agent header value + user_agent_suffix: Suffix for User-Agent header value + logger: Custom logger + """ + self.url = url + self.timeout = timeout + self.ssl = ssl + self.proxy = proxy + self.trust_env_in_session = trust_env_in_session + self.session = session + self.auth = auth + self.default_headers = default_headers if default_headers else {} + self.default_headers["User-Agent"] = get_user_agent(user_agent_prefix, user_agent_suffix) + self.logger = logger if logger is not None else logging.getLogger(__name__) + self.retry_handlers = retry_handlers if retry_handlers is not None else async_default_handlers() + + if self.proxy is None or len(self.proxy.strip()) == 0: + env_variable = load_http_proxy_from_env(self.logger) + if env_variable is not None: + self.proxy = env_variable + + async def send( + self, + *, + text: Optional[str] = None, + attachments: Optional[Sequence[Union[Dict[str, Any], Attachment]]] = None, + blocks: Optional[Sequence[Union[Dict[str, Any], Block]]] = None, + response_type: Optional[str] = None, + replace_original: Optional[bool] = None, + delete_original: Optional[bool] = None, + unfurl_links: Optional[bool] = None, + unfurl_media: Optional[bool] = None, + metadata: Optional[Dict[str, Any]] = None, + headers: Optional[Dict[str, str]] = None, + ) -> WebhookResponse: + """Performs a Slack API request and returns the result. + + Args: + text: The text message (even when having blocks, setting this as well is recommended as it works as fallback) + attachments: A collection of attachments + blocks: A collection of Block Kit UI components + response_type: The type of message (either 'in_channel' or 'ephemeral') + replace_original: True if you use this option for response_url requests + delete_original: True if you use this option for response_url requests + unfurl_links: Option to indicate whether text url should unfurl + unfurl_media: Option to indicate whether media url should unfurl + metadata: Metadata attached to the message + headers: Request headers to append only for this request + + Returns: + Webhook response + """ + return await self.send_dict( + # It's fine to have None value elements here + # because _build_body() filters them out when constructing the actual body data + body={ + "text": text, + "attachments": attachments, + "blocks": blocks, + "response_type": response_type, + "replace_original": replace_original, + "delete_original": delete_original, + "unfurl_links": unfurl_links, + "unfurl_media": unfurl_media, + "metadata": metadata, + }, + headers=headers, + ) + + async def send_dict(self, body: Dict[str, Any], headers: Optional[Dict[str, str]] = None) -> WebhookResponse: + """Performs a Slack API request and returns the result. + + Args: + body: JSON data structure (it's still a dict at this point), + if you give this argument, body_params and files will be skipped + headers: Request headers to append only for this request + Returns: + Webhook response + """ + return await self._perform_http_request( + body=_build_body(body), # type: ignore[arg-type] + headers=_build_request_headers(self.default_headers, headers), + ) + + async def _perform_http_request(self, *, body: Dict[str, Any], headers: Dict[str, str]) -> WebhookResponse: + str_body: str = json.dumps(body) + headers["Content-Type"] = "application/json;charset=utf-8" + + session: Optional[ClientSession] = None + use_running_session = self.session and not self.session.closed + if use_running_session: + session = self.session + else: + session = aiohttp.ClientSession( + timeout=aiohttp.ClientTimeout(total=self.timeout), + auth=self.auth, + trust_env=self.trust_env_in_session, + ) + + last_error: Optional[Exception] = None + resp: Optional[WebhookResponse] = None + try: + request_kwargs = { + "headers": headers, + "data": str_body, + "ssl": self.ssl, + "proxy": self.proxy, + } + retry_request = RetryHttpRequest( + method="POST", + url=self.url, + headers=headers, # type: ignore[arg-type] + body_params=body, + ) + + retry_state = RetryState() + counter_for_safety = 0 + while counter_for_safety < 100: + counter_for_safety += 1 + # If this is a retry, the next try started here. We can reset the flag. + retry_state.next_attempt_requested = False + retry_response: Optional[RetryHttpResponse] = None + response_body = "" + + if self.logger.level <= logging.DEBUG: + self.logger.debug(f"Sending a request - url: {self.url}, body: {str_body}, headers: {headers}") + + try: + async with session.request("POST", self.url, **request_kwargs) as res: # type: ignore[arg-type, union-attr] # noqa: E501 + try: + response_body = await res.text() + retry_response = RetryHttpResponse( + status_code=res.status, + headers=res.headers, # type: ignore[arg-type] + data=response_body.encode("utf-8") if response_body is not None else None, + ) + except aiohttp.ContentTypeError: + self.logger.debug(f"No response data returned from the following API call: {self.url}") + retry_response = RetryHttpResponse( + status_code=res.status, + headers=res.headers, # type: ignore[arg-type] + ) + + if res.status == 429: + for handler in self.retry_handlers: + if await handler.can_retry_async( + state=retry_state, + request=retry_request, + response=retry_response, + ): + if self.logger.level <= logging.DEBUG: + self.logger.info( + f"A retry handler found: {type(handler).__name__} " + f"for POST {self.url} - rate_limited" + ) + await handler.prepare_for_next_attempt_async( + state=retry_state, + request=retry_request, + response=retry_response, + ) + break + + if retry_state.next_attempt_requested is False: + resp = WebhookResponse( + url=self.url, + status_code=res.status, + body=response_body, + headers=res.headers, # type: ignore[arg-type] + ) + _debug_log_response(self.logger, resp) + return resp + + except Exception as e: + last_error = e + for handler in self.retry_handlers: + if await handler.can_retry_async( + state=retry_state, + request=retry_request, + response=retry_response, + error=e, + ): + if self.logger.level <= logging.DEBUG: + self.logger.info( + f"A retry handler found: {type(handler).__name__} " f"for POST {self.url} - {e}" + ) + await handler.prepare_for_next_attempt_async( + state=retry_state, + request=retry_request, + response=retry_response, + error=e, + ) + break + + if retry_state.next_attempt_requested is False: + raise last_error + + if resp is not None: + return resp + raise last_error # type: ignore[misc] + + finally: + if not use_running_session: + await session.close() # type: ignore[union-attr] + + return resp diff --git a/slack_sdk/webhook/client.py b/slack_sdk/webhook/client.py new file mode 100644 index 000000000..d7f9f603d --- /dev/null +++ b/slack_sdk/webhook/client.py @@ -0,0 +1,279 @@ +import json +import logging +import urllib +from http.client import HTTPResponse +from ssl import SSLContext +from typing import Dict, Union, Sequence, Optional, List, Any +from urllib.error import HTTPError +from urllib.request import Request, urlopen, OpenerDirector, ProxyHandler, HTTPSHandler + +from slack_sdk.errors import SlackRequestError +from slack_sdk.models.attachments import Attachment +from slack_sdk.models.blocks import Block +from .internal_utils import ( + _build_body, + _build_request_headers, + _debug_log_response, + get_user_agent, +) +from .webhook_response import WebhookResponse +from slack_sdk.http_retry import default_retry_handlers +from slack_sdk.http_retry.handler import RetryHandler +from slack_sdk.http_retry.request import HttpRequest as RetryHttpRequest +from slack_sdk.http_retry.response import HttpResponse as RetryHttpResponse +from slack_sdk.http_retry.state import RetryState +from ..proxy_env_variable_loader import load_http_proxy_from_env + + +class WebhookClient: + url: str + timeout: int + ssl: Optional[SSLContext] + proxy: Optional[str] + default_headers: Dict[str, str] + logger: logging.Logger + retry_handlers: List[RetryHandler] + + def __init__( + self, + url: str, + timeout: int = 30, + ssl: Optional[SSLContext] = None, + proxy: Optional[str] = None, + default_headers: Optional[Dict[str, str]] = None, + user_agent_prefix: Optional[str] = None, + user_agent_suffix: Optional[str] = None, + logger: Optional[logging.Logger] = None, + retry_handlers: Optional[List[RetryHandler]] = None, + ): + """API client for Incoming Webhooks and `response_url` + + https://docs.slack.dev/messaging/sending-messages-using-incoming-webhooks/ + + Args: + url: Complete URL to send data (e.g., `https://hooks.slack.com/XXX`) + timeout: Request timeout (in seconds) + ssl: `ssl.SSLContext` to use for requests + proxy: Proxy URL (e.g., `localhost:9000`, `http://localhost:9000`) + default_headers: Request headers to add to all requests + user_agent_prefix: Prefix for User-Agent header value + user_agent_suffix: Suffix for User-Agent header value + logger: Custom logger + retry_handlers: Retry handlers + """ + self.url = url + self.timeout = timeout + self.ssl = ssl + self.proxy = proxy + self.default_headers = default_headers if default_headers else {} + self.default_headers["User-Agent"] = get_user_agent(user_agent_prefix, user_agent_suffix) + self.logger = logger if logger is not None else logging.getLogger(__name__) + self.retry_handlers = retry_handlers if retry_handlers is not None else default_retry_handlers() + + if self.proxy is None or len(self.proxy.strip()) == 0: + env_variable = load_http_proxy_from_env(self.logger) + if env_variable is not None: + self.proxy = env_variable + + def send( + self, + *, + text: Optional[str] = None, + attachments: Optional[Sequence[Union[Dict[str, Any], Attachment]]] = None, + blocks: Optional[Sequence[Union[Dict[str, Any], Block]]] = None, + response_type: Optional[str] = None, + replace_original: Optional[bool] = None, + delete_original: Optional[bool] = None, + unfurl_links: Optional[bool] = None, + unfurl_media: Optional[bool] = None, + metadata: Optional[Dict[str, Any]] = None, + headers: Optional[Dict[str, str]] = None, + ) -> WebhookResponse: + """Performs a Slack API request and returns the result. + + Args: + text: The text message + (even when having blocks, setting this as well is recommended as it works as fallback) + attachments: A collection of attachments + blocks: A collection of Block Kit UI components + response_type: The type of message (either 'in_channel' or 'ephemeral') + replace_original: True if you use this option for response_url requests + delete_original: True if you use this option for response_url requests + unfurl_links: Option to indicate whether text url should unfurl + unfurl_media: Option to indicate whether media url should unfurl + metadata: Metadata attached to the message + headers: Request headers to append only for this request + + Returns: + Webhook response + """ + return self.send_dict( + # It's fine to have None value elements here + # because _build_body() filters them out when constructing the actual body data + body={ + "text": text, + "attachments": attachments, + "blocks": blocks, + "response_type": response_type, + "replace_original": replace_original, + "delete_original": delete_original, + "unfurl_links": unfurl_links, + "unfurl_media": unfurl_media, + "metadata": metadata, + }, + headers=headers, + ) + + def send_dict(self, body: Dict[str, Any], headers: Optional[Dict[str, str]] = None) -> WebhookResponse: + """Performs a Slack API request and returns the result. + + Args: + body: JSON data structure (it's still a dict at this point), + if you give this argument, body_params and files will be skipped + headers: Request headers to append only for this request + Returns: + Webhook response + """ + return self._perform_http_request( + body=_build_body(body), # type: ignore[arg-type] + headers=_build_request_headers(self.default_headers, headers), + ) + + def _perform_http_request(self, *, body: Dict[str, Any], headers: Dict[str, str]) -> WebhookResponse: + raw_body = json.dumps(body) + headers["Content-Type"] = "application/json;charset=utf-8" + + if self.logger.level <= logging.DEBUG: + self.logger.debug(f"Sending a request - url: {self.url}, body: {raw_body}, headers: {headers}") + + url = self.url + # NOTE: Intentionally ignore the `http_verb` here + # Slack APIs accepts any API method requests with POST methods + req = Request(method="POST", url=url, data=raw_body.encode("utf-8"), headers=headers) + resp = None + last_error = Exception("undefined internal error") + + retry_state = RetryState() + counter_for_safety = 0 + while counter_for_safety < 100: + counter_for_safety += 1 + # If this is a retry, the next try started here. We can reset the flag. + retry_state.next_attempt_requested = False + + try: + resp = self._perform_http_request_internal(url, req) + # The resp is a 200 OK response + return resp + + except HTTPError as e: + # read the response body here + charset = e.headers.get_content_charset() or "utf-8" + response_body: str = e.read().decode(charset) + # As adding new values to HTTPError#headers can be ignored, building a new dict object here + response_headers = dict(e.headers.items()) + resp = WebhookResponse( + url=url, + status_code=e.code, + body=response_body, + headers=response_headers, + ) + if e.code == 429: + # for backward-compatibility with WebClient (v.2.5.0 or older) + if "retry-after" not in resp.headers and "Retry-After" in resp.headers: + resp.headers["retry-after"] = resp.headers["Retry-After"] + if "Retry-After" not in resp.headers and "retry-after" in resp.headers: + resp.headers["Retry-After"] = resp.headers["retry-after"] + _debug_log_response(self.logger, resp) + + # Try to find a retry handler for this error + retry_request = RetryHttpRequest.from_urllib_http_request(req) + retry_response = RetryHttpResponse( + status_code=e.code, + headers={k: [v] for k, v in e.headers.items()}, + data=response_body.encode("utf-8") if response_body is not None else None, + ) + for handler in self.retry_handlers: + if handler.can_retry( + state=retry_state, + request=retry_request, + response=retry_response, + error=e, + ): + if self.logger.level <= logging.DEBUG: + self.logger.info( + f"A retry handler found: {type(handler).__name__} for {req.method} {req.full_url} - {e}" + ) + handler.prepare_for_next_attempt( + state=retry_state, + request=retry_request, + response=retry_response, + error=e, + ) + break + + if retry_state.next_attempt_requested is False: + return resp + + except Exception as err: + last_error = err + self.logger.error(f"Failed to send a request to Slack API server: {err}") + + # Try to find a retry handler for this error + retry_request = RetryHttpRequest.from_urllib_http_request(req) + for handler in self.retry_handlers: + if handler.can_retry( + state=retry_state, + request=retry_request, + response=None, + error=err, + ): + if self.logger.level <= logging.DEBUG: + self.logger.info( + f"A retry handler found: {type(handler).__name__} for {req.method} {req.full_url} - {err}" + ) + handler.prepare_for_next_attempt( + state=retry_state, + request=retry_request, + response=None, + error=err, + ) + self.logger.info(f"Going to retry the same request: {req.method} {req.full_url}") + break + + if retry_state.next_attempt_requested is False: + raise err + + if resp is not None: + return resp + raise last_error + + def _perform_http_request_internal(self, url: str, req: Request): + opener: Optional[OpenerDirector] = None + # for security (BAN-B310) + if url.lower().startswith("http"): + if self.proxy is not None: + if isinstance(self.proxy, str): + opener = urllib.request.build_opener( + ProxyHandler({"http": self.proxy, "https": self.proxy}), + HTTPSHandler(context=self.ssl), + ) + else: + raise SlackRequestError(f"Invalid proxy detected: {self.proxy} must be a str value") + else: + raise SlackRequestError(f"Invalid URL detected: {url}") + + http_resp: Optional[HTTPResponse] = None + if opener: + http_resp = opener.open(req, timeout=self.timeout) + else: + http_resp = urlopen(req, context=self.ssl, timeout=self.timeout) + charset: str = http_resp.headers.get_content_charset() or "utf-8" + response_body: str = http_resp.read().decode(charset) + resp = WebhookResponse( + url=url, + status_code=http_resp.status, + body=response_body, + headers=http_resp.headers, # type: ignore[arg-type] + ) + _debug_log_response(self.logger, resp) + return resp diff --git a/slack_sdk/webhook/internal_utils.py b/slack_sdk/webhook/internal_utils.py new file mode 100644 index 000000000..1dd94deeb --- /dev/null +++ b/slack_sdk/webhook/internal_utils.py @@ -0,0 +1,45 @@ +import logging +from typing import Optional, Dict, Any + +from slack_sdk.web.internal_utils import ( + _parse_web_class_objects, + get_user_agent, +) +from .webhook_response import WebhookResponse + + +def _build_body(original_body: Optional[Dict[str, Any]]) -> Optional[Dict[str, Any]]: + if original_body: + body = {k: v for k, v in original_body.items() if v is not None} + _parse_web_class_objects(body) + return body + return None + + +def _build_request_headers( + default_headers: Dict[str, str], + additional_headers: Optional[Dict[str, str]], +) -> Dict[str, str]: + if default_headers is None and additional_headers is None: + return {} + + request_headers = { + "Content-Type": "application/json;charset=utf-8", + } + if default_headers is None or "User-Agent" not in default_headers: + request_headers["User-Agent"] = get_user_agent() + + request_headers.update(default_headers) + if additional_headers: + request_headers.update(additional_headers) + return request_headers + + +def _debug_log_response(logger, resp: WebhookResponse) -> None: + if logger.level <= logging.DEBUG: + logger.debug( + "Received the following response - " + f"status: {resp.status_code}, " + f"headers: {(dict(resp.headers))}, " + f"body: {resp.body}" + ) diff --git a/slack_sdk/webhook/webhook_response.py b/slack_sdk/webhook/webhook_response.py new file mode 100644 index 000000000..c45eebac0 --- /dev/null +++ b/slack_sdk/webhook/webhook_response.py @@ -0,0 +1,16 @@ +from typing import Dict, Any + + +class WebhookResponse: + def __init__( + self, + *, + url: str, + status_code: int, + body: str, + headers: Dict[str, Any], + ): + self.api_url = url + self.status_code = status_code + self.body = body + self.headers = headers diff --git a/slackclient/__init__.py b/slackclient/__init__.py deleted file mode 100644 index 14e478682..000000000 --- a/slackclient/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from slackclient._client import SlackClient diff --git a/slackclient/_channel.py b/slackclient/_channel.py deleted file mode 100644 index 905297d0e..000000000 --- a/slackclient/_channel.py +++ /dev/null @@ -1,26 +0,0 @@ -class Channel(object): - def __init__(self, server, name, id, members=[]): - self.server = server - self.name = name - self.id = id - self.members = members - - def __eq__(self, compare_str): - if self.name == compare_str or self.name == "#" + compare_str or self.id == compare_str: - return True - else: - return False - - def __str__(self): - data = "" - for key in self.__dict__.keys(): - data += "{} : {}\n".format(key, str(self.__dict__[key])[:40]) - return data - - def __repr__(self): - return self.__str__() - - def send_message(self, message): - message_json = {"type": "message", "channel": self.id, "text": message} - self.server.send_to_websocket(message_json) - diff --git a/slackclient/_client.py b/slackclient/_client.py deleted file mode 100644 index c4924a32c..000000000 --- a/slackclient/_client.py +++ /dev/null @@ -1,51 +0,0 @@ -#!/usr/bin/python -#mostly a proxy object to abstract how some of this works - -import json - -from slackclient._server import Server - -class SlackClient(object): - def __init__(self, token): - self.token = token - self.server = Server(self.token, False) - - def rtm_connect(self): - try: - self.server.rtm_connect() - return True - except: - return False - - def api_call(self, method, **kwargs): - return self.server.api_call(method, **kwargs) - - def rtm_read(self): - #in the future, this should handle some events internally i.e. channel creation - if self.server: - json_data = self.server.websocket_safe_read() - data = [] - if json_data != '': - for d in json_data.split('\n'): - data.append(json.loads(d)) - for item in data: - self.process_changes(item) - return data - else: - raise SlackNotConnected - - def rtm_send_message(self, channel, message): - return self.server.channels.find(channel).send_message(message) - - def process_changes(self, data): - if "type" in data.keys(): - if data["type"] == 'channel_created': - channel = data["channel"] - self.server.attach_channel(channel["name"], channel["id"], []) - if data["type"] == 'im_created': - channel = data["channel"] - self.server.attach_channel(channel["user"], channel["id"], []) - pass - -class SlackNotConnected(Exception): - pass diff --git a/slackclient/_im.py b/slackclient/_im.py deleted file mode 100644 index 2a339f9a5..000000000 --- a/slackclient/_im.py +++ /dev/null @@ -1,26 +0,0 @@ -class Im(object): - def __init__(self, server, user, id): - self.server = server - self.user = user - self.id = id - - def __eq__(self, compare_str): - if self.id == compare_str or self.user == compare_str: - return True - else: - return False - - def __str__(self): - data = "" - for key in self.__dict__.keys(): - if key != "server": - data += "{} : {}\n".format(key, str(self.__dict__[key])[:40]) - return data - - def __repr__(self): - return self.__str__() - - def send_message(self, message): - message_json = {"type": "message", "channel": self.id, "text": message} - self.server.send_to_websocket(message_json) - diff --git a/slackclient/_server.py b/slackclient/_server.py deleted file mode 100644 index 3500d7834..000000000 --- a/slackclient/_server.py +++ /dev/null @@ -1,120 +0,0 @@ -from slackclient._slackrequest import SlackRequest -from slackclient._channel import Channel -from slackclient._user import User -from slackclient._util import SearchList - -from websocket import create_connection -import json - -class Server(object): - def __init__(self, token, connect=True): - self.token = token - self.username = None - self.domain = None - self.login_data = None - self.websocket = None - self.users = SearchList() - self.channels = SearchList() - self.connected = False - self.pingcounter = 0 - self.api_requester = SlackRequest() - - if connect: - self.rtm_connect() - def __eq__(self, compare_str): - if compare_str == self.domain or compare_str == self.token: - return True - else: - return False - def __str__(self): - data = "" - for key in self.__dict__.keys(): - data += "{} : {}\n".format(key, str(self.__dict__[key])[:40]) - return data - def __repr__(self): - return self.__str__() - - def rtm_connect(self, reconnect=False): - reply = self.api_requester.do(self.token, "rtm.start") - if reply.code != 200: - raise SlackConnectionError - else: - login_data = json.loads(reply.read()) - if login_data["ok"]: - self.ws_url = login_data['url'] - if not reconnect: - self.parse_slack_login_data(login_data) - self.connect_slack_websocket(self.ws_url) - else: - raise SlackLoginError - - def parse_slack_login_data(self, login_data): - self.login_data = login_data - self.domain = self.login_data["team"]["domain"] - self.username = self.login_data["self"]["name"] - self.parse_channel_data(login_data["channels"]) - self.parse_channel_data(login_data["groups"]) - self.parse_channel_data(login_data["ims"]) - self.parse_user_data(login_data["users"]) - - def connect_slack_websocket(self, ws_url): - try: - self.websocket = create_connection(ws_url) - self.websocket.sock.setblocking(0) - except: - raise SlackConnectionError - - def parse_channel_data(self, channel_data): - for channel in channel_data: - if "name" not in channel: - channel["name"] = channel["id"] - if "members" not in channel: - channel["members"] = [] - self.attach_channel(channel["name"], channel["id"], channel["members"]) - - def parse_user_data(self, user_data): - for user in user_data: - if "tz" not in user: - user["tz"] = "unknown" - if "real_name" not in user: - user["real_name"] = user["name"] - self.attach_user(user["name"], user["id"], user["real_name"], user["tz"]) - - def send_to_websocket(self, data): - """Send (data) directly to the websocket.""" - try: - data = json.dumps(data) - self.websocket.send(data) - except: - self.rtm_connect(reconnect=True) - - def ping(self): - return self.send_to_websocket({"type": "ping"}) - - def websocket_safe_read(self): - """Returns data if available, otherwise ''. Newlines indicate multiple messages """ - data = "" - while True: - try: - data += "{}\n".format(self.websocket.recv()) - except: - return data.rstrip() - - def attach_user(self, name, id, real_name, tz): - self.users.append(User(self, name, id, real_name, tz)) - - def attach_channel(self, name, id, members=[]): - self.channels.append(Channel(self, name, id, members)) - - def join_channel(self, name): - print self.api_requester.do(self.token, "channels.join?name={}".format(name)).read() - - def api_call(self, method, **kwargs): - reply = self.api_requester.do(self.token, method, kwargs) - return reply.read() - -class SlackConnectionError(Exception): - pass - -class SlackLoginError(Exception): - pass diff --git a/slackclient/_slackrequest.py b/slackclient/_slackrequest.py deleted file mode 100644 index ddf12cfba..000000000 --- a/slackclient/_slackrequest.py +++ /dev/null @@ -1,14 +0,0 @@ -import time -import urllib -import urllib2 - -class SlackRequest(object): - def __init__(self): - pass - - def do(self, token, request="?", post_data={}, domain="slack.com"): - post_data["token"] = token - post_data = urllib.urlencode(post_data) - url = 'https://{}/api/{}'.format(domain, request) - return urllib2.urlopen(url, post_data) - diff --git a/slackclient/_user.py b/slackclient/_user.py deleted file mode 100644 index 93e2c9265..000000000 --- a/slackclient/_user.py +++ /dev/null @@ -1,23 +0,0 @@ -class User(object): - def __init__(self, server, name, id, real_name, tz): - self.tz = tz - self.name = name - self.real_name = real_name - self.server = server - self.id = id - - def __eq__(self, compare_str): - if self.id == compare_str or self.name == compare_str: - return True - else: - return False - - def __str__(self): - data = "" - for key in self.__dict__.keys(): - if key != "server": - data += "{} : {}\n".format(key, str(self.__dict__[key])[:40]) - return data - - def __repr__(self): - return self.__str__() diff --git a/slackclient/_util.py b/slackclient/_util.py deleted file mode 100644 index a5aa54d82..000000000 --- a/slackclient/_util.py +++ /dev/null @@ -1,16 +0,0 @@ -class SearchList(list): - - def find(self, name): - items = [] - for child in self: - if child.__class__ == self.__class__: - items += child.find(name) - else: - if child == name: - items.append(child) - - if len(items) == 1: - return items[0] - elif items != []: - return items - diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/_pytest/data/channel.created.json b/tests/data/channel.created.json similarity index 100% rename from _pytest/data/channel.created.json rename to tests/data/channel.created.json diff --git a/_pytest/data/im.created.json b/tests/data/im.created.json similarity index 100% rename from _pytest/data/im.created.json rename to tests/data/im.created.json diff --git a/_pytest/data/rtm.start.json b/tests/data/rtm.start.json similarity index 88% rename from _pytest/data/rtm.start.json rename to tests/data/rtm.start.json index f02c91dab..a0696eda9 100644 --- a/_pytest/data/rtm.start.json +++ b/tests/data/rtm.start.json @@ -245,13 +245,7 @@ "deleted": false, "status": null, "color": "9f69e7", - "real_name": "", - "tz": "America\/Los_Angeles", - "tz_label": "Pacific Daylight Time", - "tz_offset": -25200, "profile": { - "real_name": "", - "real_name_normalized": "", "email": "fakeuser@example.com", "image_24": "https:\/\/secure.gravatar.com\/avatar\/4f1bd7fd71e645fa19620504b4c0e3ba.jpg?s=24&d=https%3A%2F%2Fslack.global.ssl.fastly.net%2F3654%2Fimg%2Favatars%2Fava_0002-24.png", "image_32": "https:\/\/secure.gravatar.com\/avatar\/4f1bd7fd71e645fa19620504b4c0e3ba.jpg?s=32&d=https%3A%2F%2Fslack.global.ssl.fastly.net%2F3654%2Fimg%2Favatars%2Fava_0002-32.png", @@ -268,6 +262,28 @@ "has_files": false, "presence": "away" }, + { + "id": "U10CX1235", + "name": "userwithoutemail", + "deleted": false, + "status": null, + "color": "9f69e7", + "profile": { + "image_24": "https:\/\/secure.gravatar.com\/avatar\/4f1bd7fd71e645fa19620504b4c0e3ba.jpg?s=24&d=https%3A%2F%2Fslack.global.ssl.fastly.net%2F3654%2Fimg%2Favatars%2Fava_0002-24.png", + "image_32": "https:\/\/secure.gravatar.com\/avatar\/4f1bd7fd71e645fa19620504b4c0e3ba.jpg?s=32&d=https%3A%2F%2Fslack.global.ssl.fastly.net%2F3654%2Fimg%2Favatars%2Fava_0002-32.png", + "image_48": "https:\/\/secure.gravatar.com\/avatar\/4f1bd7fd71e645fa19620504b4c0e3ba.jpg?s=48&d=https%3A%2F%2Fslack.global.ssl.fastly.net%2F272a%2Fimg%2Favatars%2Fava_0002-48.png", + "image_72": "https:\/\/secure.gravatar.com\/avatar\/4f1bd7fd71e645fa19620504b4c0e3ba.jpg?s=72&d=https%3A%2F%2Fslack.global.ssl.fastly.net%2F3654%2Fimg%2Favatars%2Fava_0002-72.png", + "image_192": "https:\/\/secure.gravatar.com\/avatar\/4f1bd7fd71e645fa19620504b4c0e3ba.jpg?s=192&d=https%3A%2F%2Fslack.global.ssl.fastly.net%2F3654%2Fimg%2Favatars%2Fava_0002.png" + }, + "is_admin": true, + "is_owner": true, + "is_primary_owner": true, + "is_restricted": false, + "is_ultra_restricted": false, + "is_bot": false, + "has_files": false, + "presence": "away" + }, { "id": "USLACKBOT", "name": "slackbot", @@ -301,5 +317,5 @@ ], "bots": [], "cache_version": "v5-dog", - "url": "wss:\/\/ms9999.slack-msgs.com\/websocket\/rvyiQ_oxNhQ2C6_613rtqs1PFfT0AmivZTokv\/VOVQCmq3bk\/KarC2Z2ZMFfdMMtxn4kx9ILl6sE7JgvKv6Bct5okT0Lgru416DXsKJolJQ=" + "url": "wss:\/\/cerberus-xxxx.lb.slack-msgs.com\/websocket\/ifkp3MKfNXd6ftbrEGllwcHn" } diff --git a/tests/data/slack_logo.png b/tests/data/slack_logo.png new file mode 100644 index 000000000..232a00cf1 Binary files /dev/null and b/tests/data/slack_logo.png differ diff --git a/tests/data/slack_logo_new.png b/tests/data/slack_logo_new.png new file mode 100644 index 000000000..2b95382ce Binary files /dev/null and b/tests/data/slack_logo_new.png differ diff --git a/tests/data/view_home_001.json b/tests/data/view_home_001.json new file mode 100644 index 000000000..21e0244f0 --- /dev/null +++ b/tests/data/view_home_001.json @@ -0,0 +1,433 @@ +{ + "type": "home", + "blocks": [ + { + "type": "section", + "text": { + "type": "plain_text", + "text": "This is a plain text section block.", + "emoji": true + } + }, + { + "type": "image", + "title": { + "type": "plain_text", + "text": "image1", + "emoji": true + }, + "image_url": "https://api.slack.com/img/blocks/bkb_template_images/beagle.png", + "alt_text": "image1" + }, + { + "type": "context", + "elements": [ + { + "type": "mrkdwn", + "text": "Last updated: Jan 1, 2019" + } + ] + }, + { + "type": "divider" + }, + { + "type": "actions", + "elements": [ + { + "type": "checkboxes", + "options": [ + { + "text": { + "type": "plain_text", + "text": "*this is plain_text text*", + "emoji": true + }, + "description": { + "type": "plain_text", + "text": "*this is plain_text text*", + "emoji": true + }, + "value": "value-0" + }, + { + "text": { + "type": "plain_text", + "text": "*this is plain_text text*", + "emoji": true + }, + "description": { + "type": "plain_text", + "text": "*this is plain_text text*", + "emoji": true + }, + "value": "value-1" + }, + { + "text": { + "type": "plain_text", + "text": "*this is plain_text text*", + "emoji": true + }, + "description": { + "type": "plain_text", + "text": "*this is plain_text text*", + "emoji": true + }, + "value": "value-2" + } + ] + }, + { + "type": "checkboxes", + "options": [ + { + "text": { + "type": "mrkdwn", + "text": "*this is mrkdwn text*" + }, + "description": { + "type": "mrkdwn", + "text": "*this is mrkdwn text*" + }, + "value": "value-0" + }, + { + "text": { + "type": "mrkdwn", + "text": "*this is mrkdwn text*" + }, + "description": { + "type": "mrkdwn", + "text": "*this is mrkdwn text*" + }, + "value": "value-1" + }, + { + "text": { + "type": "mrkdwn", + "text": "*this is mrkdwn text*" + }, + "description": { + "type": "mrkdwn", + "text": "*this is mrkdwn text*" + }, + "value": "value-2" + } + ] + } + ] + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "You can add an image next to text in this block." + }, + "accessory": { + "type": "image", + "image_url": "https://api.slack.com/img/blocks/bkb_template_images/plants.png", + "alt_text": "plants" + } + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "You can add a button alongside text in your message. " + }, + "accessory": { + "type": "button", + "text": { + "type": "plain_text", + "text": "Button", + "emoji": true + }, + "value": "click_me_123" + } + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "Pick an item from the dropdown list" + }, + "accessory": { + "type": "static_select", + "placeholder": { + "type": "plain_text", + "text": "Select an item", + "emoji": true + }, + "options": [ + { + "text": { + "type": "plain_text", + "text": "Choice 1", + "emoji": true + }, + "value": "value-0" + }, + { + "text": { + "type": "plain_text", + "text": "Choice 2", + "emoji": true + }, + "value": "value-1" + }, + { + "text": { + "type": "plain_text", + "text": "Choice 3", + "emoji": true + }, + "value": "value-2" + } + ] + } + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "Pick one or more items from the list" + }, + "accessory": { + "type": "multi_static_select", + "placeholder": { + "type": "plain_text", + "text": "Select items", + "emoji": true + }, + "options": [ + { + "text": { + "type": "plain_text", + "text": "Choice 1", + "emoji": true + }, + "value": "value-0" + }, + { + "text": { + "type": "plain_text", + "text": "Choice 2", + "emoji": true + }, + "value": "value-1" + }, + { + "text": { + "type": "plain_text", + "text": "Choice 3", + "emoji": true + }, + "value": "value-2" + } + ] + } + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "This block has an overflow menu." + }, + "accessory": { + "type": "overflow", + "options": [ + { + "text": { + "type": "plain_text", + "text": "Option 1", + "emoji": true + }, + "value": "value-0" + }, + { + "text": { + "type": "plain_text", + "text": "Option 2", + "emoji": true + }, + "value": "value-1" + }, + { + "text": { + "type": "plain_text", + "text": "Option 3", + "emoji": true + }, + "value": "value-2" + }, + { + "text": { + "type": "plain_text", + "text": "Option 4", + "emoji": true + }, + "value": "value-3" + } + ] + } + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "Pick a date for the deadline." + }, + "accessory": { + "type": "datepicker", + "initial_date": "1990-04-28", + "placeholder": { + "type": "plain_text", + "text": "Select a date", + "emoji": true + } + } + }, + { + "type": "section", + "fields": [ + { + "type": "plain_text", + "text": "*this is plain_text text*", + "emoji": true + }, + { + "type": "plain_text", + "text": "*this is plain_text text*", + "emoji": true + }, + { + "type": "plain_text", + "text": "*this is plain_text text*", + "emoji": true + }, + { + "type": "plain_text", + "text": "*this is plain_text text*", + "emoji": true + }, + { + "type": "plain_text", + "text": "*this is plain_text text*", + "emoji": true + } + ] + }, + { + "type": "section", + "fields": [ + { + "type": "mrkdwn", + "text": "This is a section block with _another_ fields radio button accessory" + }, + { + "type": "mrkdwn", + "text": "This is a section block with *fields* radio button accessory" + }, + { + "type": "mrkdwn", + "text": "This is a section block." + } + ], + "accessory": { + "type": "radio_buttons", + "initial_option": { + "text": { + "type": "plain_text", + "text": "Option 3" + }, + "value": "option 3", + "description": { + "type": "plain_text", + "text": "Description for option 3" + } + }, + "options": [ + { + "text": { + "type": "plain_text", + "text": "Option 1" + }, + "value": "option 1", + "description": { + "type": "plain_text", + "text": "Description for option 1" + } + }, + { + "text": { + "type": "plain_text", + "text": "Option 2" + }, + "value": "option 2", + "description": { + "type": "plain_text", + "text": "Description for option 2" + } + }, + { + "text": { + "type": "plain_text", + "text": "Option 3" + }, + "value": "option 3", + "description": { + "type": "plain_text", + "text": "Description for option 3" + } + } + ] + } + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "This is a section block with checkboxes." + }, + "accessory": { + "type": "checkboxes", + "options": [ + { + "text": { + "type": "mrkdwn", + "text": "*this is mrkdwn text*" + }, + "description": { + "type": "mrkdwn", + "text": "*this is mrkdwn text*" + }, + "value": "value-0" + }, + { + "text": { + "type": "mrkdwn", + "text": "*this is mrkdwn text*" + }, + "description": { + "type": "mrkdwn", + "text": "*this is mrkdwn text*" + }, + "value": "value-1" + }, + { + "text": { + "type": "mrkdwn", + "text": "*this is mrkdwn text*" + }, + "description": { + "type": "mrkdwn", + "text": "*this is mrkdwn text*" + }, + "value": "value-2" + } + ] + } + } + ] +} \ No newline at end of file diff --git a/tests/data/view_home_002.json b/tests/data/view_home_002.json new file mode 100644 index 000000000..64f4a2592 --- /dev/null +++ b/tests/data/view_home_002.json @@ -0,0 +1,117 @@ +{ + "type": "home", + "blocks": [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "*Here's what you can do with Project Tracker:*" + } + }, + { + "type": "actions", + "elements": [ + { + "type": "button", + "text": { + "type": "plain_text", + "text": "Create New Task", + "emoji": true + }, + "style": "primary", + "value": "create_task" + }, + { + "type": "button", + "text": { + "type": "plain_text", + "text": "Create New Project", + "emoji": true + }, + "value": "create_project" + }, + { + "type": "button", + "text": { + "type": "plain_text", + "text": "Help", + "emoji": true + }, + "value": "help" + } + ] + }, + { + "type": "context", + "elements": [ + { + "type": "image", + "image_url": "https://api.slack.com/img/blocks/bkb_template_images/placeholder.png", + "alt_text": "placeholder" + } + ] + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "*Your Configurations*" + } + }, + { + "type": "divider" + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "*#public-relations*\n posts new tasks, comments, and project updates to " + }, + "accessory": { + "type": "button", + "text": { + "type": "plain_text", + "text": "Edit", + "emoji": true + }, + "value": "public-relations" + } + }, + { + "type": "divider" + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "*#team-updates*\n posts project updates to " + }, + "accessory": { + "type": "button", + "text": { + "type": "plain_text", + "text": "Edit", + "emoji": true + }, + "value": "public-relations" + } + }, + { + "type": "divider" + }, + { + "type": "actions", + "elements": [ + { + "type": "button", + "text": { + "type": "plain_text", + "text": "New Configuration", + "emoji": true + }, + "value": "new_configuration" + } + ] + } + ] +} \ No newline at end of file diff --git a/tests/data/view_home_003.json b/tests/data/view_home_003.json new file mode 100644 index 000000000..b26677a1b --- /dev/null +++ b/tests/data/view_home_003.json @@ -0,0 +1,236 @@ +{ + "type": "home", + "blocks": [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "*Today, 22 October*" + }, + "accessory": { + "type": "button", + "text": { + "type": "plain_text", + "text": "Manage App Settings", + "emoji": true + }, + "value": "settings" + } + }, + { + "type": "actions", + "elements": [ + { + "type": "datepicker", + "initial_date": "2019-10-22", + "placeholder": { + "type": "plain_text", + "text": "Select a date", + "emoji": true + } + } + ] + }, + { + "type": "divider" + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "**\n11:30am — 12:30pm | SF500 · 7F · Saturn (5)\nStatus: ✅ Going" + }, + "accessory": { + "type": "overflow", + "options": [ + { + "text": { + "type": "plain_text", + "text": "View Event Details", + "emoji": true + }, + "value": "view_event_details" + }, + { + "text": { + "type": "plain_text", + "text": "Change Response", + "emoji": true + }, + "value": "change_response" + } + ] + } + }, + { + "type": "actions", + "elements": [ + { + "type": "button", + "text": { + "type": "plain_text", + "text": "Join Video Call", + "emoji": true + }, + "style": "primary", + "value": "join" + } + ] + }, + { + "type": "divider" + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "**\n1:30pm — 2:00pm | SF500 · 4F · Finch (4)" + }, + "accessory": { + "type": "overflow", + "options": [ + { + "text": { + "type": "plain_text", + "text": "View Event Details", + "emoji": true + }, + "value": "view_event_details" + }, + { + "text": { + "type": "plain_text", + "text": "Change Response", + "emoji": true + }, + "value": "change_response" + } + ] + } + }, + { + "type": "actions", + "elements": [ + { + "type": "static_select", + "placeholder": { + "type": "plain_text", + "text": "Going?", + "emoji": true + }, + "options": [ + { + "text": { + "type": "plain_text", + "text": "Going", + "emoji": true + }, + "value": "going" + }, + { + "text": { + "type": "plain_text", + "text": "Maybe", + "emoji": true + }, + "value": "maybe" + }, + { + "text": { + "type": "plain_text", + "text": "Not going", + "emoji": true + }, + "value": "decline" + } + ] + } + ] + }, + { + "type": "divider" + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "**\n4:00pm — 5:30pm | SF500 · 7F · Saturn (5)\nStatus: ✅ Going" + }, + "accessory": { + "type": "overflow", + "options": [ + { + "text": { + "type": "plain_text", + "text": "View Event Details", + "emoji": true + }, + "value": "view_event_details" + }, + { + "text": { + "type": "plain_text", + "text": "Change Response", + "emoji": true + }, + "value": "change_response" + } + ] + } + }, + { + "type": "actions", + "elements": [ + { + "type": "button", + "text": { + "type": "plain_text", + "text": "Join Video Call", + "emoji": true + }, + "style": "primary", + "value": "join" + } + ] + }, + { + "type": "divider" + }, + { + "type": "context", + "elements": [ + { + "type": "image", + "image_url": "https://api.slack.com/img/blocks/bkb_template_images/placeholder.png", + "alt_text": "placeholder" + } + ] + }, + { + "type": "context", + "elements": [ + { + "type": "mrkdwn", + "text": "Past events" + } + ] + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "*Marketing team breakfast*\n8:30am — 9:30am | SF500 · 7F · Saturn (5)" + } + }, + { + "type": "divider" + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "*Coffee chat w/ candidate*\n10:30am — 11:00am | SF500 · 10F · Cafe" + } + } + ] +} \ No newline at end of file diff --git a/tests/data/view_home_004.json b/tests/data/view_home_004.json new file mode 100644 index 000000000..ec133c6e9 --- /dev/null +++ b/tests/data/view_home_004.json @@ -0,0 +1,188 @@ +{ + "type": "home", + "blocks": [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "*Budget Performance*" + }, + "accessory": { + "type": "button", + "text": { + "type": "plain_text", + "text": "Manage App Settings", + "emoji": true + }, + "value": "app_settings" + } + }, + { + "type": "divider" + }, + { + "type": "section", + "fields": [ + { + "type": "mrkdwn", + "text": "*Current Quarter*\nBudget: $18,000 (ends in 53 days)\nSpend: $4,289.70\nRemain: $13,710.30" + }, + { + "type": "mrkdwn", + "text": "*Top Expense Categories*\n:airplane: Flights · 30%\n:taxi: Taxi / Uber / Lyft · 24% \n:knife_fork_plate: Client lunch / meetings · 18%" + } + ] + }, + { + "type": "context", + "elements": [ + { + "type": "image", + "image_url": "https://api.slack.com/img/blocks/bkb_template_images/placeholder.png", + "alt_text": "placeholder" + } + ] + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "*Expenses Awaiting Your Approval*" + } + }, + { + "type": "divider" + }, + { + "type": "context", + "elements": [ + { + "type": "mrkdwn", + "text": "Submitted by" + }, + { + "type": "image", + "image_url": "https://api.slack.com/img/blocks/bkb_template_images/profile_3.png", + "alt_text": "Dwight Schrute" + }, + { + "type": "mrkdwn", + "text": "*Dwight Schrute*" + } + ] + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "*Team Lunch (Internal)*\nCost: *$85.50USD*\nDate: *10/16/2019*\nService Provider: *Honest Sandwiches* \nExpense no. **" + }, + "accessory": { + "type": "image", + "image_url": "https://api.slack.com/img/blocks/bkb_template_images/creditcard.png", + "alt_text": "credit card" + } + }, + { + "type": "actions", + "elements": [ + { + "type": "button", + "text": { + "type": "plain_text", + "text": "Approve", + "emoji": true + }, + "style": "primary", + "value": "approve" + }, + { + "type": "button", + "text": { + "type": "plain_text", + "text": "Decline", + "emoji": true + }, + "style": "danger", + "value": "decline" + }, + { + "type": "button", + "text": { + "type": "plain_text", + "text": "View Details", + "emoji": true + }, + "value": "details" + } + ] + }, + { + "type": "divider" + }, + { + "type": "context", + "elements": [ + { + "type": "mrkdwn", + "text": "Submitted by" + }, + { + "type": "image", + "image_url": "https://api.slack.com/img/blocks/bkb_template_images/profile_2.png", + "alt_text": "Pam Beasely" + }, + { + "type": "mrkdwn", + "text": "*Pam Beasely*" + } + ] + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "*Flights to New York*\nCost: *$520.78USD*\nDate: *10/18/2019*\nService Provider: *Delta Airways*\nExpense no. **" + }, + "accessory": { + "type": "image", + "image_url": "https://api.slack.com/img/blocks/bkb_template_images/plane.png", + "alt_text": "plane" + } + }, + { + "type": "actions", + "elements": [ + { + "type": "button", + "text": { + "type": "plain_text", + "text": "Approve", + "emoji": true + }, + "style": "primary", + "value": "approve" + }, + { + "type": "button", + "text": { + "type": "plain_text", + "text": "Decline", + "emoji": true + }, + "style": "danger", + "value": "decline" + }, + { + "type": "button", + "text": { + "type": "plain_text", + "text": "View Details", + "emoji": true + }, + "value": "details" + } + ] + } + ] +} \ No newline at end of file diff --git a/tests/data/view_home_005.json b/tests/data/view_home_005.json new file mode 100644 index 000000000..60dd7a4e2 --- /dev/null +++ b/tests/data/view_home_005.json @@ -0,0 +1,237 @@ +{ + "type": "home", + "blocks": [ + { + "type": "actions", + "elements": [ + { + "type": "button", + "text": { + "type": "plain_text", + "text": "Create New TODO List", + "emoji": true + }, + "style": "primary", + "value": "create_task" + }, + { + "type": "button", + "text": { + "type": "plain_text", + "text": "Help", + "emoji": true + }, + "value": "help" + } + ] + }, + { + "type": "context", + "elements": [ + { + "type": "image", + "image_url": "https://api.slack.com/img/blocks/bkb_template_images/placeholder.png", + "alt_text": "placeholder" + } + ] + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "*Today*" + }, + "accessory": { + "type": "overflow", + "options": [ + { + "text": { + "type": "plain_text", + "text": ":memo: Edit", + "emoji": true + }, + "value": "edit" + }, + { + "text": { + "type": "plain_text", + "text": ":x: Delete", + "emoji": true + }, + "value": "delete" + } + ] + } + }, + { + "type": "actions", + "elements": [ + { + "type": "checkboxes", + "initial_options": [ + { + "text": { + "type": "mrkdwn", + "text": "~*Get into the garden :house_with_garden:*~" + }, + "value": "option 1" + } + ], + "options": [ + { + "text": { + "type": "mrkdwn", + "text": "~*Get into the garden :house_with_garden:*~" + }, + "value": "option 1" + }, + { + "text": { + "type": "mrkdwn", + "text": "*Get the groundskeeper wet :sweat_drops:*" + }, + "value": "option 2" + }, + { + "text": { + "type": "mrkdwn", + "text": "*Steal the groundskeeper's keys :old_key:*" + }, + "value": "option 3" + }, + { + "text": { + "type": "mrkdwn", + "text": "*Make the groundskeeper wear his sun hat :male-farmer:*" + }, + "value": "option 4" + }, + { + "text": { + "type": "mrkdwn", + "text": "*Rake in the lake :ocean:*" + }, + "value": "option 5" + }, + { + "text": { + "type": "mrkdwn", + "text": "*Have a picnic :knife_fork_plate:*" + }, + "value": "option 6", + "description": { + "type": "mrkdwn", + "text": "Bring to the picnic: sandwich, apple, pumpkin, carrot, basket" + } + } + ] + }, + { + "type": "button", + "text": { + "type": "plain_text", + "text": "Add Item", + "emoji": true + }, + "style": "primary" + } + ] + }, + { + "type": "divider" + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "*Tomorrow*" + }, + "accessory": { + "type": "overflow", + "options": [ + { + "text": { + "type": "plain_text", + "text": ":memo: Edit", + "emoji": true + }, + "value": "edit" + }, + { + "text": { + "type": "plain_text", + "text": ":x: Delete", + "emoji": true + }, + "value": "delete" + } + ] + } + }, + { + "type": "actions", + "elements": [ + { + "type": "checkboxes", + "options": [ + { + "text": { + "type": "mrkdwn", + "text": "*Break the broom :anger:*" + }, + "value": "option 1" + }, + { + "text": { + "type": "mrkdwn", + "text": "*Trap the boy in the phone booth :phone:*" + }, + "value": "option 2" + }, + { + "text": { + "type": "mrkdwn", + "text": "*Make the boy wear the wrong glasses :nerd_face:*" + }, + "value": "option 3" + }, + { + "text": { + "type": "mrkdwn", + "text": "*Make someone buy back their own stuff :money_with_wings:*" + }, + "value": "option 4" + }, + { + "text": { + "type": "mrkdwn", + "text": "*Get on TV :tv:*" + }, + "value": "option 5" + }, + { + "text": { + "type": "mrkdwn", + "text": "*Go shopping :shopping_trolley:*" + }, + "value": "option 6", + "description": { + "type": "mrkdwn", + "text": "Toothbrush, hairbrush, tinned food, cleaner, fruits & vegetables" + } + } + ] + }, + { + "type": "button", + "text": { + "type": "plain_text", + "text": "Add Item", + "emoji": true + }, + "style": "primary" + } + ] + } + ] +} \ No newline at end of file diff --git a/tests/data/view_home_006.json b/tests/data/view_home_006.json new file mode 100644 index 000000000..447779170 --- /dev/null +++ b/tests/data/view_home_006.json @@ -0,0 +1,25 @@ +{ + "id": "VMHU10V25", + "team_id": "T8N4K1JN", + "type": "home", + "blocks": [ + { + "type": "section", + "block_id": "2WGp9", + "text": { + "type": "mrkdwn", + "text": "A simple section with some sample sentence.", + "verbatim": false + } + } + ], + "private_metadata": "Shh it is a secret", + "callback_id": "identify_your_home_tab", + "hash": "156772938.1827394", + "clear_on_close": false, + "notify_on_close": false, + "root_view_id": "VMHU10V25", + "app_id": "AA4928AQ", + "external_id": "some-unique-id", + "bot_id": "BA13894H" +} \ No newline at end of file diff --git a/tests/data/view_modal_001.json b/tests/data/view_modal_001.json new file mode 100644 index 000000000..8907eb3cd --- /dev/null +++ b/tests/data/view_modal_001.json @@ -0,0 +1,192 @@ +{ + "type": "modal", + "callback_id": "modal-id", + "title": { + "type": "plain_text", + "text": "Workplace check-in", + "emoji": true + }, + "submit": { + "type": "plain_text", + "text": "Submit", + "emoji": true + }, + "close": { + "type": "plain_text", + "text": "Cancel", + "emoji": true + }, + "blocks": [ + { + "type": "section", + "text": { + "type": "plain_text", + "text": ":wave: Hey David!\n\nWe'd love to hear from you how we can make this place the best place you’ve ever worked.", + "emoji": true + } + }, + { + "type": "divider" + }, + { + "type": "input", + "label": { + "type": "plain_text", + "text": "You enjoy working here at Pistachio & Co", + "emoji": true + }, + "element": { + "type": "radio_buttons", + "options": [ + { + "text": { + "type": "plain_text", + "text": "Strongly agree", + "emoji": true + }, + "value": "1" + }, + { + "text": { + "type": "plain_text", + "text": "Agree", + "emoji": true + }, + "value": "2" + }, + { + "text": { + "type": "plain_text", + "text": "Neither agree nor disagree", + "emoji": true + }, + "value": "3" + }, + { + "text": { + "type": "plain_text", + "text": "Disagree", + "emoji": true + }, + "value": "4" + }, + { + "text": { + "type": "plain_text", + "text": "Strongly disagree", + "emoji": true + }, + "value": "5" + } + ] + } + }, + { + "type": "input", + "label": { + "type": "plain_text", + "text": "What do you want for our team weekly lunch?", + "emoji": true + }, + "element": { + "type": "multi_static_select", + "placeholder": { + "type": "plain_text", + "text": "Select your favorites", + "emoji": true + }, + "options": [ + { + "text": { + "type": "plain_text", + "text": ":pizza: Pizza", + "emoji": true + }, + "value": "value-0" + }, + { + "text": { + "type": "plain_text", + "text": ":fried_shrimp: Thai food", + "emoji": true + }, + "value": "value-1" + }, + { + "text": { + "type": "plain_text", + "text": ":desert_island: Hawaiian", + "emoji": true + }, + "value": "value-2" + }, + { + "text": { + "type": "plain_text", + "text": ":meat_on_bone: Texas BBQ", + "emoji": true + }, + "value": "value-3" + }, + { + "text": { + "type": "plain_text", + "text": ":hamburger: Burger", + "emoji": true + }, + "value": "value-4" + }, + { + "text": { + "type": "plain_text", + "text": ":taco: Tacos", + "emoji": true + }, + "value": "value-5" + }, + { + "text": { + "type": "plain_text", + "text": ":green_salad: Salad", + "emoji": true + }, + "value": "value-6" + }, + { + "text": { + "type": "plain_text", + "text": ":stew: Indian", + "emoji": true + }, + "value": "value-7" + } + ] + } + }, + { + "type": "input", + "label": { + "type": "plain_text", + "text": "What can we do to improve your experience working here?", + "emoji": true + }, + "element": { + "type": "plain_text_input", + "multiline": true + } + }, + { + "type": "input", + "label": { + "type": "plain_text", + "text": "Anything else you want to tell us?", + "emoji": true + }, + "element": { + "type": "plain_text_input", + "multiline": true + }, + "optional": true + } + ] +} \ No newline at end of file diff --git a/tests/data/view_modal_002.json b/tests/data/view_modal_002.json new file mode 100644 index 000000000..aa9c7a676 --- /dev/null +++ b/tests/data/view_modal_002.json @@ -0,0 +1,197 @@ +{ + "type": "modal", + "title": { + "type": "plain_text", + "text": "Your accommodation", + "emoji": true + }, + "submit": { + "type": "plain_text", + "text": "Submit", + "emoji": true + }, + "close": { + "type": "plain_text", + "text": "Cancel", + "emoji": true + }, + "blocks": [ + { + "type": "section", + "text": { + "type": "plain_text", + "text": "Please choose an option where you'd like to stay from Oct 21 - Oct 23 (2 nights).", + "emoji": true + } + }, + { + "type": "divider" + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "*Airstream Suite*\n*Share with another person*. Private walk-in bathroom. TV. Heating. Kitchen with microwave, basic cooking utensils, wine glasses and silverware." + }, + "accessory": { + "type": "image", + "image_url": "https://api.slack.com/img/blocks/bkb_template_images/Streamline-Beach.png", + "alt_text": "Airstream Suite" + } + }, + { + "type": "context", + "elements": [ + { + "type": "mrkdwn", + "text": "1x Queen Bed" + }, + { + "type": "mrkdwn", + "text": "|" + }, + { + "type": "mrkdwn", + "text": "$220 / night" + } + ] + }, + { + "type": "actions", + "elements": [ + { + "type": "button", + "text": { + "type": "plain_text", + "text": "Choose", + "emoji": true + }, + "value": "click_me_123" + }, + { + "type": "button", + "text": { + "type": "plain_text", + "text": "View Details", + "emoji": true + }, + "value": "click_me_123" + } + ] + }, + { + "type": "divider" + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "*Redwood Suite*\n*Share with 2 other person*. Studio home. Modern bathroom. TV. Heating. Full kitchen. Patio with lounge chairs and campfire style fire pit and grill." + }, + "accessory": { + "type": "image", + "image_url": "https://api.slack.com/img/blocks/bkb_template_images/redwoodcabin.png", + "alt_text": "Redwood Suite" + } + }, + { + "type": "context", + "elements": [ + { + "type": "mrkdwn", + "text": "1x King Bed" + }, + { + "type": "mrkdwn", + "text": "|" + }, + { + "type": "mrkdwn", + "text": "$350 / night" + } + ] + }, + { + "type": "actions", + "elements": [ + { + "type": "button", + "text": { + "type": "plain_text", + "text": "✓ Your Choice", + "emoji": true + }, + "style": "primary", + "value": "click_me_123" + }, + { + "type": "button", + "text": { + "type": "plain_text", + "text": "View Details", + "emoji": true + }, + "value": "click_me_123" + } + ] + }, + { + "type": "divider" + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "*Luxury Tent*\n*One person only*. Shared modern bathrooms and showers in lounge building. Temperature control with heated blankets. Lights and electrical outlets." + }, + "accessory": { + "type": "image", + "image_url": "https://api.slack.com/img/blocks/bkb_template_images/tent.png", + "alt_text": "Redwood Suite" + } + }, + { + "type": "context", + "elements": [ + { + "type": "mrkdwn", + "text": "1x Queen Bed" + }, + { + "type": "mrkdwn", + "text": "|" + }, + { + "type": "mrkdwn", + "text": "$260 / night" + } + ] + }, + { + "type": "actions", + "elements": [ + { + "type": "button", + "text": { + "type": "plain_text", + "text": "Choose", + "emoji": true + }, + "value": "click_me_123" + }, + { + "type": "button", + "text": { + "type": "plain_text", + "text": "View Details", + "emoji": true + }, + "value": "click_me_123" + } + ] + }, + { + "type": "divider" + } + ] +} \ No newline at end of file diff --git a/tests/data/view_modal_003.json b/tests/data/view_modal_003.json new file mode 100644 index 000000000..ed8bc5b22 --- /dev/null +++ b/tests/data/view_modal_003.json @@ -0,0 +1,144 @@ +{ + "type": "modal", + "title": { + "type": "plain_text", + "text": "App menu", + "emoji": true + }, + "submit": { + "type": "plain_text", + "text": "Submit", + "emoji": true + }, + "close": { + "type": "plain_text", + "text": "Cancel", + "emoji": true + }, + "blocks": [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "*Hi !* Here's how I can help you:" + } + }, + { + "type": "divider" + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": ":calendar: *Create event*\nCreate a new event" + }, + "accessory": { + "type": "button", + "text": { + "type": "plain_text", + "text": "Create event", + "emoji": true + }, + "style": "primary", + "value": "click_me_123" + } + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": ":clipboard: *List of events*\nChoose from different event lists" + }, + "accessory": { + "type": "static_select", + "placeholder": { + "type": "plain_text", + "text": "Choose list", + "emoji": true + }, + "options": [ + { + "text": { + "type": "plain_text", + "text": "My events", + "emoji": true + }, + "value": "value-0" + }, + { + "text": { + "type": "plain_text", + "text": "All events", + "emoji": true + }, + "value": "value-1" + }, + { + "text": { + "type": "plain_text", + "text": "Event invites", + "emoji": true + }, + "value": "value-1" + } + ] + } + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": ":gear: *Settings*\nManage your notifications and team settings" + }, + "accessory": { + "type": "static_select", + "placeholder": { + "type": "plain_text", + "text": "Edit settings", + "emoji": true + }, + "options": [ + { + "text": { + "type": "plain_text", + "text": "Notifications", + "emoji": true + }, + "value": "value-0" + }, + { + "text": { + "type": "plain_text", + "text": "Team settings", + "emoji": true + }, + "value": "value-1" + } + ] + } + }, + { + "type": "actions", + "elements": [ + { + "type": "button", + "text": { + "type": "plain_text", + "text": "Send feedback", + "emoji": true + }, + "value": "click_me_123" + }, + { + "type": "button", + "text": { + "type": "plain_text", + "text": "FAQs", + "emoji": true + }, + "value": "click_me_123" + } + ] + } + ] +} \ No newline at end of file diff --git a/tests/data/view_modal_004.json b/tests/data/view_modal_004.json new file mode 100644 index 000000000..66da3749a --- /dev/null +++ b/tests/data/view_modal_004.json @@ -0,0 +1,70 @@ +{ + "type": "modal", + "title": { + "type": "plain_text", + "text": "Notification settings", + "emoji": true + }, + "submit": { + "type": "plain_text", + "text": "Submit", + "emoji": true + }, + "close": { + "type": "plain_text", + "text": "Cancel", + "emoji": true + }, + "blocks": [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "* posts into *\n\nSelect which notifications to send:" + } + }, + { + "type": "actions", + "elements": [ + { + "type": "checkboxes", + "options": [ + { + "text": { + "type": "plain_text", + "text": "New tasks" + }, + "value": "tasks", + "description": { + "type": "plain_text", + "text": "When new tasks are added to project" + } + }, + { + "text": { + "type": "plain_text", + "text": "New comments" + }, + "value": "comments", + "description": { + "type": "plain_text", + "text": "When new comments are added" + } + }, + { + "text": { + "type": "plain_text", + "text": "Project updates" + }, + "value": "updates", + "description": { + "type": "plain_text", + "text": "When project is updated" + } + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/tests/data/view_modal_005.json b/tests/data/view_modal_005.json new file mode 100644 index 000000000..94589796e --- /dev/null +++ b/tests/data/view_modal_005.json @@ -0,0 +1,169 @@ +{ + "type": "modal", + "title": { + "type": "plain_text", + "text": "Your itinerary", + "emoji": true + }, + "submit": { + "type": "plain_text", + "text": "Submit", + "emoji": true + }, + "close": { + "type": "plain_text", + "text": "Cancel", + "emoji": true + }, + "blocks": [ + { + "type": "section", + "block_id": "1", + "text": { + "type": "mrkdwn", + "text": ":tada: You're all set! This is your booking summary." + } + }, + { + "type": "divider", + "block_id": "2" + }, + { + "type": "section", + "block_id": "3", + "fields": [ + { + "type": "mrkdwn", + "text": "*Attendee*\nKatie Chen" + }, + { + "type": "mrkdwn", + "text": "*Date*\nOct 22-23" + } + ] + }, + { + "type": "context", + "block_id": "4", + "elements": [ + { + "type": "mrkdwn", + "text": ":house: Accommodation" + } + ] + }, + { + "type": "divider", + "block_id": "5" + }, + { + "type": "section", + "block_id": "6", + "text": { + "type": "mrkdwn", + "text": "*Redwood Suite*\n*Share with 2 other person.* Studio home. Modern bathroom. TV. Heating. Full kitchen. Patio with lounge chairs and campfire style fire pit and grill." + }, + "accessory": { + "type": "image", + "image_url": "https://api.slack.com/img/blocks/bkb_template_images/redwood-suite.png", + "alt_text": "Redwood Suite" + } + }, + { + "type": "context", + "block_id": "7", + "elements": [ + { + "type": "mrkdwn", + "text": ":fork_and_knife: Food & Dietary restrictions" + } + ] + }, + { + "type": "divider", + "block_id": "8" + }, + { + "type": "section", + "block_id": "9", + "text": { + "type": "mrkdwn", + "text": "*All-rounder*\nYou eat most meats, seafood, dairy and vegetables." + } + }, + { + "type": "context", + "block_id": "10", + "elements": [ + { + "type": "mrkdwn", + "text": ":woman-running: Activities" + } + ] + }, + { + "type": "divider", + "block_id": "11" + }, + { + "type": "section", + "block_id": "12", + "text": { + "type": "mrkdwn", + "text": "*Winery tour and tasting*" + }, + "fields": [ + { + "type": "plain_text", + "text": "Wednesday, Oct 22 2019, 2pm-5pm", + "emoji": true + }, + { + "type": "plain_text", + "text": "Hosted by Sandra Mullens", + "emoji": true + } + ] + }, + { + "type": "section", + "block_id": "13", + "text": { + "type": "mrkdwn", + "text": "*Sunrise hike to Mount Amazing*" + }, + "fields": [ + { + "type": "plain_text", + "text": "Thursday, Oct 23 2019, 5:30am", + "emoji": true + }, + { + "type": "plain_text", + "text": "Hosted by Jordan Smith", + "emoji": true + } + ] + }, + { + "type": "section", + "block_id": "14", + "text": { + "type": "mrkdwn", + "text": "*Design systems brainstorm*" + }, + "fields": [ + { + "type": "plain_text", + "text": "Thursday, Oct 23 2019, 11a", + "emoji": true + }, + { + "type": "plain_text", + "text": "Hosted by Mary Lee", + "emoji": true + } + ] + } + ] +} \ No newline at end of file diff --git a/tests/data/view_modal_006.json b/tests/data/view_modal_006.json new file mode 100644 index 000000000..9080a7298 --- /dev/null +++ b/tests/data/view_modal_006.json @@ -0,0 +1,424 @@ +{ + "type": "modal", + "title": { + "type": "plain_text", + "text": "Ticket app", + "emoji": true + }, + "submit": { + "type": "plain_text", + "text": "Submit", + "emoji": true + }, + "close": { + "type": "plain_text", + "text": "Cancel", + "emoji": true + }, + "blocks": [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "Pick a ticket list from the dropdown" + }, + "accessory": { + "type": "static_select", + "placeholder": { + "type": "plain_text", + "text": "Select an item", + "emoji": true + }, + "options": [ + { + "text": { + "type": "plain_text", + "text": "All Tickets", + "emoji": true + }, + "value": "all_tickets" + }, + { + "text": { + "type": "plain_text", + "text": "Assigned To Me", + "emoji": true + }, + "value": "assigned_to_me" + }, + { + "text": { + "type": "plain_text", + "text": "Issued By Me", + "emoji": true + }, + "value": "issued_by_me" + } + ], + "initial_option": { + "text": { + "type": "plain_text", + "text": "Assigned To Me", + "emoji": true + }, + "value": "assigned_to_me" + } + } + }, + { + "type": "divider" + }, + { + "type": "context", + "elements": [ + { + "type": "image", + "image_url": "https://api.slack.com/img/blocks/bkb_template_images/highpriority.png", + "alt_text": "High Priority" + }, + { + "type": "mrkdwn", + "text": "*High Priority*" + } + ] + }, + { + "type": "divider" + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "**" + }, + "accessory": { + "type": "overflow", + "options": [ + { + "text": { + "type": "plain_text", + "text": ":white_check_mark: Mark as done", + "emoji": true + }, + "value": "done" + }, + { + "text": { + "type": "plain_text", + "text": ":pencil: Edit", + "emoji": true + }, + "value": "edit" + }, + { + "text": { + "type": "plain_text", + "text": ":x: Delete", + "emoji": true + }, + "value": "delete" + } + ] + } + }, + { + "type": "context", + "elements": [ + { + "type": "mrkdwn", + "text": "Awaiting Release" + }, + { + "type": "image", + "image_url": "https://api.slack.com/img/blocks/bkb_template_images/task-icon.png", + "alt_text": "Task Icon" + }, + { + "type": "mrkdwn", + "text": "Task" + }, + { + "type": "image", + "image_url": "https://api.slack.com/img/blocks/bkb_template_images/profile_1.png", + "alt_text": "Michael Scott" + }, + { + "type": "mrkdwn", + "text": "" + } + ] + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "**" + }, + "accessory": { + "type": "overflow", + "options": [ + { + "text": { + "type": "plain_text", + "text": ":white_check_mark: Mark as done", + "emoji": true + }, + "value": "done" + }, + { + "text": { + "type": "plain_text", + "text": ":pencil: Edit", + "emoji": true + }, + "value": "edit" + }, + { + "text": { + "type": "plain_text", + "text": ":x: Delete", + "emoji": true + }, + "value": "delete" + } + ] + } + }, + { + "type": "context", + "elements": [ + { + "type": "mrkdwn", + "text": "Open" + }, + { + "type": "image", + "image_url": "https://api.slack.com/img/blocks/bkb_template_images/newfeature.png", + "alt_text": "New Feature Icon" + }, + { + "type": "mrkdwn", + "text": "New Feature" + }, + { + "type": "image", + "image_url": "https://api.slack.com/img/blocks/bkb_template_images/profile_2.png", + "alt_text": "Pam Beasely" + }, + { + "type": "mrkdwn", + "text": "" + } + ] + }, + { + "type": "divider" + }, + { + "type": "context", + "elements": [ + { + "type": "image", + "image_url": "https://api.slack.com/img/blocks/bkb_template_images/mediumpriority.png", + "alt_text": "palm tree" + }, + { + "type": "mrkdwn", + "text": "*Medium Priority*" + } + ] + }, + { + "type": "divider" + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "**" + }, + "accessory": { + "type": "overflow", + "options": [ + { + "text": { + "type": "plain_text", + "text": ":white_check_mark: Mark as done", + "emoji": true + }, + "value": "done" + }, + { + "text": { + "type": "plain_text", + "text": ":pencil: Edit", + "emoji": true + }, + "value": "edit" + }, + { + "text": { + "type": "plain_text", + "text": ":x: Delete", + "emoji": true + }, + "value": "delete" + } + ] + } + }, + { + "type": "context", + "elements": [ + { + "type": "mrkdwn", + "text": "Awaiting Release" + }, + { + "type": "image", + "image_url": "https://api.slack.com/img/blocks/bkb_template_images/task-icon.png", + "alt_text": "Task Icon" + }, + { + "type": "mrkdwn", + "text": "Task" + }, + { + "type": "image", + "image_url": "https://api.slack.com/img/blocks/bkb_template_images/profile_1.png", + "alt_text": "Michael Scott" + }, + { + "type": "mrkdwn", + "text": "" + } + ] + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "**" + }, + "accessory": { + "type": "overflow", + "options": [ + { + "text": { + "type": "plain_text", + "text": ":white_check_mark: Mark as done", + "emoji": true + }, + "value": "done" + }, + { + "text": { + "type": "plain_text", + "text": ":pencil: Edit", + "emoji": true + }, + "value": "edit" + }, + { + "text": { + "type": "plain_text", + "text": ":x: Delete", + "emoji": true + }, + "value": "delete" + } + ] + } + }, + { + "type": "context", + "elements": [ + { + "type": "mrkdwn", + "text": "Open" + }, + { + "type": "image", + "image_url": "https://api.slack.com/img/blocks/bkb_template_images/newfeature.png", + "alt_text": "New Feature Icon" + }, + { + "type": "mrkdwn", + "text": "New Feature" + }, + { + "type": "image", + "image_url": "https://api.slack.com/img/blocks/bkb_template_images/profile_2.png", + "alt_text": "Pam Beasely" + }, + { + "type": "mrkdwn", + "text": "" + } + ] + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "**" + }, + "accessory": { + "type": "overflow", + "options": [ + { + "text": { + "type": "plain_text", + "text": ":white_check_mark: Mark as done", + "emoji": true + }, + "value": "done" + }, + { + "text": { + "type": "plain_text", + "text": ":pencil: Edit", + "emoji": true + }, + "value": "edit" + }, + { + "text": { + "type": "plain_text", + "text": ":x: Delete", + "emoji": true + }, + "value": "delete" + } + ] + } + }, + { + "type": "context", + "elements": [ + { + "type": "mrkdwn", + "text": "Awaiting Release" + }, + { + "type": "image", + "image_url": "https://api.slack.com/img/blocks/bkb_template_images/task-icon.png", + "alt_text": "Task Icon" + }, + { + "type": "mrkdwn", + "text": "Task" + }, + { + "type": "image", + "image_url": "https://api.slack.com/img/blocks/bkb_template_images/profile_1.png", + "alt_text": "Michael Scott" + }, + { + "type": "mrkdwn", + "text": "" + } + ] + } + ] +} \ No newline at end of file diff --git a/tests/data/view_modal_007.json b/tests/data/view_modal_007.json new file mode 100644 index 000000000..e8b338654 --- /dev/null +++ b/tests/data/view_modal_007.json @@ -0,0 +1,524 @@ +{ + "type": "modal", + "title": { + "type": "plain_text", + "text": "My App", + "emoji": true + }, + "submit": { + "type": "plain_text", + "text": "Submit", + "emoji": true + }, + "close": { + "type": "plain_text", + "text": "Cancel", + "emoji": true + }, + "blocks": [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "This is a mrkdwn section block :ghost: *this is bold*, and ~this is crossed out~, and " + } + }, + { + "type": "image", + "title": { + "type": "plain_text", + "text": "image1", + "emoji": true + }, + "image_url": "https://api.slack.com/img/blocks/bkb_template_images/beagle.png", + "alt_text": "image1" + }, + { + "type": "context", + "elements": [ + { + "type": "mrkdwn", + "text": "For more info, contact " + } + ] + }, + { + "type": "divider" + }, + { + "type": "actions", + "elements": [ + { + "type": "datepicker", + "initial_date": "1990-04-28", + "placeholder": { + "type": "plain_text", + "text": "Select a date", + "emoji": true + } + }, + { + "type": "datepicker", + "initial_date": "1990-04-28", + "placeholder": { + "type": "plain_text", + "text": "Select a date", + "emoji": true + } + } + ] + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "You can add an image next to text in this block." + }, + "accessory": { + "type": "image", + "image_url": "https://api.slack.com/img/blocks/bkb_template_images/plants.png", + "alt_text": "plants" + } + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "You can add a button alongside text in your message. " + }, + "accessory": { + "type": "button", + "text": { + "type": "plain_text", + "text": "Button", + "emoji": true + }, + "value": "click_me_123" + } + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "Pick an item from the dropdown list" + }, + "accessory": { + "type": "static_select", + "placeholder": { + "type": "plain_text", + "text": "Select an item", + "emoji": true + }, + "options": [ + { + "text": { + "type": "plain_text", + "text": "Choice 1", + "emoji": true + }, + "value": "value-0" + }, + { + "text": { + "type": "plain_text", + "text": "Choice 2", + "emoji": true + }, + "value": "value-1" + }, + { + "text": { + "type": "plain_text", + "text": "Choice 3", + "emoji": true + }, + "value": "value-2" + } + ] + } + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "Pick one or more items from the list" + }, + "accessory": { + "type": "multi_static_select", + "placeholder": { + "type": "plain_text", + "text": "Select items", + "emoji": true + }, + "options": [ + { + "text": { + "type": "plain_text", + "text": "Choice 1", + "emoji": true + }, + "value": "value-0" + }, + { + "text": { + "type": "plain_text", + "text": "Choice 2", + "emoji": true + }, + "value": "value-1" + }, + { + "text": { + "type": "plain_text", + "text": "Choice 3", + "emoji": true + }, + "value": "value-2" + } + ] + } + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "This block has an overflow menu." + }, + "accessory": { + "type": "overflow", + "options": [ + { + "text": { + "type": "plain_text", + "text": "Option 1", + "emoji": true + }, + "value": "value-0" + }, + { + "text": { + "type": "plain_text", + "text": "Option 2", + "emoji": true + }, + "value": "value-1" + }, + { + "text": { + "type": "plain_text", + "text": "Option 3", + "emoji": true + }, + "value": "value-2" + }, + { + "text": { + "type": "plain_text", + "text": "Option 4", + "emoji": true + }, + "value": "value-3" + } + ] + } + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "Pick a date for the deadline." + }, + "accessory": { + "type": "datepicker", + "initial_date": "1990-04-28", + "placeholder": { + "type": "plain_text", + "text": "Select a date", + "emoji": true + } + } + }, + { + "type": "section", + "fields": [ + { + "type": "plain_text", + "text": "*this is plain_text text*", + "emoji": true + }, + { + "type": "plain_text", + "text": "*this is plain_text text*", + "emoji": true + }, + { + "type": "plain_text", + "text": "*this is plain_text text*", + "emoji": true + }, + { + "type": "plain_text", + "text": "*this is plain_text text*", + "emoji": true + }, + { + "type": "plain_text", + "text": "*this is plain_text text*", + "emoji": true + } + ] + }, + { + "type": "section", + "fields": [ + { + "type": "mrkdwn", + "text": "This is a section block with _another_ fields radio button accessory" + }, + { + "type": "mrkdwn", + "text": "This is a section block with *fields* radio button accessory" + }, + { + "type": "mrkdwn", + "text": "This is a section block." + } + ], + "accessory": { + "type": "radio_buttons", + "initial_option": { + "text": { + "type": "plain_text", + "text": "Option 3" + }, + "value": "option 3", + "description": { + "type": "plain_text", + "text": "Description for option 3" + } + }, + "options": [ + { + "text": { + "type": "plain_text", + "text": "Option 1" + }, + "value": "option 1", + "description": { + "type": "plain_text", + "text": "Description for option 1" + } + }, + { + "text": { + "type": "plain_text", + "text": "Option 2" + }, + "value": "option 2", + "description": { + "type": "plain_text", + "text": "Description for option 2" + } + }, + { + "text": { + "type": "plain_text", + "text": "Option 3" + }, + "value": "option 3", + "description": { + "type": "plain_text", + "text": "Description for option 3" + } + } + ] + } + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "This is a section block with checkboxes." + }, + "accessory": { + "type": "checkboxes", + "options": [ + { + "text": { + "type": "plain_text", + "text": "*this is plain_text text*", + "emoji": true + }, + "description": { + "type": "plain_text", + "text": "*this is plain_text text*", + "emoji": true + }, + "value": "value-0" + }, + { + "text": { + "type": "plain_text", + "text": "*this is plain_text text*", + "emoji": true + }, + "description": { + "type": "plain_text", + "text": "*this is plain_text text*", + "emoji": true + }, + "value": "value-1" + }, + { + "text": { + "type": "plain_text", + "text": "*this is plain_text text*", + "emoji": true + }, + "description": { + "type": "plain_text", + "text": "*this is plain_text text*", + "emoji": true + }, + "value": "value-2" + } + ] + } + }, + { + "type": "input", + "element": { + "type": "plain_text_input" + }, + "label": { + "type": "plain_text", + "text": "Label", + "emoji": true + } + }, + { + "type": "input", + "element": { + "type": "plain_text_input", + "multiline": true + }, + "label": { + "type": "plain_text", + "text": "Label", + "emoji": true + } + }, + { + "type": "input", + "element": { + "type": "datepicker", + "initial_date": "1990-04-28", + "placeholder": { + "type": "plain_text", + "text": "Select a date", + "emoji": true + } + }, + "label": { + "type": "plain_text", + "text": "Label", + "emoji": true + } + }, + { + "type": "input", + "element": { + "type": "static_select", + "placeholder": { + "type": "plain_text", + "text": "Select an item", + "emoji": true + }, + "options": [ + { + "text": { + "type": "plain_text", + "text": "*this is plain_text text*", + "emoji": true + }, + "value": "value-0" + }, + { + "text": { + "type": "plain_text", + "text": "*this is plain_text text*", + "emoji": true + }, + "value": "value-1" + }, + { + "text": { + "type": "plain_text", + "text": "*this is plain_text text*", + "emoji": true + }, + "value": "value-2" + } + ] + }, + "label": { + "type": "plain_text", + "text": "Label", + "emoji": true + } + }, + { + "type": "input", + "element": { + "type": "multi_users_select", + "placeholder": { + "type": "plain_text", + "text": "Select users", + "emoji": true + } + }, + "label": { + "type": "plain_text", + "text": "Label", + "emoji": true + } + }, + { + "type": "input", + "element": { + "type": "checkboxes", + "options": [ + { + "text": { + "type": "plain_text", + "text": "*this is plain_text text*", + "emoji": true + }, + "value": "value-0" + }, + { + "text": { + "type": "plain_text", + "text": "*this is plain_text text*", + "emoji": true + }, + "value": "value-1" + }, + { + "text": { + "type": "plain_text", + "text": "*this is plain_text text*", + "emoji": true + }, + "value": "value-2" + } + ] + }, + "label": { + "type": "plain_text", + "text": "Label", + "emoji": true + } + } + ] +} \ No newline at end of file diff --git a/tests/data/view_modal_008.json b/tests/data/view_modal_008.json new file mode 100644 index 000000000..e2999be32 --- /dev/null +++ b/tests/data/view_modal_008.json @@ -0,0 +1,39 @@ +{ + "type": "modal", + "title": { + "type": "plain_text", + "text": "My App", + "emoji": true + }, + "submit": { + "type": "plain_text", + "text": "Submit", + "emoji": true + }, + "close": { + "type": "plain_text", + "text": "Cancel", + "emoji": true + }, + "private_metadata": "something important here", + "blocks": [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "This is a mrkdwn section block :ghost: *this is bold*, and ~this is crossed out~, and " + } + } + ], + "state": { + "values": { + "multi-line": { + "ml-value": { + "type": "plain_text_input", + "value": "This is my example inputted value" + } + } + } + }, + "hash": "156663117.cd33ad1f" +} \ No newline at end of file diff --git a/tests/data/view_modal_009.json b/tests/data/view_modal_009.json new file mode 100644 index 000000000..74640861e --- /dev/null +++ b/tests/data/view_modal_009.json @@ -0,0 +1,46 @@ +{ + "id": "VNM522E2U", + "team_id": "T9M4RL1JM", + "type": "modal", + "title": { + "type": "plain_text", + "text": "Pushed Modal", + "emoji": true + }, + "close": { + "type": "plain_text", + "text": "Back", + "emoji": true + }, + "submit": { + "type": "plain_text", + "text": "Save", + "emoji": true + }, + "blocks": [ + { + "type": "input", + "block_id": "edit_details", + "element": { + "type": "plain_text_input", + "action_id": "detail_input" + }, + "label": { + "type": "plain_text", + "text": "Edit details" + } + } + ], + "private_metadata": "secret", + "callback_id": "view_4", + "external_id": "some-unique-id", + "state": { + "values": {} + }, + "hash": "1569362015.55b5e41b", + "clear_on_close": true, + "notify_on_close": false, + "root_view_id": "VNN729E3U", + "app_id": "AAD3351BQ", + "bot_id": "BADF7A34H" +} \ No newline at end of file diff --git a/tests/data/view_modal_010.json b/tests/data/view_modal_010.json new file mode 100644 index 000000000..b01739874 --- /dev/null +++ b/tests/data/view_modal_010.json @@ -0,0 +1,41 @@ +{ + "id": "VMHU10V25", + "team_id": "T8N4K1JN", + "type": "modal", + "title": { + "type": "plain_text", + "text": "Quite a plain modal" + }, + "submit": { + "type": "plain_text", + "text": "Create" + }, + "blocks": [ + { + "type": "input", + "block_id": "a_block_id", + "label": { + "type": "plain_text", + "text": "A simple label", + "emoji": true + }, + "optional": false, + "element": { + "type": "plain_text_input", + "action_id": "an_action_id" + } + } + ], + "private_metadata": "Shh it is a secret", + "callback_id": "identify_your_modals", + "external_id": "some-unique-id", + "state": { + "values": {} + }, + "hash": "156772938.1827394", + "clear_on_close": false, + "notify_on_close": false, + "root_view_id": "VMHU10V25", + "app_id": "AA4928AQ", + "bot_id": "BA13894H" +} \ No newline at end of file diff --git a/tests/data/web_response_api_test.json b/tests/data/web_response_api_test.json new file mode 100644 index 000000000..47971aa3c --- /dev/null +++ b/tests/data/web_response_api_test.json @@ -0,0 +1,3 @@ +{ + "ok": true +} \ No newline at end of file diff --git a/tests/data/web_response_api_test_false.json b/tests/data/web_response_api_test_false.json new file mode 100644 index 000000000..8cba4747d --- /dev/null +++ b/tests/data/web_response_api_test_false.json @@ -0,0 +1,3 @@ +{ + "ok": false +} \ No newline at end of file diff --git a/tests/data/web_response_channels_list_pagination.json b/tests/data/web_response_channels_list_pagination.json new file mode 100644 index 000000000..9d4b9b847 --- /dev/null +++ b/tests/data/web_response_channels_list_pagination.json @@ -0,0 +1,11 @@ +{ + "ok": true, + "channels": [ + { + "id": "C1" + } + ], + "response_metadata": { + "next_cursor": "has_page2" + } +} \ No newline at end of file diff --git a/tests/data/web_response_channels_list_pagination2.json b/tests/data/web_response_channels_list_pagination2.json new file mode 100644 index 000000000..8709f7938 --- /dev/null +++ b/tests/data/web_response_channels_list_pagination2.json @@ -0,0 +1,11 @@ +{ + "ok": true, + "channels": [ + { + "id": "C2" + } + ], + "response_metadata": { + "next_cursor": "page2" + } +} \ No newline at end of file diff --git a/tests/data/web_response_channels_list_pagination2_page2.json b/tests/data/web_response_channels_list_pagination2_page2.json new file mode 100644 index 000000000..5175f163c --- /dev/null +++ b/tests/data/web_response_channels_list_pagination2_page2.json @@ -0,0 +1,8 @@ +{ + "ok": true, + "channels": [ + { + "id": "C3" + } + ] +} \ No newline at end of file diff --git a/tests/data/web_response_channels_list_pagination_has_page2.json b/tests/data/web_response_channels_list_pagination_has_page2.json new file mode 100644 index 000000000..f34bd4f2c --- /dev/null +++ b/tests/data/web_response_channels_list_pagination_has_page2.json @@ -0,0 +1,11 @@ +{ + "ok": true, + "channels": [ + { + "id": "C2" + } + ], + "response_metadata": { + "next_cursor": "has_page3" + } +} \ No newline at end of file diff --git a/tests/data/web_response_channels_list_pagination_has_page3.json b/tests/data/web_response_channels_list_pagination_has_page3.json new file mode 100644 index 000000000..5175f163c --- /dev/null +++ b/tests/data/web_response_channels_list_pagination_has_page3.json @@ -0,0 +1,8 @@ +{ + "ok": true, + "channels": [ + { + "id": "C3" + } + ] +} \ No newline at end of file diff --git a/tests/data/web_response_conversations_list.json b/tests/data/web_response_conversations_list.json new file mode 100644 index 000000000..e909a1dd1 --- /dev/null +++ b/tests/data/web_response_conversations_list.json @@ -0,0 +1,82 @@ +{ + "ok": true, + "channels": [ + { + "id": "C111", + "name": "general", + "is_channel": true, + "is_group": false, + "is_im": false, + "created": 1421924831, + "is_archived": false, + "is_general": true, + "unlinked": 0, + "name_normalized": "general", + "is_shared": false, + "creator": "U111", + "is_ext_shared": false, + "is_org_shared": false, + "shared_team_ids": [ + "T111" + ], + "pending_shared": [], + "pending_connected_team_ids": [], + "is_pending_ext_shared": false, + "is_member": true, + "is_private": false, + "is_mpim": false, + "topic": { + "value": "", + "creator": "", + "last_set": 0 + }, + "purpose": { + "value": "This channel is for team-wide communication and announcements. All team members are in this channel.", + "creator": "", + "last_set": 0 + }, + "previous_names": [], + "num_members": 8 + }, + { + "id": "C222", + "name": "random", + "is_channel": true, + "is_group": false, + "is_im": false, + "created": 1421924831, + "is_archived": false, + "is_general": false, + "unlinked": 0, + "name_normalized": "random", + "is_shared": false, + "creator": "U111", + "is_ext_shared": false, + "is_org_shared": false, + "shared_team_ids": [ + "T111" + ], + "pending_shared": [], + "pending_connected_team_ids": [], + "is_pending_ext_shared": false, + "is_member": true, + "is_private": false, + "is_mpim": false, + "topic": { + "value": "", + "creator": "", + "last_set": 0 + }, + "purpose": { + "value": "A place for non-work banter, links, articles of interest, humor or anything else which you\u0027d like concentrated in some place other than work-related channels.", + "creator": "", + "last_set": 0 + }, + "previous_names": [], + "num_members": 10 + } + ], + "response_metadata": { + "next_cursor": "" + } +} \ No newline at end of file diff --git a/tests/data/web_response_users_list_pagination.json b/tests/data/web_response_users_list_pagination.json new file mode 100644 index 000000000..11019de02 --- /dev/null +++ b/tests/data/web_response_users_list_pagination.json @@ -0,0 +1,10 @@ +{ + "ok": true, + "members": [ + "Bob", + "cat" + ], + "response_metadata": { + "next_cursor": 1 + } +} \ No newline at end of file diff --git a/tests/data/web_response_users_list_pagination_1.json b/tests/data/web_response_users_list_pagination_1.json new file mode 100644 index 000000000..e9e4ca59e --- /dev/null +++ b/tests/data/web_response_users_list_pagination_1.json @@ -0,0 +1,10 @@ +{ + "ok": true, + "members": [ + "Kevin", + "dog" + ], + "response_metadata": { + "next_cursor": "" + } +} \ No newline at end of file diff --git a/tests/data/web_response_users_setPhoto.json b/tests/data/web_response_users_setPhoto.json new file mode 100644 index 000000000..47971aa3c --- /dev/null +++ b/tests/data/web_response_users_setPhoto.json @@ -0,0 +1,3 @@ +{ + "ok": true +} \ No newline at end of file diff --git "a/tests/data/\346\227\245\346\234\254\350\252\236.txt" "b/tests/data/\346\227\245\346\234\254\350\252\236.txt" new file mode 100644 index 000000000..e83710b95 --- /dev/null +++ "b/tests/data/\346\227\245\346\234\254\350\252\236.txt" @@ -0,0 +1 @@ +日本語の文書です。 \ No newline at end of file diff --git a/tests/helpers.py b/tests/helpers.py new file mode 100644 index 000000000..776aa49c2 --- /dev/null +++ b/tests/helpers.py @@ -0,0 +1,45 @@ +import asyncio +import copy +import os +import sys +from typing import Any + + +def async_test(coro): + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + + def wrapper(*args, **kwargs): + future = coro(*args, **kwargs) + return asyncio.get_event_loop().run_until_complete(future) + + return wrapper + + +def remove_os_env_temporarily() -> dict: + old_env = os.environ.copy() + os.environ.clear() + for key, value in old_env.items(): + if key.startswith("PYTHON_SLACK_SDK_"): + os.environ[key] = value + return old_env + + +def restore_os_env(old_env: dict) -> None: + os.environ.update(old_env) + + +def create_copy(original: Any) -> Any: + if sys.version_info.major == 3 and sys.version_info.minor <= 6: + # NOTE: Unfortunately, copy.deepcopy doesn't work in Python 3.6.5. + # -------------------- + # > rv = reductor(4) + # E TypeError: can't pickle _thread.RLock objects + # ../../.pyenv/versions/3.6.10/lib/python3.6/copy.py:169: TypeError + # -------------------- + # As a workaround, this operation uses shallow copies in Python 3.6. + # If your code modifies the shared data in threads / async functions, race conditions may arise. + # Please consider upgrading Python major version to 3.7+ if you encounter some issues due to this. + return copy.copy(original) + else: + return copy.deepcopy(original) diff --git a/tests/mock_web_api_server/__init__.py b/tests/mock_web_api_server/__init__.py new file mode 100644 index 000000000..afb5761c4 --- /dev/null +++ b/tests/mock_web_api_server/__init__.py @@ -0,0 +1,87 @@ +import asyncio +from http.server import SimpleHTTPRequestHandler +from queue import Queue +import threading +import time +from typing import Type +from unittest import TestCase + +from tests.mock_web_api_server.received_requests import ReceivedRequests +from tests.mock_web_api_server.mock_server_thread import MockServerThread + + +def setup_mock_web_api_server(test: TestCase, handler: Type[SimpleHTTPRequestHandler], port: int = 8888): + test.server_started = threading.Event() + test.received_requests = ReceivedRequests(Queue()) + test.thread = MockServerThread(queue=test.received_requests.queue, test=test, handler=handler, port=port) + test.thread.start() + test.server_started.wait() + + +def cleanup_mock_web_api_server(test: TestCase): + test.thread.stop() + test.thread = None + + +def assert_received_request_count(test: TestCase, path: str, min_count: int, timeout: float = 1): + start_time = time.time() + error = None + while time.time() - start_time < timeout: + try: + received_count = test.received_requests.get(path, 0) + assert ( + received_count == min_count + ), f"Expected {min_count} '{path}' {'requests' if min_count > 1 else 'request'}, but got {received_count}!" + return + except Exception as e: + error = e + # waiting for some requests to be received + time.sleep(0.05) + + if error is not None: + raise error + + +def assert_auth_test_count(test: TestCase, expected_count: int): + assert_received_request_count(test, "/auth.test", expected_count, 0.5) + + +######### +# async # +######### + + +def setup_mock_web_api_server_async(test: TestCase, handler: Type[SimpleHTTPRequestHandler], port: int = 8888): + test.server_started = threading.Event() + test.received_requests = ReceivedRequests(asyncio.Queue()) + test.thread = MockServerThread(queue=test.received_requests.queue, test=test, handler=handler, port=port) + test.thread.start() + test.server_started.wait() + + +def cleanup_mock_web_api_server_async(test: TestCase): + test.thread.stop_unsafe() + test.thread = None + + +async def assert_received_request_count_async(test: TestCase, path: str, min_count: int, timeout: float = 1): + start_time = time.time() + error = None + while time.time() - start_time < timeout: + try: + received_count = await test.received_requests.get_async(path, 0) + assert ( + received_count == min_count + ), f"Expected {min_count} '{path}' {'requests' if min_count > 1 else 'request'}, but got {received_count}!" + return + except Exception as e: + error = e + # waiting for mock_received_requests updates + await asyncio.sleep(0.05) + + if error is not None: + raise error + + +async def assert_auth_test_count_async(test: TestCase, expected_count: int): + await assert_received_request_count_async(test, "/auth.test", expected_count, 0.5) diff --git a/tests/mock_web_api_server/mock_server_thread.py b/tests/mock_web_api_server/mock_server_thread.py new file mode 100644 index 000000000..0888cc4ea --- /dev/null +++ b/tests/mock_web_api_server/mock_server_thread.py @@ -0,0 +1,41 @@ +from asyncio import Queue +import asyncio +from http.server import HTTPServer, SimpleHTTPRequestHandler +import threading +from typing import Type, Union +from unittest import TestCase + + +class MockServerThread(threading.Thread): + def __init__( + self, queue: Union[Queue, asyncio.Queue], test: TestCase, handler: Type[SimpleHTTPRequestHandler], port: int = 8888 + ): + threading.Thread.__init__(self) + self.handler = handler + self.test = test + self.queue = queue + self.port = port + + def run(self): + self.server = HTTPServer(("localhost", self.port), self.handler) + self.server.queue = self.queue + self.test.server_url = f"http://localhost:{str(self.port)}" + self.test.host, self.test.port = self.server.socket.getsockname() + self.test.server_started.set() # threading.Event() + + self.test = None + try: + self.server.serve_forever(0.05) + finally: + self.server.server_close() + + def stop(self): + with self.server.queue.mutex: + del self.server.queue + self.server.shutdown() + self.join() + + def stop_unsafe(self): + del self.server.queue + self.server.shutdown() + self.join() diff --git a/tests/mock_web_api_server/received_requests.py b/tests/mock_web_api_server/received_requests.py new file mode 100644 index 000000000..a146f5e1f --- /dev/null +++ b/tests/mock_web_api_server/received_requests.py @@ -0,0 +1,21 @@ +import asyncio +from queue import Queue +from typing import Optional, Union + + +class ReceivedRequests: + def __init__(self, queue: Union[Queue, asyncio.Queue]): + self.queue = queue + self.received_requests: dict = {} + + def get(self, key: str, default: Optional[int] = None) -> Optional[int]: + while not self.queue.empty(): + path = self.queue.get() + self.received_requests[path] = self.received_requests.get(path, 0) + 1 + return self.received_requests.get(key, default) + + async def get_async(self, key: str, default: Optional[int] = None) -> Optional[int]: + while not self.queue.empty(): + path = await self.queue.get() + self.received_requests[path] = self.received_requests.get(path, 0) + 1 + return self.received_requests.get(key, default) diff --git a/tests/rtm/mock_web_api_server.py b/tests/rtm/mock_web_api_server.py new file mode 100644 index 000000000..9d03ac837 --- /dev/null +++ b/tests/rtm/mock_web_api_server.py @@ -0,0 +1,97 @@ +import json +import logging +import threading +from http import HTTPStatus +from http.server import HTTPServer, SimpleHTTPRequestHandler +from typing import Type +from unittest import TestCase + + +class MockHandler(SimpleHTTPRequestHandler): + protocol_version = "HTTP/1.1" + default_request_version = "HTTP/1.1" + logger = logging.getLogger(__name__) + + def is_valid_token(self): + return "authorization" in self.headers and str(self.headers["authorization"]).startswith("Bearer xoxb-") + + def is_invalid_rtm_start(self): + return ( + "authorization" in self.headers + and str(self.headers["authorization"]).startswith("Bearer xoxb-rtm.start") + and str(self.path) != "/rtm.start" + ) + + def set_common_headers(self): + self.send_header("content-type", "application/json;charset=utf-8") + self.send_header("connection", "close") + self.end_headers() + + rtm_start_success = { + "ok": True, + "url": "ws://localhost:8765", + "self": {"id": "U01234ABC", "name": "robotoverlord"}, + "team": { + "domain": "exampledomain", + "id": "T123450FP", + "name": "ExampleName", + }, + } + + rtm_start_failure = { + "ok": False, + "error": "invalid_auth", + } + + def _handle(self): + if self.is_invalid_rtm_start(): + self.send_response(HTTPStatus.BAD_REQUEST) + self.set_common_headers() + return + + self.send_response(HTTPStatus.OK) + self.set_common_headers() + body = self.rtm_start_success if self.is_valid_token() else self.rtm_start_failure + self.wfile.write(json.dumps(body).encode("utf-8")) + self.wfile.close() + + def do_GET(self): + self._handle() + + def do_POST(self): + self._handle() + + +class MockServerThread(threading.Thread): + def __init__(self, test: TestCase, handler: Type[SimpleHTTPRequestHandler] = MockHandler): + threading.Thread.__init__(self) + self.handler = handler + self.test = test + + def run(self): + self.server = HTTPServer(("localhost", 8888), self.handler) + self.test.server_url = "http://localhost:8888" + self.test.host, self.test.port = self.server.socket.getsockname() + self.test.server_started.set() # threading.Event() + + self.test = None + try: + self.server.serve_forever(0.05) + finally: + self.server.server_close() + + def stop(self): + self.server.shutdown() + self.join() + + +def setup_mock_web_api_server(test: TestCase): + test.server_started = threading.Event() + test.thread = MockServerThread(test) + test.thread.start() + test.server_started.wait() + + +def cleanup_mock_web_api_server(test: TestCase): + test.thread.stop() + test.thread = None diff --git a/tests/rtm/test_rtm_client.py b/tests/rtm/test_rtm_client.py new file mode 100644 index 000000000..ece3e57ac --- /dev/null +++ b/tests/rtm/test_rtm_client.py @@ -0,0 +1,91 @@ +import asyncio +import collections +import unittest + +import slack +import slack.errors as e +from tests.rtm.mock_web_api_server import ( + setup_mock_web_api_server, + cleanup_mock_web_api_server, +) + + +class TestRTMClient(unittest.TestCase): + def setUp(self): + setup_mock_web_api_server(self) + self.client = slack.RTMClient(token="xoxp-1234", base_url="http://localhost:8888", auto_reconnect=False) + + def tearDown(self): + cleanup_mock_web_api_server(self) + slack.RTMClient._callbacks = collections.defaultdict(list) + + def test_run_on_returns_callback(self): + @slack.RTMClient.run_on(event="message") + def fn_used_elsewhere(**_unused_payload): + pass + + self.assertIsNotNone(fn_used_elsewhere) + self.assertEqual(fn_used_elsewhere.__name__, "fn_used_elsewhere") + + def test_run_on_annotation_sets_callbacks(self): + @slack.RTMClient.run_on(event="message") + def say_run_on(**payload): + pass + + self.assertTrue(self.client._callbacks["message"][0].__name__ == "say_run_on") + + def test_on_sets_callbacks(self): + def say_on(**payload): + pass + + self.client.on(event="message", callback=say_on) + self.assertTrue(self.client._callbacks["message"][0].__name__ == "say_on") + + def test_on_accepts_a_list_of_callbacks(self): + def say_on(**payload): + pass + + def say_off(**payload): + pass + + self.client.on(event="message", callback=[say_on, say_off]) + self.assertEqual(len(self.client._callbacks["message"]), 2) + + def test_on_raises_when_not_callable(self): + invalid_callback = "a" + + with self.assertRaises(e.SlackClientError) as context: + self.client.on(event="message", callback=invalid_callback) + + expected_error = "The specified callback 'a' is not callable." + error = str(context.exception) + self.assertIn(expected_error, error) + + def test_on_raises_when_kwargs_not_accepted(self): + def invalid_cb(): + pass + + with self.assertRaises(e.SlackClientError) as context: + self.client.on(event="message", callback=invalid_cb) + + expected_error = "The callback 'invalid_cb' must accept keyword arguments (**kwargs)." + error = str(context.exception) + self.assertIn(expected_error, error) + + def test_send_over_websocket_raises_when_not_connected(self): + with self.assertRaises(e.SlackClientError) as context: + loop = asyncio.get_event_loop() + loop.run_until_complete(self.client.send_over_websocket(payload={})) + + expected_error = "Websocket connection is closed." + error = str(context.exception) + self.assertIn(expected_error, error) + + def test_start_raises_an_error_if_rtm_ws_url_is_not_returned(self): + with self.assertRaises(e.SlackApiError) as context: + slack.RTMClient(token="xoxp-1234", auto_reconnect=False).start() + + expected_error = ( + "The request to the Slack API failed.\n" "The server responded with: {'ok': False, 'error': 'invalid_auth'}" + ) + self.assertIn(expected_error, str(context.exception)) diff --git a/tests/rtm/test_rtm_client_functional.py b/tests/rtm/test_rtm_client_functional.py new file mode 100644 index 000000000..a7323342b --- /dev/null +++ b/tests/rtm/test_rtm_client_functional.py @@ -0,0 +1,290 @@ +import asyncio +import collections +import unittest + +from aiohttp import web, WSCloseCode + +import slack +import slack.errors as e +from tests.helpers import async_test +from tests.rtm.mock_web_api_server import ( + setup_mock_web_api_server, + cleanup_mock_web_api_server, +) + + +class TestRTMClientFunctional(unittest.TestCase): + def setUp(self): + setup_mock_web_api_server(self) + + self.loop = asyncio.new_event_loop() + asyncio.set_event_loop(self.loop) + task = asyncio.ensure_future(self.mock_server(), loop=self.loop) + self.loop.run_until_complete(asyncio.wait_for(task, 0.1)) + + self.client = slack.RTMClient( + token="xoxb-valid", + base_url="http://localhost:8765", + auto_reconnect=False, + run_async=False, + ) + self.client._web_client = slack.WebClient( + token="xoxb-valid", + base_url="http://localhost:8888", + run_async=False, + ) + + def tearDown(self): + self.loop.run_until_complete(self.site.stop()) + cleanup_mock_web_api_server(self) + if self.client: + # self.client.stop() + + # If you see the following errors with #stop() method calls, call `RTMClient#async_stop()` instead + # + # /python3.8/asyncio/base_events.py:641: + # RuntimeWarning: coroutine 'ClientWebSocketResponse.close' was never awaited self._ready.clear() + # + self.client._event_loop.run_until_complete(self.client.async_stop()) + + slack.RTMClient._callbacks = collections.defaultdict(list) + + # ------------------------------------------- + + async def mock_server(self): + app = web.Application() + app["websockets"] = [] + app.router.add_get("/", self.websocket_handler) + app.on_shutdown.append(self.on_shutdown) + runner = web.AppRunner(app) + await runner.setup() + self.site = web.TCPSite(runner, "localhost", 8765) + await self.site.start() + + async def websocket_handler(self, request): + ws = web.WebSocketResponse() + await ws.prepare(request) + + request.app["websockets"].append(ws) + try: + async for msg in ws: + await ws.send_json({"type": "message", "message_sent": msg.json()}) + finally: + request.app["websockets"].remove(ws) + return ws + + async def on_shutdown(self, app): + for ws in set(app["websockets"]): + await ws.close(code=WSCloseCode.GOING_AWAY, message="Server shutdown") + + # ------------------------------------------- + + def test_client_auto_reconnects_if_connection_randomly_closes(self): + @slack.RTMClient.run_on(event="open") + def stop_on_open(**payload): + rtm_client = payload["rtm_client"] + + if rtm_client._connection_attempts == 1: + rtm_client._close_websocket() + else: + self.assertEqual(rtm_client._connection_attempts, 2) + rtm_client.stop() + + self.client.auto_reconnect = True + self.client.start() + + def test_client_auto_reconnects_if_an_error_is_thrown(self): + @slack.RTMClient.run_on(event="open") + def stop_on_open(**payload): + rtm_client = payload["rtm_client"] + + if rtm_client._connection_attempts == 1: + raise e.SlackApiError("Test Error", {"headers": {"Retry-After": 0.001}}) + else: + self.assertEqual(rtm_client._connection_attempts, 2) + rtm_client.stop() + + self.client.auto_reconnect = True + self.client.start() + + def test_open_event_receives_expected_arguments(self): + @slack.RTMClient.run_on(event="open") + def stop_on_open(**payload): + self.assertIsInstance(payload["data"], dict) + self.assertIsInstance(payload["web_client"], slack.WebClient) + rtm_client = payload["rtm_client"] + self.assertIsInstance(rtm_client, slack.RTMClient) + rtm_client.stop() + + self.client.start() + + def test_stop_closes_websocket(self): + @slack.RTMClient.run_on(event="open") + def stop_on_open(**payload): + self.assertFalse(self.client._websocket.closed) + + rtm_client = payload["rtm_client"] + rtm_client.stop() + + self.client.start() + self.assertIsNone(self.client._websocket) + + def test_start_calls_rtm_connect_by_default(self): + @slack.RTMClient.run_on(event="open") + def stop_on_open(**payload): + self.assertFalse(self.client._websocket.closed) + rtm_client = payload["rtm_client"] + rtm_client.stop() + + self.client.start() + + def test_start_calls_rtm_start_when_specified(self): + @slack.RTMClient.run_on(event="open") + def stop_on_open(**payload): + self.assertFalse(self.client._websocket.closed) + rtm_client = payload["rtm_client"] + rtm_client.stop() + + self.client.token = "xoxb-rtm.start" + self.client.connect_method = "rtm.start" + self.client.start() + + def test_send_over_websocket_sends_expected_message(self): + @slack.RTMClient.run_on(event="open") + def echo_message(**payload): + rtm_client = payload["rtm_client"] + message = { + "id": 1, + "type": "message", + "channel": "C024BE91L", + "text": "Hello world", + } + rtm_client.send_over_websocket(payload=message) + + @slack.RTMClient.run_on(event="message") + def check_message(**payload): + message = { + "id": 1, + "type": "message", + "channel": "C024BE91L", + "text": "Hello world", + } + rtm_client = payload["rtm_client"] + self.assertDictEqual(payload["data"]["message_sent"], message) + rtm_client.stop() + + self.client.start() + + def test_ping_sends_expected_message(self): + @slack.RTMClient.run_on(event="open") + async def ping_message(**payload): + rtm_client = payload["rtm_client"] + await rtm_client.ping() + + @slack.RTMClient.run_on(event="message") + def check_message(**payload): + message = {"id": 1, "type": "ping"} + rtm_client = payload["rtm_client"] + self.assertDictEqual(payload["data"]["message_sent"], message) + rtm_client.stop() + + self.client.start() + + def test_typing_sends_expected_message(self): + @slack.RTMClient.run_on(event="open") + async def typing_message(**payload): + rtm_client = payload["rtm_client"] + await rtm_client.typing(channel="C01234567") + + @slack.RTMClient.run_on(event="message") + def check_message(**payload): + message = {"id": 1, "type": "typing", "channel": "C01234567"} + rtm_client = payload["rtm_client"] + self.assertDictEqual(payload["data"]["message_sent"], message) + rtm_client.stop() + + self.client.start() + + def test_on_error_callbacks(self): + @slack.RTMClient.run_on(event="open") + def raise_an_error(**payload): + raise e.SlackClientNotConnectedError("Testing error handling.") + + self.called = False + + @slack.RTMClient.run_on(event="error") + def error_callback(**payload): + self.called = True + + with self.assertRaises(e.SlackClientNotConnectedError): + self.client.start() + self.assertTrue(self.called) + + def test_callback_errors_are_raised(self): + @slack.RTMClient.run_on(event="open") + def raise_an_error(**payload): + raise Exception("Testing error handling.") + + with self.assertRaises(Exception) as context: + self.client.start() + + expected_error = "Testing error handling." + self.assertIn(expected_error, str(context.exception)) + + def test_on_close_callbacks(self): + @slack.RTMClient.run_on(event="open") + def stop_on_open(**payload): + payload["rtm_client"].stop() + + self.called = False + + @slack.RTMClient.run_on(event="close") + def assert_on_close(**payload): + self.called = True + + self.client.start() + self.assertTrue(self.called) + + @async_test + async def test_run_async_valid(self): + client = slack.RTMClient( + token="xoxb-valid", + base_url="http://localhost:8765", + run_async=True, + ) + client._web_client = slack.WebClient( + token="xoxb-valid", + base_url="http://localhost:8888", + run_async=True, + ) + self.called = False + + @slack.RTMClient.run_on(event="open") + async def handle_open_event(**payload): + self.called = True + + client.start() # intentionally no await here + await asyncio.sleep(3) + self.assertTrue(self.called) + + @async_test + async def test_run_async_invalid(self): + client = slack.RTMClient( + token="xoxb-valid", + base_url="http://localhost:8765", + run_async=True, + ) + client._web_client = slack.WebClient( + token="xoxb-valid", + base_url="http://localhost:8888", + run_async=True, + ) + self.called = False + + @slack.RTMClient.run_on(event="open") + def handle_open_event(**payload): + self.called = True + + client.start() # intentionally no await here + await asyncio.sleep(3) + self.assertFalse(self.called) diff --git a/tests/rtm/test_rtm_client_v2.py b/tests/rtm/test_rtm_client_v2.py new file mode 100644 index 000000000..5f1a34bf5 --- /dev/null +++ b/tests/rtm/test_rtm_client_v2.py @@ -0,0 +1,99 @@ +import unittest + +from slack_sdk.rtm.v2 import RTMClient +from slack_sdk import errors as e +from tests.rtm.mock_web_api_server import ( + setup_mock_web_api_server, + cleanup_mock_web_api_server, +) + + +class TestRTMClient(unittest.TestCase): + def setUp(self): + setup_mock_web_api_server(self) + self.rtm = RTMClient( + token="xoxp-1234", + base_url="http://localhost:8888", + auto_reconnect_enabled=False, + ) + + def tearDown(self): + cleanup_mock_web_api_server(self) + + def test_run_on_returns_callback(self): + def fn1(client, payload): + pass + + @self.rtm.on("message") + def fn2(client, payload): + pass + + self.assertIsNotNone(fn1) + self.assertIsNotNone(fn2) + self.assertEqual(fn2.__name__, "fn2") + + def test_run_on_annotation_sets_callbacks(self): + @self.rtm.on("message") + def say_run_on(client, payload): + pass + + self.assertTrue(len(self.rtm.message_listeners) == 2) + + def test_on_sets_callbacks(self): + def say_on(client, payload): + pass + + self.rtm.on("message")(say_on) + self.assertTrue(len(self.rtm.message_listeners) == 2) + + def test_on_accepts_a_list_of_callbacks(self): + def say_on(client, payload): + pass + + def say_off(client, payload): + pass + + self.rtm.on("message")(say_on) + self.rtm.on("message")(say_off) + self.assertEqual(len(self.rtm.message_listeners), 3) + + def test_on_raises_when_not_callable(self): + invalid_callback = "a" + + with self.assertRaises(e.SlackClientError) as context: + self.rtm.on("message")(invalid_callback) + + expected_error = "The listener 'a' is not a Callable (actual: str)" + error = str(context.exception) + self.assertIn(expected_error, error) + + def test_on_raises_when_kwargs_not_accepted(self): + def invalid_cb(): + pass + + with self.assertRaises(e.SlackClientError) as context: + self.rtm.on("message")(invalid_cb) + + error = str(context.exception) + self.assertIn( + "The listener 'invalid_cb' must accept two args: client, event (actual: )", + error, + ) + + def test_send_over_websocket_raises_when_not_connected(self): + with self.assertRaises(e.SlackClientError) as context: + self.rtm.send(payload={}) + + expected_error = "The RTM client is not connected to the Slack servers" + error = str(context.exception) + self.assertIn(expected_error, error) + + def test_start_raises_an_error_if_rtm_ws_url_is_not_returned(self): + with self.assertRaises(e.SlackApiError) as context: + RTMClient(token="xoxp-1234", auto_reconnect_enabled=False).start() + + expected_error = ( + "The request to the Slack API failed. (url: https://slack.com/api/auth.test)\n" + "The server responded with: {'ok': False, 'error': 'invalid_auth'}" + ) + self.assertIn(expected_error, str(context.exception)) diff --git a/tests/signature/test_signature_verifier.py b/tests/signature/test_signature_verifier.py new file mode 100644 index 000000000..ef7ac0512 --- /dev/null +++ b/tests/signature/test_signature_verifier.py @@ -0,0 +1,99 @@ +import unittest + +from slack.signature import SignatureVerifier + + +class MockClock: + def now(self) -> float: + return 1531420618 + + +class TestSignatureVerifier(unittest.TestCase): + def setUp(self): + pass + + def tearDown(self): + pass + + # https://docs.slack.dev/authentication/verifying-requests-from-slack/ + signing_secret = "8f742231b10e8888abcd99yyyzzz85a5" + + body = "token=xyzz0WbapA4vBCDEFasx0q6G&team_id=T1DC2JH3J&team_domain=testteamnow&channel_id=G8PSS9T3V&channel_name=foobar&user_id=U2CERLKJA&user_name=roadrunner&command=%2Fwebhook-collect&text=&response_url=https%3A%2F%2Fhooks.slack.com%2Fcommands%2FT1DC2JH3J%2F397700885554%2F96rGlfmibIGlgcZRskXaIFfN&trigger_id=398738663015.47445629121.803a0bc887a14d10d2c447fce8b6703c" + + timestamp = "1531420618" + valid_signature = "v0=a2114d57b48eac39b9ad189dd8316235a7b4a8d21a10bd27519666489c69b503" + + headers = { + "X-Slack-Request-Timestamp": timestamp, + "X-Slack-Signature": valid_signature, + } + + def test_generate_signature(self): + verifier = SignatureVerifier(self.signing_secret) + signature = verifier.generate_signature(timestamp=self.timestamp, body=self.body) + self.assertEqual(self.valid_signature, signature) + + def test_generate_signature_body_as_bytes(self): + verifier = SignatureVerifier(self.signing_secret) + signature = verifier.generate_signature(timestamp=self.timestamp, body=self.body.encode("utf-8")) + self.assertEqual(self.valid_signature, signature) + + def test_is_valid_request(self): + verifier = SignatureVerifier(signing_secret=self.signing_secret, clock=MockClock()) + self.assertTrue(verifier.is_valid_request(self.body, self.headers)) + + def test_is_valid_request_body_as_bytes(self): + verifier = SignatureVerifier(signing_secret=self.signing_secret, clock=MockClock()) + self.assertTrue(verifier.is_valid_request(self.body.encode("utf-8"), self.headers)) + + def test_is_valid_request_invalid_body(self): + verifier = SignatureVerifier( + signing_secret=self.signing_secret, + clock=MockClock(), + ) + modified_body = self.body + "------" + self.assertFalse(verifier.is_valid_request(modified_body, self.headers)) + + def test_is_valid_request_invalid_body_as_bytes(self): + verifier = SignatureVerifier( + signing_secret=self.signing_secret, + clock=MockClock(), + ) + modified_body = self.body + "------" + self.assertFalse(verifier.is_valid_request(modified_body.encode("utf-8"), self.headers)) + + def test_is_valid_request_expiration(self): + verifier = SignatureVerifier( + signing_secret=self.signing_secret, + ) + self.assertFalse(verifier.is_valid_request(self.body, self.headers)) + + def test_is_valid_request_none(self): + verifier = SignatureVerifier( + signing_secret=self.signing_secret, + clock=MockClock(), + ) + self.assertFalse(verifier.is_valid_request(None, self.headers)) + self.assertFalse(verifier.is_valid_request(self.body, None)) + self.assertFalse(verifier.is_valid_request(None, None)) + + def test_is_valid(self): + verifier = SignatureVerifier( + signing_secret=self.signing_secret, + clock=MockClock(), + ) + self.assertTrue(verifier.is_valid(self.body, self.timestamp, self.valid_signature)) + self.assertTrue(verifier.is_valid(self.body, 1531420618, self.valid_signature)) + + def test_is_valid_none(self): + verifier = SignatureVerifier( + signing_secret=self.signing_secret, + clock=MockClock(), + ) + self.assertFalse(verifier.is_valid(None, self.timestamp, self.valid_signature)) + self.assertFalse(verifier.is_valid(self.body, None, self.valid_signature)) + self.assertFalse(verifier.is_valid(self.body, self.timestamp, None)) + self.assertFalse(verifier.is_valid(None, None, self.valid_signature)) + self.assertFalse(verifier.is_valid(None, self.timestamp, None)) + self.assertFalse(verifier.is_valid(self.body, None, None)) + self.assertFalse(verifier.is_valid(None, None, None)) diff --git a/tests/slack_sdk/__init__.py b/tests/slack_sdk/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/slack_sdk/audit_logs/__init__.py b/tests/slack_sdk/audit_logs/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/slack_sdk/audit_logs/mock_web_api_handler.py b/tests/slack_sdk/audit_logs/mock_web_api_handler.py new file mode 100644 index 000000000..7feae4f3a --- /dev/null +++ b/tests/slack_sdk/audit_logs/mock_web_api_handler.py @@ -0,0 +1,93 @@ +import json +import logging +import re +import time +from http import HTTPStatus +from http.server import SimpleHTTPRequestHandler + + +class MockHandler(SimpleHTTPRequestHandler): + protocol_version = "HTTP/1.1" + default_request_version = "HTTP/1.1" + logger = logging.getLogger(__name__) + + pattern_for_language = re.compile("python/(\\S+)", re.IGNORECASE) + pattern_for_package_identifier = re.compile("slackclient/(\\S+)") + + def is_valid_user_agent(self): + user_agent = self.headers["User-Agent"] + return self.pattern_for_language.search(user_agent) and self.pattern_for_package_identifier.search(user_agent) + + def set_common_headers(self): + self.send_header("content-type", "application/json;charset=utf-8") + self.send_header("connection", "close") + self.end_headers() + + def do_GET(self): + # put_nowait is common between Queue & asyncio.Queue, it does not need to be awaited + self.server.queue.put_nowait(self.path) + if self.path == "/received_requests.json": + self.send_response(200) + self.set_common_headers() + self.wfile.write(json.dumps(self.received_requests).encode("utf-8")) + return + + header = self.headers["Authorization"] + if header is not None and "xoxp-" in header: + pattern = str(header).split("xoxp-", 1)[1] + if "remote_disconnected" in pattern: + # http.client.RemoteDisconnected + self.finish() + return + if "ratelimited" in pattern: + self.send_response(429) + self.send_header("retry-after", 1) + self.set_common_headers() + self.wfile.write("""{"ok": false, "error": "ratelimited"}""".encode("utf-8")) + return + + try: + if self.path == "/error": + self.send_response(500) + self.set_common_headers() + self.wfile.write("unexpected response body".encode("utf-8")) + return + + if self.path == "/timeout": + time.sleep(2) + + # user-agent-this_is-test + if self.path.startswith("/user-agent-"): + elements = self.path.split("-") + prefix, suffix = elements[2], elements[-1] + ua: str = self.headers["User-Agent"] + if ua.startswith(prefix) and ua.endswith(suffix): + self.send_response(HTTPStatus.OK) + self.set_common_headers() + self.wfile.write("ok".encode("utf-8")) + self.wfile.close() + return + else: + self.send_response(HTTPStatus.BAD_REQUEST) + self.set_common_headers() + self.wfile.write("invalid user agent".encode("utf-8")) + self.wfile.close() + return + + body = "{}" + + if self.path.startswith("/logs"): + body = """{"entries":[{"id":"xxx-yyy-zzz-111","date_create":1611221649,"action":"user_login","actor":{"type":"user","user":{"id":"W111","name":"your name","email":"foo@example.com","team":"E111"}},"entity":{"type":"user","user":{"id":"W111","name":"your name","email":"foo@example.com","team":"E111"}},"context":{"location":{"type":"workspace","id":"T111","name":"WS","domain":"foo-bar-baz"},"ua":"UA","ip_address":"1.2.3.4","session_id":1656410836837}},{"id":"32c68de4-cbfa-4fcb-9780-25fdd5aacf32","date_create":1611221649,"action":"user_login","actor":{"type":"user","user":{"id":"W111","name":"your name","email":"foo@example.com","team":"E111"}},"entity":{"type":"user","user":{"id":"W111","name":"your name","email":"foo@example.com","team":"E111"}},"context":{"location":{"type":"workspace","id":"T111","name":"WS","domain":"foo-bar-baz"},"ua":"UA","ip_address":"1.2.3.4","session_id":1656410836837}}],"response_metadata":{"next_cursor":"xxx"}}""" + if self.path == "/schemas": + body = """{"schemas":[{"type":"workspace","workspace":{"id":"string","name":"string","domain":"string"}},{"type":"enterprise","enterprise":{"id":"string","name":"string","domain":"string"}},{"type":"user","user":{"id":"string","name":"string","email":"string","team":"string"}},{"type":"file","file":{"id":"string","name":"string","filetype":"string","title":"string"}},{"type":"channel","channel":{"id":"string","name":"string","privacy":"string","is_shared":"bool","is_org_shared":"bool","teams_shared_with":"Optional: varray"}},{"type":"app","app":{"id":"string","name":"string","is_distributed":"bool","is_directory_approved":"bool","is_workflow_app":"bool","scopes":"array"}},{"type":"workflow","workflow":{"id":"string","name":"string"}},{"type":"barrier","barrier":{"id":"string","primary_usergroup":"string","barriered_from_usergroup":"string"}},{"type":"message","message":{"team":"string","channel":"string","timestamp":"string"}}]}""" + if self.path == "/actions": + body = """{"actions":{"workspace_or_org":["workspace_created","workspace_deleted","organization_created","organization_deleted","organization_renamed","organization_domain_changed","organization_accepted_migration","organization_declined_migration","emoji_added","emoji_removed","emoji_aliased","emoji_renamed","billing_address_added","migration_scheduled","workspace_accepted_migration","workspace_declined_migration","migration_completed","migration_dms_mpdms_completed","corporate_exports_approved","corporate_exports_enabled","manual_export_started","manual_export_completed","manual_export_downloaded","manual_export_deleted","scheduled_export_started","scheduled_export_completed","scheduled_export_downloaded","scheduled_export_deleted","channels_export_started","channels_export_completed","channels_export_downloaded","channels_export_deleted","manual_user_export_started","manual_user_export_completed","manual_user_export_downloaded","manual_user_export_deleted","ekm_enrolled","ekm_unenrolled","ekm_key_added","ekm_key_removed","ekm_clear_cache_set","ekm_logging_config_set","ekm_slackbot_enroll_notification_sent","ekm_slackbot_unenroll_notification_sent","ekm_slackbot_rekey_notification_sent","ekm_slackbot_logging_notification_sent","approved_orgs_added","approved_orgs_removed","organization_verified","organization_unverified","organization_public_url_updated","pref.admin_retention_override_changed","pref.allow_calls","pref.dlp_access_changed","pref.allow_message_deletion","pref.retention_override_changed","pref.app_dir_only","pref.app_whitelist_enabled","pref.block_file_download_for_unapproved_ip","pref.can_receive_shared_channels_invites","pref.commands_only_regular","pref.custom_tos","pref.disallow_public_file_urls","pref.display_real_names","pref.dm_retention_changed","pref.dnd_enabled","pref.dnd_end_hour","pref.dnd_start_hour","pref.emoji_only_admins","pref.ent_required_browser","pref.enterprise_default_channels","pref.block_download_and_copy_on_untrusted_mobile","pref.enterprise_mobile_device_check","pref.enterprise_team_creation_request","pref.file_retention_changed","pref.private_channel_retention_changed","pref.hide_referers","pref.loading_only_admins","pref.mobile_secondary_auth_timeout_changed","pref.msg_edit_window_mins","pref.notification_redaction_type","pref.required_minimum_mobile_version_changed","pref.public_channel_retention_changed","pref.session_duration_changed","pref.session_duration_type_changed","pref.sign_in_with_slack_disabled","pref.slackbot_responses_disabled","pref.slackbot_responses_only_admins","pref.stats_only_admins","pref.two_factor_auth_changed","pref.username_policy","pref.who_can_archive_channels","pref.who_can_create_public_channels","pref.who_can_create_delete_user_groups","pref.who_can_create_private_channels","pref.who_can_edit_user_groups","pref.who_can_remove_from_public_channels","pref.who_can_remove_from_private_channels","pref.who_can_manage_channel_posting_prefs","pref.who_can_manage_ext_shared_channels","pref.who_can_manage_guests","pref.who_can_manage_shared_channels","pref.sso_setting_changed"],"user":["custom_tos_accepted","guest_created","guest_deactivated","guest_reactivated","owner_transferred","role_change_to_admin","role_change_to_guest","role_change_to_owner","role_change_to_user","user_created","user_deactivated","user_login","user_login_failed","user_logout","user_reactivated","guest_expiration_set","guest_expiration_cleared","guest_expired","user_logout_compromised","user_session_reset_by_admin","user_session_invalidated","user_logout_non_compliant_mobile_app_version","user_force_upgrade_non_compliant_mobile_app_version"],"file":["file_downloaded","file_uploaded","file_public_link_created","file_public_link_revoked","file_shared","file_downloaded_blocked"],"channel":["user_channel_join","user_channel_leave","guest_channel_join","guest_channel_leave","public_channel_created","private_channel_created","public_channel_deleted","private_channel_deleted","public_channel_archive","private_channel_archive","public_channel_unarchive","private_channel_unarchive","mpim_converted_to_private","public_channel_converted_to_private","group_converted_to_channel","channel_workspaces_updated","external_shared_channel_invite_sent","external_shared_channel_invite_accepted","external_shared_channel_invite_approved","external_shared_channel_invite_created","external_shared_channel_invite_declined","external_shared_channel_invite_expired","external_shared_channel_invite_revoked","external_shared_channel_invite_auto_revoked","external_shared_channel_connected","external_shared_channel_disconnected","external_shared_channel_reconnected","channel_moved","channel_posting_pref_changed_from_org_level","channel_renamed","channel_email_address_created","channel_email_address_deleted"],"app":["app_installed","app_uninstalled","app_scopes_expanded","app_approved","app_restricted","app_removed_from_whitelist","app_resources_granted","app_token_preserved","workflow_app_token_preserved","bot_token_upgraded","bot_token_downgraded","org_app_workspace_added","org_app_workspace_removed","org_app_future_workspace_install_enabled","org_app_future_workspace_install_disabled","org_app_upgraded_to_org_install"],"workflow_builder":["workflow_created","workflow_deleted","workflow_published","workflow_unpublished","workflow_responses_csv_download"],"barrier":["barrier_created","barrier_updated","barrier_deleted"],"message":["message_tombstoned","message_restored"]}}""" + + self.send_response(HTTPStatus.OK) + self.set_common_headers() + self.wfile.write(body.encode("utf-8")) + self.wfile.close() + + except Exception as e: + self.logger.error(str(e), exc_info=True) + raise diff --git a/tests/slack_sdk/audit_logs/test_client.py b/tests/slack_sdk/audit_logs/test_client.py new file mode 100644 index 000000000..f682888a8 --- /dev/null +++ b/tests/slack_sdk/audit_logs/test_client.py @@ -0,0 +1,50 @@ +import unittest +from urllib.error import URLError + +from slack_sdk.audit_logs import AuditLogsClient, AuditLogsResponse +from tests.slack_sdk.audit_logs.mock_web_api_handler import MockHandler +from tests.mock_web_api_server import setup_mock_web_api_server, cleanup_mock_web_api_server + + +class TestAuditLogsClient(unittest.TestCase): + def setUp(self): + self.client = AuditLogsClient(token="xoxp-", base_url="http://localhost:8888/") + setup_mock_web_api_server(self, MockHandler) + + def tearDown(self): + cleanup_mock_web_api_server(self) + + def test_logs(self): + resp: AuditLogsResponse = self.client.logs(limit=1, action="user_login") + self.assertEqual(200, resp.status_code) + self.assertIsNotNone(resp.body.get("entries")) + + self.assertEqual(resp.typed_body.entries[0].id, "xxx-yyy-zzz-111") + + def test_logs_pagination(self): + resp: AuditLogsResponse = self.client.logs(limit=1, action="user_login", cursor="xxxxxxx") + self.assertEqual(200, resp.status_code) + self.assertIsNotNone(resp.body.get("entries")) + + self.assertEqual(resp.typed_body.entries[0].id, "xxx-yyy-zzz-111") + + def test_actions(self): + resp: AuditLogsResponse = self.client.actions() + self.assertEqual(200, resp.status_code) + self.assertIsNotNone(resp.body.get("actions")) + + def test_schemas(self): + resp: AuditLogsResponse = self.client.schemas() + self.assertEqual(200, resp.status_code) + self.assertIsNotNone(resp.body.get("schemas")) + + def test_url_error(self): + invalid_url = "http://localhost:9999/" + client = AuditLogsClient(token="xoxp-", base_url=invalid_url) + with self.assertRaises(URLError): + client.logs(limit=1, action="user_login") + + def test_http_error(self): + resp: AuditLogsResponse = self.client.api_call(path="error") + self.assertEqual(500, resp.status_code) + self.assertEqual("unexpected response body", resp.raw_body) diff --git a/tests/slack_sdk/audit_logs/test_client_http_retry.py b/tests/slack_sdk/audit_logs/test_client_http_retry.py new file mode 100644 index 000000000..7c76d3819 --- /dev/null +++ b/tests/slack_sdk/audit_logs/test_client_http_retry.py @@ -0,0 +1,41 @@ +import unittest + +from slack_sdk.audit_logs import AuditLogsClient +from slack_sdk.http_retry import RateLimitErrorRetryHandler +from tests.slack_sdk.audit_logs.mock_web_api_handler import MockHandler +from tests.mock_web_api_server import setup_mock_web_api_server, cleanup_mock_web_api_server +from ..my_retry_handler import MyRetryHandler + + +class TestAuditLogsClient_HttpRetries(unittest.TestCase): + def setUp(self): + setup_mock_web_api_server(self, MockHandler) + + def tearDown(self): + cleanup_mock_web_api_server(self) + + def test_retries(self): + retry_handler = MyRetryHandler(max_retry_count=2) + client = AuditLogsClient( + token="xoxp-remote_disconnected", + base_url="http://localhost:8888/", + retry_handlers=[retry_handler], + ) + try: + client.actions() + self.fail("An exception is expected") + except Exception as _: + pass + + self.assertEqual(2, retry_handler.call_count) + + def test_ratelimited(self): + client = AuditLogsClient( + token="xoxp-ratelimited", + base_url="http://localhost:8888/", + ) + client.retry_handlers.append(RateLimitErrorRetryHandler()) + + response = client.actions() + # Just running retries; no assertions for call count so far + self.assertEqual(429, response.status_code) diff --git a/tests/slack_sdk/audit_logs/test_response.py b/tests/slack_sdk/audit_logs/test_response.py new file mode 100644 index 000000000..f3218a74d --- /dev/null +++ b/tests/slack_sdk/audit_logs/test_response.py @@ -0,0 +1,375 @@ +import json +import unittest + +from slack_sdk.audit_logs.v1.logs import LogsResponse + + +class TestAuditLogsClient(unittest.TestCase): + def setUp(self): + pass + + def tearDown(self): + pass + + def test_logs(self): + json_data = """{ + "entries": [ + { + "id": "xxx-yyy-zzz-111", + "date_create": 1611221649, + "action": "user_login", + "new_attribute": "this should be just accepted as unknown attribute", + "actor": { + "type": "user", + "new_attribute": "this should be just accepted as unknown attribute", + "user": { + "id": "W111", + "name": "your name", + "email": "foo@example.com", + "team": "E111" + } + }, + "entity": { + "type": "user", + "new_attribute": "this should be just accepted as unknown attribute", + "user": { + "id": "W111", + "name": "your name", + "email": "foo@example.com", + "team": "E111" + } + }, + "context": { + "new_attribute": "this should be just accepted as unknown attribute", + "location": { + "type": "workspace", + "id": "T111", + "new_attribute": "this should be just accepted as unknown attribute", + "name": "WS", + "domain": "foo-bar-baz" + }, + "ua": "UA", + "ip_address": "1.2.3.4", + "session_id": 1656410836837 + } + }, + { + "id": "32c68de4-cbfa-4fcb-9780-25fdd5aacf32", + "date_create": 1611221649, + "action": "user_login", + "actor": { + "type": "user", + "user": { + "id": "W111", + "name": "your name", + "email": "foo@example.com", + "team": "E111" + } + }, + "entity": { + "type": "user", + "user": { + "id": "W111", + "name": "your name", + "email": "foo@example.com", + "team": "E111" + } + }, + "context": { + "location": { + "type": "workspace", + "id": "T111", + "name": "WS", + "domain": "foo-bar-baz" + }, + "ua": "UA", + "ip_address": "1.2.3.4", + "session_id": 1656410836837 + } + } + ], + "response_metadata": { + "next_cursor": "xxx", + "new_attribute": "this should be just accepted as unknown attribute" + }, + "new_attribute": "this should be just accepted as unknown attribute" +} +""" + logs = LogsResponse(**json.loads(json_data)) + self.assertIsNotNone(logs) + self.assertIsNotNone(logs.entries[0].unknown_fields.get("new_attribute")) + self.assertIsNotNone(logs.response_metadata.unknown_fields.get("new_attribute")) + self.assertIsNotNone(logs.unknown_fields.get("new_attribute")) + + def test_logs_complete(self): + logs = LogsResponse(**json.loads(logs_response_data)) + self.assertIsNotNone(logs) + self.assertEqual(logs.response_metadata.next_cursor, "") + entry = logs.entries[0] + self.assertEqual( + list(entry.details.unknown_fields.keys()), + [], + f"found: {entry.details.unknown_fields.keys()}", + ) + self.assertEqual( + list(entry.entity.unknown_fields.keys()), + [], + f"found: {entry.entity.unknown_fields.keys()}", + ) + self.assertEqual( + list(entry.context.unknown_fields.keys()), + [], + f"found: {entry.context.unknown_fields.keys()}", + ) + self.assertEqual( + list(entry.actor.unknown_fields.keys()), + [], + f"found: {entry.actor.unknown_fields.keys()}", + ) + self.assertEqual(entry.details.is_token_rotation_enabled_app, True) + self.assertEqual(entry.details.inviter.id, "inviter_id") + self.assertEqual(entry.details.kicker.id, "kicker_id") + self.assertEqual(entry.details.old_retention_policy.type, "old") + self.assertEqual(entry.details.new_retention_policy.type, "new") + self.assertEqual(entry.details.is_internal_integration, True) + self.assertEqual(entry.details.cleared_resolution, "approved") + self.assertEqual(entry.details.who_can_post.type, ["owner", "admin"]) + self.assertEqual(entry.details.who_can_post.user, ["W111"]) + self.assertEqual(entry.details.can_thread.type, ["admin", "org_admin"]) + self.assertEqual(entry.details.can_thread.user, ["W222"]) + self.assertEqual(entry.details.is_external_limited, True) + # Due to historical reasons, succeeded_users/failed_users can be + # either an array or a single string with encoded JSON data + self.assertEqual(entry.details.succeeded_users, ["W111", "W222"]) + self.assertEqual(entry.details.failed_users, ["W333", "W444"]) + self.assertEqual(entry.details.exporting_team_id, 1134128598372) + + +logs_response_data = """{ + "ok": false, + "warning": "", + "error": "", + "needed": "", + "provided": "", + "response_metadata": { + "next_cursor": "" + }, + "entries": [ + { + "id": "", + "date_create": 123, + "action": "", + "actor": { + "type": "", + "user": { + "id": "", + "name": "", + "email": "", + "team": "" + } + }, + "entity": { + "type": "", + "app": { + "id": "", + "name": "", + "is_distributed": false, + "is_directory_approved": false, + "is_workflow_app": false, + "scopes": [ + "" + ] + }, + "user": { + "id": "", + "name": "", + "email": "", + "team": "" + }, + "usergroup": { + "id": "", + "name": "" + }, + "workspace": { + "id": "", + "name": "", + "domain": "" + }, + "enterprise": { + "id": "", + "name": "", + "domain": "" + }, + "file": { + "id": "", + "name": "", + "filetype": "", + "title": "" + }, + "channel": { + "id": "", + "name": "", + "privacy": "", + "is_shared": false, + "is_org_shared": false, + "teams_shared_with": [ + "" + ], + "original_connected_channel_id": "" + }, + "workflow": { + "id": "", + "name": "" + }, + "barrier": { + "id": "", + "primary_usergroup": "", + "barriered_from_usergroups": [ + "" + ], + "restricted_subjects": [ + "" + ] + } + }, + "context": { + "session_id": "", + "location": { + "type": "", + "id": "", + "name": "", + "domain": "" + }, + "ua": "", + "ip_address": "" + }, + "details": { + "type": "", + "app_owner_id": "", + "scopes": [ + "" + ], + "bot_scopes": [ + "" + ], + "new_scopes": [ + "" + ], + "previous_scopes": [ + "" + ], + "inviter": { + "id": "inviter_id", + "name": "", + "email": "", + "team": "" + }, + "kicker": { + "id": "kicker_id", + "name": "", + "email": "", + "team": "" + }, + "installer_user_id": "", + "approver_id": "", + "approval_type": "", + "app_previously_approved": false, + "old_scopes": [ + "" + ], + "name": "", + "bot_id": "", + "channels": [ + "" + ], + "permissions": [ + { + "resource": { + "type": "", + "grant": { + "type": "", + "resource_id": "", + "wildcard": { + "type": "" + } + } + }, + "scopes": [ + "" + ] + } + ], + "shared_to": "", + "reason": "", + "is_internal_integration": false, + "is_workflow": false, + "mobile_only": false, + "web_only": false, + "non_sso_only": false, + "expires_on": 123, + "new_version_id": "", + "trigger": "", + "granular_bot_token": false, + "origin_team": "", + "target_team": "", + "resolution": "", + "app_previously_resolved": false, + "admin_app_id": "", + "export_type": "", + "export_start_ts": "", + "export_end_ts": "", + "barrier_id": "", + "primary_usergroup_id": "", + "barriered_from_usergroup_ids": [ + "" + ], + "restricted_subjects": [ + "" + ], + "duration": 123, + "desktop_app_browser_quit": false, + "invite_id": "", + "external_organization_id": "", + "external_organization_name": "", + "external_user_id": "", + "external_user_email": "", + "channel_id": "", + "added_team_id": "", + "is_token_rotation_enabled_app": true, + "old_retention_policy": { + "type": "old", + "duration_days": 111 + }, + "new_retention_policy": { + "type": "new", + "duration_days": 222 + }, + "is_internal_integration": true, + "cleared_resolution": "approved", + "who_can_post": { + "type": [ + "owner", + "admin" + ], + "user": [ + "W111" + ] + }, + "can_thread": { + "type": [ + "admin", + "org_admin" + ], + "user": [ + "W222" + ] + }, + "is_external_limited": true, + "succeeded_users": "[\\\"W111\\\", \\\"W222\\\"]", + "failed_users": "[\\\"W333\\\", \\\"W444\\\"]", + "exporting_team_id": 1134128598372 + } + } + ] +} +""" diff --git a/tests/slack_sdk/fatal_error_retry_handler.py b/tests/slack_sdk/fatal_error_retry_handler.py new file mode 100644 index 000000000..49340f56e --- /dev/null +++ b/tests/slack_sdk/fatal_error_retry_handler.py @@ -0,0 +1,28 @@ +from typing import Optional + +from slack_sdk.http_retry.interval_calculator import RetryIntervalCalculator +from slack_sdk.http_retry.state import RetryState +from slack_sdk.http_retry.request import HttpRequest +from slack_sdk.http_retry.response import HttpResponse +from slack_sdk.http_retry.handler import RetryHandler, default_interval_calculator + + +class FatalErrorRetryHandler(RetryHandler): + def __init__( + self, + max_retry_count: int = 1, + interval_calculator: RetryIntervalCalculator = default_interval_calculator, + ): + super().__init__(max_retry_count, interval_calculator) + self.call_count = 0 + + def _can_retry( + self, + *, + state: RetryState, + request: HttpRequest, + response: Optional[HttpResponse], + error: Optional[Exception], + ) -> bool: + self.call_count += 1 + return response is not None and response.status_code == 200 and response.body.get("error") == "fatal_error" diff --git a/tests/slack_sdk/http_retry/__init__.py b/tests/slack_sdk/http_retry/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/slack_sdk/http_retry/test_builtins.py b/tests/slack_sdk/http_retry/test_builtins.py new file mode 100644 index 000000000..49394bb30 --- /dev/null +++ b/tests/slack_sdk/http_retry/test_builtins.py @@ -0,0 +1,30 @@ +import unittest + +from slack_sdk.http_retry import ( + FixedValueRetryIntervalCalculator, + default_retry_handlers, + all_builtin_retry_handlers, +) + + +class TestBuiltins(unittest.TestCase): + def test_default_ones(self): + list = default_retry_handlers() + self.assertEqual(1, len(list)) + list.clear() + self.assertEqual(0, len(list)) + list = default_retry_handlers() + self.assertEqual(1, len(list)) + + list = all_builtin_retry_handlers() + self.assertEqual(2, len(list)) + list.clear() + self.assertEqual(0, len(list)) + list = all_builtin_retry_handlers() + self.assertEqual(2, len(list)) + + def test_fixed_value_retry_interval_calculator(self): + for fixed_value in [0.1, 0.2]: + calculator = FixedValueRetryIntervalCalculator(fixed_internal=fixed_value) + for i in range(10): + self.assertEqual(fixed_value, calculator.calculate_sleep_duration(i)) diff --git a/tests/slack_sdk/models/__init__.py b/tests/slack_sdk/models/__init__.py new file mode 100644 index 000000000..eea9b2100 --- /dev/null +++ b/tests/slack_sdk/models/__init__.py @@ -0,0 +1,46 @@ +STRING_51_CHARS = "SFOTYFUZTMDSOULXMKVFDOBQWNBAVGANMVLXQQZZQZQHBLJRZNY" +STRING_301_CHARS = ( + "ZFOMVKXETILJKBZPVKOYAUPNYWWWUICNEVXVPWNAMGCNHDBRMATGPMUHUZHUJKFWWLXBQXVDNCGJHAPKEK" + "DZCXKBXEHWCWBYDIGNYXTOFWWNLPBTVIGTNQKIQDHUAHZPWQDKKCHERBYKLAUOOKJXJJLGOPSCRVEHCOAD" + "BFYKJTXHMPPYWQVXCVGNNSXLNIHVKTVMEOIRXQDPLHIDZBAHUEDWXKXILEBOLILOYGZLNGCNXKWMFJWYYI" + "PIDUKJVGKTUERTPRMMMVZNAAOMZJFXFSEENCAMBOUJMYXTPHJEOPKDB" +) +STRING_3001_CHARS = ( + "UJSUOROQMIMCCCGFHQJVJXBCPWAOIMVOIIPFZGIZOBZWJHQLIABTGHXJMYVWYCFUIOWMVLJPJOHDVZRHUE" + "SVNQTHGXFKMGNBPRALVWQEYTFBKKKFUONDFRALDRZHKPGTWZAXOUFQJKOGTMYSFEDBEQQXIGKZMXNKDCEN" + "LSVHNGWVCIDMNSIZTBWBBVUMLPHRUCIZLZBFEGNFXZNJEZBUTNHNCYWWYSJSJDNOPPGHUPZLPJWDKEATZO" + "UGKZEGFTFBGZDNRITDFBDJLYDGETUHBDGFEELBJBDMSRBVFPXMRJXWULONCZRZZBNFOPARFNXPQONKEIKG" + "QDPJWCMGYSEIBAOLJNWPJVUSMJGCSQBLGZCWXJOYJHIZMNFMTLUQFGEBOONOZMGBWORFEUGYIUJAKLVAJZ" + "FTNOPOZNMUJPWRMGPKNQSBMZQRJXLRQJPYYUXLFUPICAFTXDTQIUOQRCSLWPHHUZAOPVTBRCXWUIXMFGYT" + "RBKPWJJXNQPLIAZAOKIMDWCDZABPLNOXYOZZBTHSDIPXXBKXKOSYYCITFSMNVIOCNGEMRKRBPCLBOCXBZQ" + "VVWKNJBPWQNJOJWAGAIBOBFRVDWLXVBLMBSXYLOAWMPLKJOVHABNNIFTKTKBIIBOSHYQZRUFPPPRDQPMUV" + "WMSWBLRUHKEMUFHIMZRUNNITKWYIWRXYPGFPXMNOABRWXGQFCWOYMMBYRQQLOIBFENIZBUIWLMDTIXCPXW" + "NNHBSRPSMCQIMYRCFCPLQQGVOHYZOUGFEXDTOETUKQAXOCNGYBYPYWDQHYOKPCCORGRNHXZAAYYZGSWWGS" + "CMJVCTAJUOMIMYRSVQGGPHCENXHLNFJJOEKIQWNYKBGKBMBJSFKKKYEPVXMOTAGFECZWQGVAEXHIAKTWYO" + "WFYMDMNNHWZGBHDEXYGRYQVXQXZJYAWLJLWUGQGPHAYJWJQWRQZBNAMNGEPVPPUMOFTOZNYLEXLWWUTABR" + "OLHPFFSWTZGYPAZJXRRPATWXKRDFQJRAEOBFNIWVZDKLNYXUFBOAWSDSKFYYRTADBBYHEWNZSTDXAAOQCD" + "WARSJZONQXRACMNBXZSEWZYBWADNDVRXBNJPJZQUNDYLBASCLCPFJWAMJUQAHBUZYDTIQPBPNJVVOHISZP" + "VGBDNXFIHYCABTSVNVILZUPPZXMPPZVBRTRHDGHTXXLBIYTMRDOUBYBVHVVKQAXAKISFJNUTRZKOCACJAX" + "ZXRRKMFOKYBHFUDBIXFAQSNUTYFNVQNGYWPJZGTLQUMOWXKKTUZGOUXAOVLQMMNKKECQCCOBNPPPXZYWZU" + "WHLHZQDIETDDPXWTILXGAYJKPHBXPLRFDPDSHFUPOIWRQDWQQNARPHPVKJPXZGGXOUVBYZSLUPVIJKWKNF" + "WMFKWYSYJJCCSCALMVPYIPHDKRXOWTUAYJFTAANCTVYDNSSIHGCWGKLDHFFBFSIFBMGHHFHZQSWOWZXOUW" + "PKNICGXPFMFIESHPDDMGSSWGBIAQVBANHLGDBYENRLSUARJXLQWPMOUSUKIIVXICBJPSWOEZPEUAJSLITV" + "XEQWSRENUJRJHPLBPFMBRPKGQNSYFWVLFLSQGGETKDUGYOLNFSMRVAZLQOAEKCUGNFEXRUDYSKBOQPYJAH" + "QHEIMSAAMTTYVJTHZDGQEITLERRYYQCTEQPTYQPHLMBDPCZZNNJYLGAGNXONCTIBSXEHXPYWBCTEEZLIYI" + "FMPYONXRVLSGZOEDZIMVDDPRXBKCKEPHOVLRBSPKMLZPXNRZVSSSYAOMGSVJODUZAJDYLGUZAFJMCOVGQX" + "ZUWQJENTEWQRFZYQTVEAHFQUWBUCFWHGRTMNQQFSPKKYYUBJVXKFQCCMBNGWNTRFGFKBFWTTPNDTGGWTAK" + "EOTXUPGFXOVWTOERFQSEZWVUYMGHVBQZIKIBJCNMKTZANNNOVMYTFLQYVNKTVZHFUJTPWNQWRYKGMYRYDC" + "WNTCUCYJCWXMMOJXUJSDWJKTTYOBFJFLBUCECGTVWKELCBDIKDUDOBLZLHYJQTVHXSUAFHDFDMETLHHEEJ" + "XJYWEOTXAUOZARSSQTBBXULKBBSTQHMJAAOUDIQCCETFWAINYIJCGXCILMDCAUYDMNZBDKIPVRCKCYKOIG" + "JHBLUHPOLDBWREFAZVEFFSOQQHMCXQYCQGMBHYKHJDBZXRAXLVZNYQXZEQYRSZHKKGCSOOEGNPFZDNGIMJ" + "QCXAEWWDYIGTQMJKBTMGSJAJCKIODCAEXVEGYCUBEEGCMARPJIKNAROJHYHKKTKGKKRVVSVYADCJXGSXAR" + "KGOUSUSZGJGFIKJDKJUIRQVSAHSTBCVOWZJDCCBWNNCBIYTCNOUPEYACCEWZNGETBTDJWQIEWRYIQXOZKP" + "ULDPCINLDFFPNORJHOZBSSYPPYNZTLXBRFZGBECKTTNVIHYNKGBXTTIXIKRBGVAPNWBPFNCGWQMZHBAHBX" + "MFEPSWVBUDLYDIVLZFHXTQJWUNWQHSWSCYFXQQSVORFQGUQIHUAJYFLBNBKJPOEIPYATRMNMGUTTVBOUHE" + "ZKXVAUEXCJYSCZEMGWTPXMQJEUWYHTFJQTBOQBEPQIPDYLBPIKKGPVYPOVLPPHYNGNWFTNQCDAATJVKRHC" + "OZGEBPFZZDPPZOWQCDFQZJAMXLVREYJQQFTQJKHMLRFJCVPVCTSVFVAGDVNXIGINSGHKGTWCKXNRZCZFVX" + "FPKZHPOMJTQOIVDIYKEVIIBAUHEDGOUNPCPMVLTZQLICXKKIYRJASBNDUZAONDDLQNVRXGWNQAOWSJSFWU" + "YWTTLOVXIJYERRZQCJMRZHCXEEAKYCLEICUWOJUXWHAPHQJDTBVRPVWTMCJRAUYCOTFXLLIQLOBASBMPED" + "KLDZDWDYAPXCKLZMEFIAOFYGFLBMURWVBFJDDEFXNIQOORYRMNROGVCOESSHSNIBNFRHPSWVAUQQVDMAHX" + "STDOVZMZEFRRFCKOLDOOFVOBCPRRLGYFJNXVPPUZONOSALUUI" +) diff --git a/tests/slack_sdk/models/test_actions.py b/tests/slack_sdk/models/test_actions.py new file mode 100644 index 000000000..df1de7fc0 --- /dev/null +++ b/tests/slack_sdk/models/test_actions.py @@ -0,0 +1,169 @@ +import unittest + +from slack_sdk.errors import SlackObjectFormationError +from slack_sdk.models.attachments import ( + ActionButton, + ActionChannelSelector, + ActionConversationSelector, + ActionExternalSelector, + ActionLinkButton, + ActionUserSelector, +) +from slack_sdk.models.blocks import ConfirmObject, Option, OptionGroup +from slack_sdk.models.dialogs import ActionStaticSelector +from tests.slack_sdk.models import STRING_3001_CHARS + + +class ButtonTests(unittest.TestCase): + def test_json(self): + self.assertDictEqual( + ActionButton(name="button_1", text="Click me!", value="btn_1").to_dict(), + { + "name": "button_1", + "text": "Click me!", + "value": "btn_1", + "type": "button", + }, + ) + + confirm = ConfirmObject(title="confirm_title", text="confirm_text") + self.assertDictEqual( + ActionButton( + name="button_1", + text="Click me!", + value="btn_1", + confirm=confirm, + style="danger", + ).to_dict(), + { + "name": "button_1", + "text": "Click me!", + "value": "btn_1", + "type": "button", + "confirm": confirm.to_dict("action"), + "style": "danger", + }, + ) + + def test_value_length(self): + with self.assertRaises(SlackObjectFormationError): + ActionButton(name="button_1", text="Click me!", value=STRING_3001_CHARS).to_dict() + + def test_style_validator(self): + b = ActionButton(name="button_1", text="Click me!", value="btn_1") + with self.assertRaises(SlackObjectFormationError): + b.style = "abcdefg" + b.to_dict() + + b.style = "primary" + b.to_dict() + + +class LinkButtonTests(unittest.TestCase): + def test_json(self): + self.assertDictEqual( + ActionLinkButton(text="Click me!", url="http://google.com").to_dict(), + {"url": "http://google.com", "text": "Click me!", "type": "button"}, + ) + + +class StaticActionSelectorTests(unittest.TestCase): + def setUp(self) -> None: + self.options = [ + Option.from_single_value("one"), + Option.from_single_value("two"), + Option.from_single_value("three"), + ] + + self.option_group = [OptionGroup(label="group_1", options=self.options)] + + def test_json(self): + self.assertDictEqual( + ActionStaticSelector(name="select_1", text="selector_1", options=self.options).to_dict(), + { + "name": "select_1", + "text": "selector_1", + "options": [o.to_dict("action") for o in self.options], + "type": "select", + "data_source": "static", + }, + ) + + self.assertDictEqual( + ActionStaticSelector(name="select_1", text="selector_1", options=self.option_group).to_dict(), + { + "name": "select_1", + "text": "selector_1", + "option_groups": [o.to_dict("action") for o in self.option_group], + "type": "select", + "data_source": "static", + }, + ) + + def test_options_length(self): + with self.assertRaises(SlackObjectFormationError): + ActionStaticSelector(name="select_1", text="selector_1", options=self.options * 34).to_dict() + + +class DynamicActionSelectorTests(unittest.TestCase): + selectors = {ActionUserSelector, ActionChannelSelector, ActionConversationSelector} + + def setUp(self) -> None: + self.selected_opt = Option.from_single_value("U12345") + + def test_json(self): + for component in self.selectors: + with self.subTest(msg=f"{component} json formation test"): + self.assertDictEqual( + component(name="select_1", text="selector_1").to_dict(), + { + "name": "select_1", + "text": "selector_1", + "type": "select", + "data_source": component.data_source, + }, + ) + + self.assertDictEqual( + component( + name="select_1", + text="selector_1", + # next line is a little silly, but so is writing the test + # three times + **{f"selected_{component.data_source[:-1]}": self.selected_opt}, + ).to_dict(), + { + "name": "select_1", + "text": "selector_1", + "type": "select", + "data_source": component.data_source, + "selected_options": [self.selected_opt.to_dict("action")], + }, + ) + + +class ExternalActionSelectorTests(unittest.TestCase): + def test_json(self): + option = Option.from_single_value("one") + + self.assertDictEqual( + ActionExternalSelector(name="select_1", text="selector_1", min_query_length=3).to_dict(), + { + "name": "select_1", + "text": "selector_1", + "min_query_length": 3, + "type": "select", + "data_source": "external", + }, + ) + + self.assertDictEqual( + ActionExternalSelector(name="select_1", text="selector_1", selected_option=option).to_dict(), + { + "name": "select_1", + "text": "selector_1", + "selected_options": [option.to_dict("action")], + "type": "select", + "data_source": "external", + }, + ) diff --git a/tests/slack_sdk/models/test_attachments.py b/tests/slack_sdk/models/test_attachments.py new file mode 100644 index 000000000..7fbce0eeb --- /dev/null +++ b/tests/slack_sdk/models/test_attachments.py @@ -0,0 +1,214 @@ +import unittest + +from slack_sdk.errors import SlackObjectFormationError +from slack_sdk.models.attachments import ( + ActionButton, + ActionLinkButton, + Attachment, + BlockAttachment, + AttachmentField, + InteractiveAttachment, +) +from slack_sdk.models.blocks import SectionBlock, ImageBlock +from tests.slack_sdk.models import STRING_301_CHARS + + +class FieldTests(unittest.TestCase): + def test_basic_json(self): + self.assertDictEqual( + AttachmentField(title="field", value="something", short=False).to_dict(), + {"title": "field", "value": "something", "short": False}, + ) + + +class AttachmentTests(unittest.TestCase): + def setUp(self) -> None: + self.simple = Attachment(text="some_text") + + def test_basic_json(self): + self.assertDictEqual(Attachment(text="some text").to_dict(), {"text": "some text", "fields": []}) + + self.assertDictEqual( + Attachment( + text="attachment text", + title="Attachment", + fallback="fallback_text", + pretext="some_pretext", + title_link="link in title", + fields=[AttachmentField(title=f"field_{i}_title", value=f"field_{i}_value") for i in range(5)], + color="#FFFF00", + author_name="John Doe", + author_subname="@jd", + author_link="http://johndoeisthebest.com", + author_icon="http://johndoeisthebest.com/avatar.jpg", + thumb_url="thumbnail URL", + footer="and a footer", + footer_icon="link to footer icon", + ts=123456789, + markdown_in=["fields"], + ).to_dict(), + { + "text": "attachment text", + "author_name": "John Doe", + "author_subname": "@jd", + "author_link": "http://johndoeisthebest.com", + "author_icon": "http://johndoeisthebest.com/avatar.jpg", + "footer": "and a footer", + "title": "Attachment", + "footer_icon": "link to footer icon", + "pretext": "some_pretext", + "ts": 123456789, + "fallback": "fallback_text", + "title_link": "link in title", + "color": "#FFFF00", + "thumb_url": "thumbnail URL", + "fields": [ + {"title": "field_0_title", "value": "field_0_value", "short": True}, + {"title": "field_1_title", "value": "field_1_value", "short": True}, + {"title": "field_2_title", "value": "field_2_value", "short": True}, + {"title": "field_3_title", "value": "field_3_value", "short": True}, + {"title": "field_4_title", "value": "field_4_value", "short": True}, + ], + "mrkdwn_in": ["fields"], + }, + ) + + def test_footer_length(self): + with self.assertRaises(SlackObjectFormationError): + self.simple.footer = STRING_301_CHARS + self.simple.to_dict() + + def test_ts_without_footer(self): + with self.assertRaises(SlackObjectFormationError): + self.simple.ts = 123456789 + self.simple.to_dict() + + def test_markdown_in_invalid(self): + with self.assertRaises(SlackObjectFormationError): + self.simple.markdown_in = ["nothing"] + self.simple.to_dict() + + def test_color_valid(self): + with self.assertRaises(SlackObjectFormationError): + self.simple.color = "red" + self.simple.to_dict() + + with self.assertRaises(SlackObjectFormationError): + self.simple.color = "#ZZZZZZ" + self.simple.to_dict() + + self.simple.color = "#bada55" + self.assertEqual(self.simple.to_dict()["color"], "#bada55") + + self.simple.color = "good" + self.assertEqual(self.simple.to_dict()["color"], "good") + + def test_image_url_and_thumb_url(self): + with self.assertRaises(SlackObjectFormationError): + self.simple.thumb_url = "some URL" + self.simple.image_url = "some URL" + self.simple.to_dict() + + self.simple.image_url = None + self.simple.to_dict() + + def author_name_without_author_link(self): + with self.assertRaises(SlackObjectFormationError): + self.simple.author_name = "http://google.com" + self.simple.to_dict() + + self.simple.author_name = None + self.simple.to_dict() + + def author_icon_without_author_name(self): + with self.assertRaises(SlackObjectFormationError): + self.simple.author_icon = "http://google.com/images.jpg" + self.simple.to_dict() + + self.simple.author_icon = None + self.simple.to_dict() + + +class InteractiveAttachmentTests(unittest.TestCase): + def test_basic_json(self): + actions = [ + ActionButton(name="button_1", text="Click me", value="button_value_1"), + ActionLinkButton(text="navigate", url="http://google.com"), + ] + self.assertDictEqual( + InteractiveAttachment(text="some text", callback_id="abc123", actions=actions).to_dict(), + { + "text": "some text", + "fields": [], + "callback_id": "abc123", + "actions": [a.to_dict() for a in actions], + }, + ) + + self.assertDictEqual( + InteractiveAttachment( + actions=actions, + callback_id="cb_123", + text="attachment text", + title="Attachment", + fallback="fallback_text", + pretext="some_pretext", + title_link="link in title", + fields=[AttachmentField(title=f"field_{i}_title", value=f"field_{i}_value") for i in range(5)], + color="#FFFF00", + author_name="John Doe", + author_subname="@jd", + author_link="http://johndoeisthebest.com", + author_icon="http://johndoeisthebest.com/avatar.jpg", + thumb_url="thumbnail URL", + footer="and a footer", + footer_icon="link to footer icon", + ts=123456789, + markdown_in=["fields"], + ).to_dict(), + { + "text": "attachment text", + "callback_id": "cb_123", + "actions": [a.to_dict() for a in actions], + "author_name": "John Doe", + "author_subname": "@jd", + "author_link": "http://johndoeisthebest.com", + "author_icon": "http://johndoeisthebest.com/avatar.jpg", + "footer": "and a footer", + "title": "Attachment", + "footer_icon": "link to footer icon", + "pretext": "some_pretext", + "ts": 123456789, + "fallback": "fallback_text", + "title_link": "link in title", + "color": "#FFFF00", + "thumb_url": "thumbnail URL", + "fields": [ + {"title": "field_0_title", "value": "field_0_value", "short": True}, + {"title": "field_1_title", "value": "field_1_value", "short": True}, + {"title": "field_2_title", "value": "field_2_value", "short": True}, + {"title": "field_3_title", "value": "field_3_value", "short": True}, + {"title": "field_4_title", "value": "field_4_value", "short": True}, + ], + "mrkdwn_in": ["fields"], + }, + ) + + def test_actions_length(self): + actions = [ActionButton(name="button_1", text="Click me", value="button_value_1")] * 6 + + with self.assertRaises(SlackObjectFormationError): + InteractiveAttachment(text="some text", callback_id="abc123", actions=actions).to_dict(), + + +class BlockAttachmentTests(unittest.TestCase): + def test_basic_json(self): + blocks = [ + SectionBlock(text="Some text"), + ImageBlock(image_url="image.jpg", alt_text="an image"), + ] + + self.assertDictEqual( + BlockAttachment(blocks=blocks).to_dict(), + {"blocks": [b.to_dict() for b in blocks]}, + ) diff --git a/tests/slack_sdk/models/test_blocks.py b/tests/slack_sdk/models/test_blocks.py new file mode 100644 index 000000000..6f3b9f141 --- /dev/null +++ b/tests/slack_sdk/models/test_blocks.py @@ -0,0 +1,1471 @@ +import unittest +from typing import List + +from slack_sdk.errors import SlackObjectFormationError +from slack_sdk.models.blocks import ( + ActionsBlock, + Block, + ButtonElement, + CallBlock, + ContextActionsBlock, + ContextBlock, + DividerBlock, + FileBlock, + HeaderBlock, + ImageBlock, + ImageElement, + InputBlock, + LinkButtonElement, + MarkdownBlock, + MarkdownTextObject, + Option, + OverflowMenuElement, + PlainTextObject, + RawTextObject, + RichTextBlock, + RichTextElementParts, + RichTextListElement, + RichTextPreformattedElement, + RichTextQuoteElement, + RichTextSectionElement, + SectionBlock, + StaticSelectElement, + TableBlock, + VideoBlock, +) +from slack_sdk.models.blocks.basic_components import FeedbackButtonObject, SlackFile +from slack_sdk.models.blocks.block_elements import FeedbackButtonsElement, IconButtonElement + +from . import STRING_3001_CHARS + +# https://docs.slack.dev/reference/block-kit/blocks + + +class BlockTests(unittest.TestCase): + def test_parse(self): + input = { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "A message *with some bold text* and _some italicized text_.", + }, + "unexpected_field": "test", + "unexpected_fields": [1, 2, 3], + "unexpected_object": {"something": "wrong"}, + } + block = Block.parse(input) + self.assertIsNotNone(block) + + self.assertDictEqual( + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "A message *with some bold text* and _some italicized text_.", + }, + }, + block.to_dict(), + ) + + def test_eq(self): + self.assertEqual(Block(), Block()) + self.assertEqual(Block(type="test"), Block(type="test")) + self.assertNotEqual(Block(type="test"), Block(type="another test")) + + +# ---------------------------------------------- +# Section +# ---------------------------------------------- + + +class SectionBlockTests(unittest.TestCase): + maxDiff = None + + def test_document_1(self): + input = { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "A message *with some bold text* and _some italicized text_.", + }, + } + self.assertDictEqual(input, SectionBlock(**input).to_dict()) + + def test_document_2(self): + input = { + "type": "section", + "text": { + "text": "A message *with some bold text* and _some italicized text_.", + "type": "mrkdwn", + }, + "fields": [ + {"type": "mrkdwn", "text": "High"}, + {"type": "plain_text", "emoji": True, "text": "String"}, + ], + } + self.assertDictEqual(input, SectionBlock(**input).to_dict()) + + def test_document_3(self): + input = { + "type": "section", + "text": { + "text": "*Sally* has requested you set the deadline for the Nano launch project", + "type": "mrkdwn", + }, + "accessory": { + "type": "datepicker", + "action_id": "datepicker123", + "initial_date": "1990-04-28", + "placeholder": {"type": "plain_text", "text": "Select a date"}, + }, + } + self.assertDictEqual(input, SectionBlock(**input).to_dict()) + + def test_parse(self): + input = { + "type": "section", + "text": { + "type": "plain_text", + "text": "This is a plain text section block.", + "emoji": True, + }, + } + self.assertDictEqual(input, SectionBlock(**input).to_dict()) + + def test_parse_2(self): + input = { + "type": "section", + "text": { + "type": "plain_text", + "text": "This is a plain text section block.", + "emoji": True, + }, + "expand": True, + } + self.assertDictEqual(input, SectionBlock(**input).to_dict()) + + def test_json(self): + self.assertDictEqual( + { + "text": {"text": "some text", "type": "mrkdwn"}, + "block_id": "a_block", + "type": "section", + }, + SectionBlock(text="some text", block_id="a_block").to_dict(), + ) + + self.assertDictEqual( + { + "text": {"text": "some text", "type": "mrkdwn"}, + "fields": [ + {"text": "field0", "type": "mrkdwn"}, + {"text": "field1", "type": "mrkdwn"}, + {"text": "field2", "type": "mrkdwn"}, + {"text": "field3", "type": "mrkdwn"}, + {"text": "field4", "type": "mrkdwn"}, + ], + "type": "section", + }, + SectionBlock(text="some text", fields=[f"field{i}" for i in range(5)]).to_dict(), + ) + + button = LinkButtonElement(text="Click me!", url="https://example.com") + self.assertDictEqual( + { + "type": "section", + "text": {"text": "some text", "type": "mrkdwn"}, + "accessory": button.to_dict(), + }, + SectionBlock(text="some text", accessory=button).to_dict(), + ) + + def test_text_or_fields_populated(self): + with self.assertRaises(SlackObjectFormationError): + SectionBlock().to_dict() + + def test_fields_length(self): + with self.assertRaises(SlackObjectFormationError): + SectionBlock(fields=[f"field{i}" for i in range(11)]).to_dict() + + def test_issue_628(self): + elem = SectionBlock(text="1234567890" * 300) + elem.to_dict() # no exception + with self.assertRaises(SlackObjectFormationError): + elem = SectionBlock(text="1234567890" * 300 + "a") + elem.to_dict() + + @classmethod + def build_slack_block(cls, msg1, msg2, data): + blocks = [ + { + "type": "section", + "text": {"type": "mrkdwn", "text": f"*{msg1}*:\n{msg2}"}, + }, + {"type": "section", "fields": []}, + ] + names = list(set(data.keys()) - set("user_comments")) + fields = [{"type": "mrkdwn", "text": f"*{name}*:\n{data[name]}"} for name in names] + blocks[1]["fields"] = fields + return blocks + + @classmethod + def build_slack_block_native(cls, msg1, msg2, data): + blocks: List[SectionBlock] = [ + SectionBlock(text=MarkdownTextObject.parse(f"*{msg1}*:\n{msg2}")), + SectionBlock(fields=[]), + ] + names: List[str] = list(set(data.keys()) - set("user_comments")) + fields = [MarkdownTextObject.parse(f"*{name}*:\n{data[name]}") for name in names] + blocks[1].fields = fields + return list(b.to_dict() for b in blocks) + + def test_issue_500(self): + data = { + "first": "1", + "second": "2", + "third": "3", + "user_comments": {"first", "other"}, + } + expected = self.build_slack_block("category", "tech", data) + actual = self.build_slack_block_native("category", "tech", data) + self.assertDictEqual({"blocks": expected}, {"blocks": actual}) + + +# ---------------------------------------------- +# Divider +# ---------------------------------------------- + + +class DividerBlockTests(unittest.TestCase): + def test_document(self): + input = {"type": "divider"} + self.assertDictEqual(input, DividerBlock(**input).to_dict()) + + def test_json(self): + self.assertDictEqual({"type": "divider"}, DividerBlock().to_dict()) + self.assertDictEqual({"type": "divider"}, DividerBlock(**{"type": "divider"}).to_dict()) + + def test_json_with_block_id(self): + self.assertDictEqual( + {"type": "divider", "block_id": "foo"}, + DividerBlock(block_id="foo").to_dict(), + ) + self.assertDictEqual( + {"type": "divider", "block_id": "foo"}, + DividerBlock(**{"type": "divider", "block_id": "foo"}).to_dict(), + ) + + +# ---------------------------------------------- +# Image +# ---------------------------------------------- + + +class ImageBlockTests(unittest.TestCase): + def test_document(self): + input = { + "type": "image", + "title": { + "type": "plain_text", + "text": "Please enjoy this photo of a kitten", + }, + "block_id": "image4", + "image_url": "http://placekitten.com/500/500", + "alt_text": "An incredibly cute kitten.", + } + self.assertDictEqual(input, ImageBlock(**input).to_dict()) + + def test_issue_1369_title_type(self): + self.assertEqual( + "plain_text", + ImageBlock( + image_url="https://example.com/", + alt_text="example", + title="example", + ).title.type, + ) + + self.assertEqual( + "plain_text", + ImageBlock( + image_url="https://example.com/", + alt_text="example", + title={ + "type": "plain_text", + "text": "Please enjoy this photo of a kitten", + }, + ).title.type, + ) + + self.assertEqual( + "plain_text", + ImageBlock( + image_url="https://example.com/", + alt_text="example", + title=PlainTextObject(text="example"), + ).title.type, + ) + + with self.assertRaises(SlackObjectFormationError): + self.assertEqual( + "plain_text", + ImageBlock( + image_url="https://example.com/", + alt_text="example", + title={ + "type": "mrkdwn", + "text": "Please enjoy this photo of a kitten", + }, + ).title.type, + ) + + with self.assertRaises(SlackObjectFormationError): + self.assertEqual( + "plain_text", + ImageBlock( + image_url="https://example.com/", + alt_text="example", + title=MarkdownTextObject(text="example"), + ).title.type, + ) + + def test_json(self): + self.assertDictEqual( + { + "image_url": "https://example.com", + "alt_text": "not really an image", + "type": "image", + }, + ImageBlock(image_url="https://example.com", alt_text="not really an image").to_dict(), + ) + + def test_image_url_length(self): + with self.assertRaises(SlackObjectFormationError): + ImageBlock(image_url=STRING_3001_CHARS, alt_text="text").to_dict() + + def test_alt_text_length(self): + with self.assertRaises(SlackObjectFormationError): + ImageBlock(image_url="https://example.com", alt_text=STRING_3001_CHARS).to_dict() + + def test_title_length(self): + with self.assertRaises(SlackObjectFormationError): + ImageBlock(image_url="https://example.com", alt_text="text", title=STRING_3001_CHARS).to_dict() + + def test_slack_file(self): + self.assertDictEqual( + { + "slack_file": {"url": "https://example.com"}, + "alt_text": "not really an image", + "type": "image", + }, + ImageBlock(slack_file=SlackFile(url="https://example.com"), alt_text="not really an image").to_dict(), + ) + self.assertDictEqual( + { + "slack_file": {"id": "F11111"}, + "alt_text": "not really an image", + "type": "image", + }, + ImageBlock(slack_file=SlackFile(id="F11111"), alt_text="not really an image").to_dict(), + ) + self.assertDictEqual( + { + "slack_file": {"url": "https://example.com"}, + "alt_text": "not really an image", + "type": "image", + }, + ImageBlock(slack_file={"url": "https://example.com"}, alt_text="not really an image").to_dict(), + ) + self.assertDictEqual( + { + "slack_file": {"id": "F11111"}, + "alt_text": "not really an image", + "type": "image", + }, + ImageBlock(slack_file={"id": "F11111"}, alt_text="not really an image").to_dict(), + ) + + +# ---------------------------------------------- +# Actions +# ---------------------------------------------- + + +class ActionsBlockTests(unittest.TestCase): + def test_document_1(self): + input = { + "type": "actions", + "block_id": "actions1", + "elements": [ + { + "type": "static_select", + "placeholder": { + "type": "plain_text", + "text": "Which witch is the witchiest witch?", + }, + "action_id": "select_2", + "options": [ + { + "text": {"type": "plain_text", "text": "Matilda"}, + "value": "matilda", + }, + { + "text": {"type": "plain_text", "text": "Glinda"}, + "value": "glinda", + }, + { + "text": {"type": "plain_text", "text": "Granny Weatherwax"}, + "value": "grannyWeatherwax", + }, + { + "text": {"type": "plain_text", "text": "Hermione"}, + "value": "hermione", + }, + ], + }, + { + "type": "button", + "text": {"type": "plain_text", "text": "Cancel"}, + "value": "cancel", + "action_id": "button_1", + }, + ], + } + self.assertDictEqual(input, ActionsBlock(**input).to_dict()) + + def test_document_2(self): + input = { + "type": "actions", + "block_id": "actionblock789", + "elements": [ + { + "type": "datepicker", + "action_id": "datepicker123", + "initial_date": "1990-04-28", + "placeholder": {"type": "plain_text", "text": "Select a date"}, + }, + { + "type": "overflow", + "options": [ + { + "text": { + "type": "plain_text", + "text": "*this is plain_text text*", + }, + "value": "value-0", + }, + { + "text": { + "type": "plain_text", + "text": "*this is plain_text text*", + }, + "value": "value-1", + }, + { + "text": { + "type": "plain_text", + "text": "*this is plain_text text*", + }, + "value": "value-2", + }, + { + "text": { + "type": "plain_text", + "text": "*this is plain_text text*", + }, + "value": "value-3", + }, + { + "text": { + "type": "plain_text", + "text": "*this is plain_text text*", + }, + "value": "value-4", + }, + ], + "action_id": "overflow", + }, + { + "type": "button", + "text": {"type": "plain_text", "text": "Click Me"}, + "value": "click_me_123", + "action_id": "button", + }, + ], + } + self.assertDictEqual(input, ActionsBlock(**input).to_dict()) + + def test_json(self): + self.elements = [ + ButtonElement(text="Click me", action_id="reg_button", value="1"), + LinkButtonElement(text="URL Button", url="https://example.com"), + ] + self.dict_elements = [] + for e in self.elements: + self.dict_elements.append(e.to_dict()) + + self.assertDictEqual( + {"elements": self.dict_elements, "type": "actions"}, + ActionsBlock(elements=self.elements).to_dict(), + ) + with self.assertRaises(SlackObjectFormationError): + ActionsBlock(elements=self.elements * 13).to_dict() + + def test_element_parsing(self): + elements = [ + ButtonElement(text="Click me", action_id="reg_button", value="1"), + StaticSelectElement(options=[Option(value="SelectOption")]), + ImageElement(image_url="url", alt_text="alt-text"), + OverflowMenuElement(options=[Option(value="MenuOption1"), Option(value="MenuOption2")]), + ] + input = { + "type": "actions", + "block_id": "actionblock789", + "elements": [e.to_dict() for e in elements], + } + parsed_elements = ActionsBlock(**input).elements + self.assertEqual(len(elements), len(parsed_elements)) + for original, parsed in zip(elements, parsed_elements): + self.assertEqual(type(original), type(parsed)) + self.assertDictEqual(original.to_dict(), parsed.to_dict()) + + +# ---------------------------------------------- +# ContextActionsBlock +# ---------------------------------------------- + + +class ContextActionsBlockTests(unittest.TestCase): + def test_document(self): + input = { + "type": "context_actions", + "block_id": "context-actions-1", + "elements": [ + { + "type": "feedback_buttons", + "action_id": "feedback-action", + "positive_button": {"text": {"type": "plain_text", "text": "+1"}, "value": "positive"}, + "negative_button": {"text": {"type": "plain_text", "text": "-1"}, "value": "negative"}, + }, + { + "type": "icon_button", + "action_id": "delete-action", + "icon": "trash", + "text": {"type": "plain_text", "text": "Delete"}, + "value": "delete", + }, + ], + } + self.assertDictEqual(input, ContextActionsBlock(**input).to_dict()) + self.assertDictEqual(input, Block.parse(input).to_dict()) + + def test_with_feedback_buttons(self): + feedback_buttons = FeedbackButtonsElement( + action_id="feedback-action", + positive_button=FeedbackButtonObject(text="Good", value="positive"), + negative_button=FeedbackButtonObject(text="Bad", value="negative"), + ) + block = ContextActionsBlock(elements=[feedback_buttons]) + self.assertEqual(len(block.elements), 1) + self.assertEqual(block.elements[0].type, "feedback_buttons") + + def test_with_icon_button(self): + icon_button = IconButtonElement( + action_id="icon-action", icon="star", text=PlainTextObject(text="Favorite"), value="favorite" + ) + block = ContextActionsBlock(elements=[icon_button]) + self.assertEqual(len(block.elements), 1) + self.assertEqual(block.elements[0].type, "icon_button") + + +# ---------------------------------------------- +# Context +# ---------------------------------------------- + + +class ContextBlockTests(unittest.TestCase): + def test_document(self): + input = { + "type": "context", + "elements": [ + { + "type": "image", + "image_url": "https://image.freepik.com/free-photo/red-drawing-pin_1156-445.jpg", + "alt_text": "images", + }, + {"type": "mrkdwn", "text": "Location: **Dogpatch**"}, + ], + } + self.assertDictEqual(input, ContextBlock(**input).to_dict()) + + def test_basic_json(self): + self.elements = [ + ImageElement( + image_url="https://api.slack.com/img/blocks/bkb_template_images/palmtree.png", + alt_text="palmtree", + ), + PlainTextObject(text="Just text"), + ] + e = { + "elements": [ + { + "type": "image", + "image_url": "https://api.slack.com/img/blocks/bkb_template_images/palmtree.png", + "alt_text": "palmtree", + }, + {"type": "plain_text", "text": "Just text"}, + ], + "type": "context", + } + d = ContextBlock(elements=self.elements).to_dict() + self.assertDictEqual(e, d) + + with self.assertRaises(SlackObjectFormationError): + ContextBlock(elements=self.elements * 6).to_dict() + + +# ---------------------------------------------- +# Input +# ---------------------------------------------- + + +class InputBlockTests(unittest.TestCase): + def test_document(self): + blocks = [ + { + "type": "input", + "element": {"type": "plain_text_input"}, + "label": {"type": "plain_text", "text": "Label", "emoji": True}, + }, + { + "type": "input", + "element": {"type": "plain_text_input", "multiline": True}, + "label": {"type": "plain_text", "text": "Label", "emoji": True}, + }, + { + "type": "input", + "element": { + "type": "datepicker", + "initial_date": "1990-04-28", + "placeholder": { + "type": "plain_text", + "text": "Select a date", + "emoji": True, + }, + }, + "label": {"type": "plain_text", "text": "Label", "emoji": True}, + }, + { + "type": "input", + "element": { + "type": "channels_select", + "placeholder": { + "type": "plain_text", + "text": "Select a channel", + "emoji": True, + }, + }, + "label": {"type": "plain_text", "text": "Label", "emoji": True}, + }, + { + "type": "input", + "element": { + "type": "multi_users_select", + "placeholder": { + "type": "plain_text", + "text": "Select users", + "emoji": True, + }, + }, + "label": {"type": "plain_text", "text": "Label", "emoji": True}, + }, + { + "type": "input", + "element": { + "type": "checkboxes", + "options": [ + { + "text": { + "type": "plain_text", + "text": "*this is plain_text text*", + "emoji": True, + }, + "value": "value-0", + }, + { + "text": { + "type": "plain_text", + "text": "*this is plain_text text*", + "emoji": True, + }, + "value": "value-1", + }, + { + "text": { + "type": "plain_text", + "text": "*this is plain_text text*", + "emoji": True, + }, + "value": "value-2", + }, + ], + }, + "label": {"type": "plain_text", "text": "Label", "emoji": True}, + }, + { + "type": "input", + "element": { + "type": "plain_text_input", + }, + "label": { + "type": "plain_text", + "text": "Label", + "emoji": True, + }, + "hint": { + "type": "plain_text", + "text": "some hint", + "emoji": True, + }, + }, + { + "dispatch_action": True, + "type": "input", + "element": { + "type": "plain_text_input", + "action_id": "plain_text_input-action", + }, + "label": {"type": "plain_text", "text": "Label", "emoji": True}, + }, + ] + for input in blocks: + self.assertDictEqual(input, InputBlock(**input).to_dict()) + + +# ---------------------------------------------- +# File +# ---------------------------------------------- + + +class FileBlockTests(unittest.TestCase): + def test_document(self): + input = { + "type": "file", + "external_id": "ABCD1", + "source": "remote", + } + self.assertDictEqual(input, FileBlock(**input).to_dict()) + + +# ---------------------------------------------- +# Call +# ---------------------------------------------- + + +class CallBlockTests(unittest.TestCase): + def test_with_real_payload(self): + self.maxDiff = None + input = { + "type": "call", + "call_id": "R00000000", + "api_decoration_available": False, + "call": { + "v1": { + "id": "R00000000", + "app_id": "A00000000", + "app_icon_urls": { + "image_32": "https://www.example.com/", + "image_36": "https://www.example.com/", + "image_48": "https://www.example.com/", + "image_64": "https://www.example.com/", + "image_72": "https://www.example.com/", + "image_96": "https://www.example.com/", + "image_128": "https://www.example.com/", + "image_192": "https://www.example.com/", + "image_512": "https://www.example.com/", + "image_1024": "https://www.example.com/", + "image_original": "https://www.example.com/", + }, + "date_start": 12345, + "active_participants": [ + {"slack_id": "U00000000"}, + { + "slack_id": "U00000000", + "external_id": "", + "avatar_url": "https://www.example.com/", + "display_name": "", + }, + ], + "all_participants": [ + {"slack_id": "U00000000"}, + { + "slack_id": "U00000000", + "external_id": "", + "avatar_url": "https://www.example.com/", + "display_name": "", + }, + ], + "display_id": "", + "join_url": "https://www.example.com/", + "name": "", + "created_by": "U00000000", + "date_end": 12345, + "channels": ["C00000000"], + "is_dm_call": False, + "was_rejected": False, + "was_missed": False, + "was_accepted": False, + "has_ended": False, + "desktop_app_join_url": "https://www.example.com/", + } + }, + } + self.assertDictEqual(input, CallBlock(**input).to_dict()) + self.assertDictEqual(input, Block.parse(input).to_dict()) + + +# ---------------------------------------------- +# Header +# ---------------------------------------------- + + +class HeaderBlockTests(unittest.TestCase): + def test_document(self): + input = { + "type": "header", + "block_id": "budget-header", + "text": {"type": "plain_text", "text": "Budget Performance"}, + } + self.assertDictEqual(input, HeaderBlock(**input).to_dict()) + self.assertDictEqual(input, Block.parse(input).to_dict()) + + def test_text_length_150(self): + input = { + "type": "header", + "block_id": "budget-header", + "text": {"type": "plain_text", "text": "1234567890" * 15}, + } + HeaderBlock(**input).validate_json() + + def test_text_length_151(self): + input = { + "type": "header", + "block_id": "budget-header", + "text": {"type": "plain_text", "text": ("1234567890" * 15) + "1"}, + } + with self.assertRaises(SlackObjectFormationError): + HeaderBlock(**input).validate_json() + + +# ---------------------------------------------- +# MarkdownBlock +# ---------------------------------------------- + + +class MarkdownBlockTests(unittest.TestCase): + def test_document(self): + input = { + "type": "markdown", + "block_id": "introduction", + "text": "**Welcome!**", + } + self.assertDictEqual(input, MarkdownBlock(**input).to_dict()) + self.assertDictEqual(input, Block.parse(input).to_dict()) + + def test_text_length_12000(self): + input = { + "type": "markdown", + "block_id": "numbers", + "text": "1234567890" * 1200, + } + MarkdownBlock(**input).validate_json() + + def test_text_length_12001(self): + input = { + "type": "markdown", + "block_id": "numbers", + "text": "1234567890" * 1200 + "1", + } + with self.assertRaises(SlackObjectFormationError): + MarkdownBlock(**input).validate_json() + + +# ---------------------------------------------- +# Video +# ---------------------------------------------- + + +class VideoBlockTests(unittest.TestCase): + def test_document(self): + input = { + "type": "video", + "title": {"type": "plain_text", "text": "How to use Slack.", "emoji": True}, + "title_url": "https://www.youtube.com/watch?v=RRxQQxiM7AA", + "description": { + "type": "plain_text", + "text": "Slack is a new way to communicate with your team. " + "It's faster, better organized and more secure than email.", + "emoji": True, + }, + "video_url": "https://www.youtube.com/embed/RRxQQxiM7AA?feature=oembed&autoplay=1", + "alt_text": "How to use Slack?", + "thumbnail_url": "https://i.ytimg.com/vi/RRxQQxiM7AA/hqdefault.jpg", + "author_name": "Arcado Buendia", + "provider_name": "YouTube", + "provider_icon_url": "https://a.slack-edge.com/80588/img/unfurl_icons/youtube.png", + } + self.assertDictEqual(input, VideoBlock(**input).to_dict()) + self.assertDictEqual(input, Block.parse(input).to_dict()) + + def test_required(self): + input = { + "type": "video", + "title": {"type": "plain_text", "text": "How to use Slack.", "emoji": True}, + "video_url": "https://www.youtube.com/embed/RRxQQxiM7AA?feature=oembed&autoplay=1", + "alt_text": "How to use Slack?", + "thumbnail_url": "https://i.ytimg.com/vi/RRxQQxiM7AA/hqdefault.jpg", + } + VideoBlock(**input).validate_json() + + def test_required_error(self): + # title is missing + input = { + "type": "video", + "video_url": "https://www.youtube.com/embed/RRxQQxiM7AA?feature=oembed&autoplay=1", + "alt_text": "How to use Slack?", + "thumbnail_url": "https://i.ytimg.com/vi/RRxQQxiM7AA/hqdefault.jpg", + } + with self.assertRaises(SlackObjectFormationError): + VideoBlock(**input).validate_json() + + def test_title_length_199(self): + input = { + "type": "video", + "title": { + "type": "plain_text", + "text": "1234567890" * 19 + "123456789", + }, + "video_url": "https://www.youtube.com/embed/RRxQQxiM7AA?feature=oembed&autoplay=1", + "alt_text": "How to use Slack?", + "thumbnail_url": "https://i.ytimg.com/vi/RRxQQxiM7AA/hqdefault.jpg", + } + VideoBlock(**input).validate_json() + + def test_title_length_200(self): + input = { + "type": "video", + "title": { + "type": "plain_text", + "text": "1234567890" * 20, + }, + "video_url": "https://www.youtube.com/embed/RRxQQxiM7AA?feature=oembed&autoplay=1", + "alt_text": "How to use Slack?", + "thumbnail_url": "https://i.ytimg.com/vi/RRxQQxiM7AA/hqdefault.jpg", + } + with self.assertRaises(SlackObjectFormationError): + VideoBlock(**input).validate_json() + + +# ---------------------------------------------- +# RichTextBlock +# ---------------------------------------------- + + +class RichTextBlockTests(unittest.TestCase): + def test_document(self): + inputs = [ + { + "type": "rich_text", + "elements": [ + { + "type": "rich_text_section", + "elements": [{"type": "text", "text": "Hello there, I am a basic rich text block!"}], + } + ], + }, + { + "type": "rich_text", + "elements": [ + { + "type": "rich_text_section", + "elements": [ + {"type": "text", "text": "Hello there, "}, + {"type": "text", "text": "I am a bold rich text block!", "style": {"bold": True}}, + ], + } + ], + }, + { + "type": "rich_text", + "elements": [ + { + "type": "rich_text_section", + "elements": [ + {"type": "text", "text": "Hello there, "}, + {"type": "text", "text": "I am an italic rich text block!", "style": {"italic": True}}, + ], + } + ], + }, + { + "type": "rich_text", + "elements": [ + { + "type": "rich_text_section", + "elements": [ + {"type": "text", "text": "Hello there, "}, + {"type": "text", "text": "I am a strikethrough rich text block!", "style": {"strike": True}}, + ], + } + ], + }, + ] + for input in inputs: + self.assertDictEqual(input, RichTextBlock(**input).to_dict()) + + def test_complex(self): + self.maxDiff = None + dict_block = { + "type": "rich_text", + "block_id": "3Uk3Q", + "elements": [ + { + "type": "rich_text_section", + "elements": [ + {"type": "text", "text": "Hey!", "style": {"bold": True}}, + {"type": "text", "text": " this is "}, + {"type": "text", "text": "very", "style": {"strike": True}}, + {"type": "text", "text": " rich text "}, + {"type": "text", "text": "block", "style": {"code": True}}, + {"type": "text", "text": " "}, + {"type": "text", "text": "test", "style": {"italic": True}}, + {"type": "text", "text": " "}, + {"type": "text", "text": "links", "style": {"underline": True}}, + {"type": "link", "url": "https://slack.com", "text": "Slack website!"}, + ], + }, + { + "type": "rich_text_list", + "elements": [ + {"type": "rich_text_section", "elements": [{"type": "text", "text": "a"}]}, + {"type": "rich_text_section", "elements": [{"type": "text", "text": "b"}]}, + ], + "style": "ordered", + "indent": 0, + "border": 0, + }, + { + "type": "rich_text_list", + "elements": [{"type": "rich_text_section", "elements": [{"type": "text", "text": "bb"}]}], + "style": "ordered", + "indent": 1, + "border": 0, + }, + { + "type": "rich_text_list", + "elements": [{"type": "rich_text_section", "elements": [{"type": "text", "text": "BBB"}]}], + "style": "ordered", + "indent": 2, + "border": 0, + }, + { + "type": "rich_text_list", + "elements": [{"type": "rich_text_section", "elements": [{"type": "text", "text": "c"}]}], + "style": "ordered", + "indent": 0, + "offset": 2, + "border": 0, + }, + {"type": "rich_text_section", "elements": [{"type": "text", "text": "\n"}]}, + { + "type": "rich_text_list", + "elements": [ + {"type": "rich_text_section", "elements": [{"type": "text", "text": "todo"}]}, + {"type": "rich_text_section", "elements": [{"type": "text", "text": "todo"}]}, + {"type": "rich_text_section", "elements": [{"type": "text", "text": "todo"}]}, + ], + "style": "bullet", + "indent": 0, + "border": 0, + }, + {"type": "rich_text_section", "elements": [{"type": "text", "text": "\n"}]}, + {"type": "rich_text_quote", "elements": [{"type": "text", "text": "this is very important"}]}, + { + "type": "rich_text_preformatted", + "elements": [{"type": "text", "text": 'print("Hello world")'}], + "border": 0, + }, + { + "type": "rich_text_section", + "elements": [ + {"type": "user", "user_id": "WJC6QG0MS"}, + {"type": "text", "text": " "}, + {"type": "usergroup", "usergroup_id": "S01BL602YLU"}, + {"type": "text", "text": " "}, + {"type": "channel", "channel_id": "C02GD0YEHDJ"}, + { + "type": "date", + "timestamp": "1628633089", + "format": "{date_long}", + "url": "https://slack.com", + "fallback": "August 10, 2021", + }, + {"type": "date", "timestamp": "1720710212", "format": "{date_num} at {time}", "fallback": "timey"}, + { + "type": "date", + "timestamp": "1628633089", + "format": "{date_short_pretty}", + "url": "https://slack.com", + }, + { + "type": "date", + "timestamp": "1628633089", + "format": "{ago}", + }, + ], + }, + ], + } + self.assertDictEqual(dict_block, RichTextBlock(**dict_block).to_dict()) + self.assertDictEqual(dict_block, Block.parse(dict_block).to_dict()) + + _ = RichTextElementParts + class_block = RichTextBlock( + block_id="3Uk3Q", + elements=[ + RichTextSectionElement( + elements=[ + _.Text(text="Hey!", style=_.TextStyle(bold=True)), + _.Text(text=" this is "), + _.Text(text="very", style=_.TextStyle(strike=True)), + _.Text(text=" rich text "), + _.Text(text="block", style=_.TextStyle(code=True)), + _.Text(text=" "), + _.Text(text="test", style=_.TextStyle(italic=True)), + _.Text(text=" "), + _.Text(text="links", style=_.TextStyle(underline=True)), + _.Link(text="Slack website!", url="https://slack.com"), + ] + ), + RichTextListElement( + elements=[ + RichTextSectionElement(elements=[_.Text(text="a")]), + RichTextSectionElement(elements=[_.Text(text="b")]), + ], + style="ordered", + indent=0, + border=0, + ), + RichTextListElement( + elements=[RichTextSectionElement(elements=[_.Text(text="bb")])], + style="ordered", + indent=1, + border=0, + ), + RichTextListElement( + elements=[RichTextSectionElement(elements=[_.Text(text="BBB")])], + style="ordered", + indent=2, + border=0, + ), + RichTextListElement( + elements=[RichTextSectionElement(elements=[_.Text(text="c")])], + style="ordered", + indent=0, + offset=2, + border=0, + ), + RichTextSectionElement(elements=[_.Text(text="\n")]), + RichTextListElement( + elements=[ + RichTextSectionElement(elements=[_.Text(text="todo")]), + RichTextSectionElement(elements=[_.Text(text="todo")]), + RichTextSectionElement(elements=[_.Text(text="todo")]), + ], + style="bullet", + indent=0, + border=0, + ), + RichTextSectionElement(elements=[_.Text(text="\n")]), + RichTextQuoteElement(elements=[_.Text(text="this is very important")]), + RichTextPreformattedElement( + elements=[_.Text(text='print("Hello world")')], + border=0, + ), + RichTextSectionElement( + elements=[ + _.User(user_id="WJC6QG0MS"), + _.Text(text=" "), + _.UserGroup(usergroup_id="S01BL602YLU"), + _.Text(text=" "), + _.Channel(channel_id="C02GD0YEHDJ"), + _.Date( + timestamp="1628633089", format="{date_long}", url="https://slack.com", fallback="August 10, 2021" + ), + _.Date(timestamp="1720710212", format="{date_num} at {time}", fallback="timey"), + _.Date(timestamp="1628633089", format="{date_short_pretty}", url="https://slack.com"), + _.Date(timestamp="1628633089", format="{ago}"), + ] + ), + ], + ) + self.assertDictEqual(dict_block, class_block.to_dict()) + + def test_elements_are_parsed(self): + dict_block = { + "type": "rich_text", + "elements": [ + { + "type": "rich_text_section", + "elements": [{"type": "text", "text": "Hello there, I am a basic rich text block!"}], + }, + { + "type": "rich_text_quote", + "elements": [{"type": "text", "text": "this is very important"}], + }, + { + "type": "rich_text_preformatted", + "elements": [{"type": "text", "text": 'print("Hello world")'}], + }, + { + "type": "rich_text_list", + "elements": [ + {"type": "rich_text_section", "elements": [{"type": "text", "text": "a"}]}, + ], + }, + ], + } + block = RichTextBlock(**dict_block) + self.assertIsInstance(block.elements[0], RichTextSectionElement) + self.assertIsInstance(block.elements[0].elements[0], RichTextElementParts.Text) + self.assertIsInstance(block.elements[1], RichTextQuoteElement) + self.assertIsInstance(block.elements[1].elements[0], RichTextElementParts.Text) + self.assertIsInstance(block.elements[2], RichTextPreformattedElement) + self.assertIsInstance(block.elements[2].elements[0], RichTextElementParts.Text) + self.assertIsInstance(block.elements[3], RichTextListElement) + self.assertIsInstance(block.elements[3].elements[0], RichTextSectionElement) + self.assertIsInstance(block.elements[3].elements[0].elements[0], RichTextElementParts.Text) + + def test_parsing_empty_block_elements(self): + empty_element_block = { + "block_id": "my-block", + "type": "rich_text", + "elements": [ + {"type": "rich_text_section", "elements": []}, + {"type": "rich_text_list", "style": "bullet", "elements": []}, + {"type": "rich_text_preformatted", "elements": []}, + {"type": "rich_text_quote", "elements": []}, + ], + } + block = RichTextBlock(**empty_element_block) + self.assertIsInstance(block.elements[0], RichTextSectionElement) + self.assertIsNotNone(block.elements[0].elements) + self.assertIsNotNone(block.elements[1].elements) + self.assertIsNotNone(block.elements[2].elements) + self.assertIsNotNone(block.elements[3].elements) + + block_dict = block.to_dict() + self.assertIsNotNone(block_dict["elements"][0].get("elements")) + self.assertIsNotNone(block_dict["elements"][1].get("elements")) + self.assertIsNotNone(block_dict["elements"][2].get("elements")) + self.assertIsNotNone(block_dict["elements"][3].get("elements")) + + +# ---------------------------------------------- +# RawTextObject +# ---------------------------------------------- + + +class RawTextObjectTests(unittest.TestCase): + def test_basic_creation(self): + """Test basic RawTextObject creation""" + obj = RawTextObject(text="Hello") + expected = {"type": "raw_text", "text": "Hello"} + self.assertDictEqual(expected, obj.to_dict()) + + def test_from_str(self): + """Test RawTextObject.from_str() helper""" + obj = RawTextObject.from_str("Test text") + expected = {"type": "raw_text", "text": "Test text"} + self.assertDictEqual(expected, obj.to_dict()) + + def test_direct_from_string(self): + """Test RawTextObject.direct_from_string() helper""" + result = RawTextObject.direct_from_string("Direct text") + expected = {"type": "raw_text", "text": "Direct text"} + self.assertDictEqual(expected, result) + + def test_text_length_validation_min(self): + """Test that empty text fails validation""" + with self.assertRaises(SlackObjectFormationError): + RawTextObject(text="").to_dict() + + def test_text_length_validation_at_min(self): + """Test that text with 1 character passes validation""" + obj = RawTextObject(text="a") + obj.to_dict() # Should not raise + + def test_attributes(self): + """Test that RawTextObject only has text and type attributes""" + obj = RawTextObject(text="Test") + self.assertEqual(obj.attributes, {"text", "type"}) + # Should not have emoji attribute like PlainTextObject + self.assertNotIn("emoji", obj.to_dict()) + + +# ---------------------------------------------- +# Table +# ---------------------------------------------- + + +class TableBlockTests(unittest.TestCase): + def test_document(self): + """Test basic table block from Slack documentation example""" + input = { + "type": "table", + "column_settings": [{"is_wrapped": True}, {"align": "right"}], + "rows": [ + [{"type": "raw_text", "text": "Header A"}, {"type": "raw_text", "text": "Header B"}], + [{"type": "raw_text", "text": "Data 1A"}, {"type": "raw_text", "text": "Data 1B"}], + [{"type": "raw_text", "text": "Data 2A"}, {"type": "raw_text", "text": "Data 2B"}], + ], + } + self.assertDictEqual(input, TableBlock(**input).to_dict()) + self.assertDictEqual(input, Block.parse(input).to_dict()) + + def test_with_rich_text(self): + """Test table block with rich_text cells""" + input = { + "type": "table", + "column_settings": [{"is_wrapped": True}, {"align": "right"}], + "rows": [ + [{"type": "raw_text", "text": "Header A"}, {"type": "raw_text", "text": "Header B"}], + [ + {"type": "raw_text", "text": "Data 1A"}, + { + "type": "rich_text", + "elements": [ + { + "type": "rich_text_section", + "elements": [{"text": "Data 1B", "type": "link", "url": "https://slack.com"}], + } + ], + }, + ], + ], + } + self.assertDictEqual(input, TableBlock(**input).to_dict()) + self.assertDictEqual(input, Block.parse(input).to_dict()) + + def test_minimal_table(self): + """Test table with only required fields""" + input = { + "type": "table", + "rows": [[{"type": "raw_text", "text": "Cell"}]], + } + self.assertDictEqual(input, TableBlock(**input).to_dict()) + + def test_with_block_id(self): + """Test table block with block_id""" + input = { + "type": "table", + "block_id": "table-123", + "rows": [ + [{"type": "raw_text", "text": "A"}, {"type": "raw_text", "text": "B"}], + [{"type": "raw_text", "text": "1"}, {"type": "raw_text", "text": "2"}], + ], + } + self.assertDictEqual(input, TableBlock(**input).to_dict()) + + def test_column_settings_variations(self): + """Test various column_settings configurations""" + # Left align + input1 = { + "type": "table", + "column_settings": [{"align": "left"}], + "rows": [[{"type": "raw_text", "text": "Left"}]], + } + self.assertDictEqual(input1, TableBlock(**input1).to_dict()) + + # Center align + input2 = { + "type": "table", + "column_settings": [{"align": "center"}], + "rows": [[{"type": "raw_text", "text": "Center"}]], + } + self.assertDictEqual(input2, TableBlock(**input2).to_dict()) + + # With wrapping + input3 = { + "type": "table", + "column_settings": [{"is_wrapped": False}], + "rows": [[{"type": "raw_text", "text": "No wrap"}]], + } + self.assertDictEqual(input3, TableBlock(**input3).to_dict()) + + # Combined settings + input4 = { + "type": "table", + "column_settings": [{"align": "center", "is_wrapped": True}], + "rows": [[{"type": "raw_text", "text": "Both"}]], + } + self.assertDictEqual(input4, TableBlock(**input4).to_dict()) + + def test_column_settings_with_none(self): + """Test column_settings with None to skip columns""" + input = { + "type": "table", + "column_settings": [{"align": "left"}, None, {"align": "right"}], + "rows": [ + [ + {"type": "raw_text", "text": "Left"}, + {"type": "raw_text", "text": "Default"}, + {"type": "raw_text", "text": "Right"}, + ] + ], + } + self.assertDictEqual(input, TableBlock(**input).to_dict()) + + def test_rows_validation(self): + """Test that rows validation works correctly""" + # Empty rows should fail validation + with self.assertRaises(SlackObjectFormationError): + TableBlock(rows=[]).to_dict() + + def test_multi_row_table(self): + """Test table with multiple rows""" + input = { + "type": "table", + "rows": [ + [{"type": "raw_text", "text": "Name"}, {"type": "raw_text", "text": "Age"}], + [{"type": "raw_text", "text": "Alice"}, {"type": "raw_text", "text": "30"}], + [{"type": "raw_text", "text": "Bob"}, {"type": "raw_text", "text": "25"}], + [{"type": "raw_text", "text": "Charlie"}, {"type": "raw_text", "text": "35"}], + ], + } + block = TableBlock(**input) + self.assertEqual(len(block.rows), 4) + self.assertDictEqual(input, block.to_dict()) + + def test_with_raw_text_object_helper(self): + """Test table using RawTextObject helper class""" + # Create table using RawTextObject helper + block = TableBlock( + rows=[ + [RawTextObject(text="Product").to_dict(), RawTextObject(text="Price").to_dict()], + [RawTextObject(text="Widget").to_dict(), RawTextObject(text="$10").to_dict()], + [RawTextObject(text="Gadget").to_dict(), RawTextObject(text="$20").to_dict()], + ], + column_settings=[{"is_wrapped": True}, {"align": "right"}], + ) + + expected = { + "type": "table", + "column_settings": [{"is_wrapped": True}, {"align": "right"}], + "rows": [ + [{"type": "raw_text", "text": "Product"}, {"type": "raw_text", "text": "Price"}], + [{"type": "raw_text", "text": "Widget"}, {"type": "raw_text", "text": "$10"}], + [{"type": "raw_text", "text": "Gadget"}, {"type": "raw_text", "text": "$20"}], + ], + } + self.assertDictEqual(expected, block.to_dict()) diff --git a/tests/slack_sdk/models/test_dialoags.py b/tests/slack_sdk/models/test_dialoags.py new file mode 100644 index 000000000..ca2544a8a --- /dev/null +++ b/tests/slack_sdk/models/test_dialoags.py @@ -0,0 +1,326 @@ +import unittest +from copy import copy + +from slack_sdk.errors import SlackObjectFormationError +from slack_sdk.models.blocks import Option +from slack_sdk.models.dialoags import ( + DialogChannelSelector, + DialogConversationSelector, + DialogExternalSelector, + DialogStaticSelector, + DialogTextArea, + DialogTextField, + DialogUserSelector, + DialogBuilder, +) +from . import STRING_3001_CHARS, STRING_301_CHARS, STRING_51_CHARS + +TextComponents = {DialogTextField, DialogTextArea} + + +class CommonTextComponentTests(unittest.TestCase): + def test_json_validators(self): + for component in TextComponents: + with self.subTest(f"Component: {component}"): + with self.assertRaises(SlackObjectFormationError, msg="name length"): + component(name=STRING_301_CHARS, label="label ").to_dict() + + with self.assertRaises(SlackObjectFormationError, msg="label length"): + component(name="dialog", label=STRING_51_CHARS).to_dict() + + with self.assertRaises(SlackObjectFormationError, msg="placeholder length"): + component(name="dialog", label="Dialog", placeholder=STRING_301_CHARS).to_dict() + + with self.assertRaises(SlackObjectFormationError, msg="hint length"): + component(name="dialog", label="Dialog", hint=STRING_301_CHARS).to_dict() + + with self.assertRaises(SlackObjectFormationError, msg="value length"): + component(name="dialog", label="Dialog", value=STRING_3001_CHARS).to_dict() + + with self.assertRaises(SlackObjectFormationError, msg="min_length out of bounds"): + component( + name="dialog", + label="Dialog", + min_length=component.max_value_length + 1, + ).to_dict() + + with self.assertRaises(SlackObjectFormationError, msg="max_length out of bounds"): + component( + name="dialog", + label="Dialog", + max_length=component.max_value_length + 1, + ).to_dict() + + with self.assertRaises(SlackObjectFormationError, msg="min_length > max length"): + component(name="dialog", label="Dialog", min_length=100, max_length=50).to_dict() + + with self.assertRaises(SlackObjectFormationError, msg="subtype invalid"): + component(name="dialog", label="Dialog", subtype="abcdefg").to_dict() + + +class TextFieldComponentTests(unittest.TestCase): + def test_json(self): + self.assertDictEqual( + DialogTextField(name="dialog", label="Dialog").to_dict(), + { + "name": "dialog", + "label": "Dialog", + "min_length": 0, + "max_length": 150, + "optional": False, + "type": "text", + }, + ) + + def test_basic_json(self): + self.assertDictEqual( + DialogTextField( + name="dialog", + label="Dialog", + optional=True, + hint="Some hint", + max_length=100, + min_length=20, + ).to_dict(), + { + "min_length": 20, + "max_length": 100, + "name": "dialog", + "optional": True, + "label": "Dialog", + "type": "text", + "hint": "Some hint", + }, + ) + + +class TextAreaComponentTests(unittest.TestCase): + def test_basic_json_formation(self): + self.assertDictEqual( + DialogTextArea(name="dialog", label="Dialog").to_dict(), + { + "min_length": 0, + "max_length": 3000, + "name": "dialog", + "optional": False, + "label": "Dialog", + "type": "textarea", + }, + ) + + def test_complex_json_formation(self): + self.assertDictEqual( + DialogTextArea( + name="dialog", + label="Dialog", + optional=True, + hint="Some hint", + max_length=500, + min_length=100, + ).to_dict(), + { + "min_length": 100, + "max_length": 500, + "name": "dialog", + "optional": True, + "label": "Dialog", + "type": "textarea", + "hint": "Some hint", + }, + ) + + +class StaticDropdownTests(unittest.TestCase): + def test_basic_json_formation(self): + options = [ + Option.from_single_value("one"), + Option.from_single_value("two"), + Option.from_single_value("three"), + ] + self.assertDictEqual( + DialogStaticSelector(name="dialog", label="Dialog", options=options).to_dict(), + { + "optional": False, + "label": "Dialog", + "type": "select", + "name": "dialog", + "options": [ + {"label": "one", "value": "one"}, + {"label": "two", "value": "two"}, + {"label": "three", "value": "three"}, + ], + "data_source": "static", + }, + ) + + +class DynamicSelectorTests(unittest.TestCase): + selectors = {DialogUserSelector, DialogChannelSelector, DialogConversationSelector} + + def setUp(self) -> None: + self.selected_opt = Option.from_single_value("U12345") + + def test_json(self): + self.maxDiff = None + for component in self.selectors: + with self.subTest(msg=f"{component} json formation test"): + self.assertDictEqual( + component(name="select_1", label="selector_1").to_dict(), + { + "name": "select_1", + "label": "selector_1", + "type": "select", + "optional": False, + "data_source": component.data_source, + }, + ) + + passing_obj = component(name="select_1", label="selector_1", value=self.selected_opt).to_dict() + + passing_str = component(name="select_1", label="selector_1", value="U12345").to_dict() + + expected = { + "name": "select_1", + "label": "selector_1", + "type": "select", + "optional": False, + "data_source": component.data_source, + "value": "U12345", + } + self.assertDictEqual(passing_obj, expected) + self.assertDictEqual(passing_str, expected) + + +class ExternalSelectorTests(unittest.TestCase): + def test_basic_json_formation(self): + o = Option.from_single_value("one") + self.assertDictEqual( + DialogExternalSelector( + name="dialog", + label="Dialog", + value=o, + min_query_length=3, + optional=True, + placeholder="something", + ).to_dict(), + { + "optional": True, + "label": "Dialog", + "type": "select", + "name": "dialog", + "min_query_length": 3, + "placeholder": "something", + "selected_options": [o.to_dict("dialog")], + "data_source": "external", + }, + ) + + +class DialogBuilderTests(unittest.TestCase): + def setUp(self) -> None: + self.builder = ( + DialogBuilder() + .title("Dialog Title") + .callback_id("function_123") + .submit_label("SubmitDialog") + .notify_on_cancel(True) + .text_field( + name="signature", + label="Signature", + optional=True, + hint="Enter your signature", + ) + .text_area(name="message", label="Message", hint="Enter message to broadcast") + .conversation_selector(name="target", label="Choose Target") + ) + + def test_basic_methods(self): + self.assertEqual(self.builder._title, "Dialog Title") + self.assertEqual(self.builder._callback_id, "function_123") + self.assertEqual(self.builder._submit_label, "SubmitDialog") + self.assertTrue(self.builder._notify_on_cancel) + + def test_element_appending(self): + text_field, text_area, dropdown = self.builder._elements + + self.assertEqual(text_field.type, "text") + self.assertEqual(text_field.name, "signature") + self.assertEqual(text_field.label, "Signature") + self.assertTrue(text_field.optional) + self.assertEqual(text_field.hint, "Enter your signature") + + self.assertEqual(text_area.type, "textarea") + self.assertEqual(text_area.name, "message") + self.assertEqual(text_area.label, "Message") + self.assertEqual(text_area.hint, "Enter message to broadcast") + + self.assertEqual(dropdown.type, "select") + self.assertEqual(dropdown.name, "target") + self.assertEqual(dropdown.label, "Choose Target") + self.assertEqual(dropdown.data_source, "conversations") + + def test_build_without_errors(self): + valid = { + "title": "Dialog Title", + "callback_id": "function_123", + "elements": [ + { + "hint": "Enter your signature", + "min_length": 0, + "label": "Signature", + "name": "signature", + "optional": True, + "max_length": 150, + "type": "text", + }, + { + "hint": "Enter message to broadcast", + "min_length": 0, + "label": "Message", + "name": "message", + "optional": False, + "max_length": 3000, + "type": "textarea", + }, + { + "type": "select", + "label": "Choose Target", + "name": "target", + "optional": False, + "data_source": "conversations", + }, + ], + "notify_on_cancel": True, + "submit_label": "SubmitDialog", + } + + self.assertDictEqual(self.builder.to_dict(), valid) + + def test_build_validation(self): + empty_title = copy(self.builder) + # noinspection PyTypeChecker + empty_title.title(None) + with self.assertRaises(SlackObjectFormationError): + empty_title.to_dict() + + too_long_title = copy(self.builder) + too_long_title.title(STRING_51_CHARS) + with self.assertRaises(SlackObjectFormationError): + too_long_title.to_dict() + + empty_callback = copy(self.builder) + # noinspection PyTypeChecker + empty_callback.callback_id(None) + with self.assertRaises(SlackObjectFormationError): + empty_callback.to_dict() + + empty_dialog = copy(self.builder) + empty_dialog._elements = [] + with self.assertRaises(SlackObjectFormationError): + empty_dialog.to_dict() + + overfull_dialog = copy(self.builder) + for i in range(8): + overfull_dialog.text_field(name=f"element {i}", label="overflow") + with self.assertRaises(SlackObjectFormationError): + overfull_dialog.to_dict() diff --git a/tests/slack_sdk/models/test_dialogs.py b/tests/slack_sdk/models/test_dialogs.py new file mode 100644 index 000000000..3dd12fe76 --- /dev/null +++ b/tests/slack_sdk/models/test_dialogs.py @@ -0,0 +1,326 @@ +import unittest +from copy import copy + +from slack_sdk.errors import SlackObjectFormationError +from slack_sdk.models.blocks import Option +from slack_sdk.models.dialogs import ( + DialogChannelSelector, + DialogConversationSelector, + DialogExternalSelector, + DialogStaticSelector, + DialogTextArea, + DialogTextField, + DialogUserSelector, + DialogBuilder, +) +from . import STRING_3001_CHARS, STRING_301_CHARS, STRING_51_CHARS + +TextComponents = {DialogTextField, DialogTextArea} + + +class CommonTextComponentTests(unittest.TestCase): + def test_json_validators(self): + for component in TextComponents: + with self.subTest(f"Component: {component}"): + with self.assertRaises(SlackObjectFormationError, msg="name length"): + component(name=STRING_301_CHARS, label="label ").to_dict() + + with self.assertRaises(SlackObjectFormationError, msg="label length"): + component(name="dialog", label=STRING_51_CHARS).to_dict() + + with self.assertRaises(SlackObjectFormationError, msg="placeholder length"): + component(name="dialog", label="Dialog", placeholder=STRING_301_CHARS).to_dict() + + with self.assertRaises(SlackObjectFormationError, msg="hint length"): + component(name="dialog", label="Dialog", hint=STRING_301_CHARS).to_dict() + + with self.assertRaises(SlackObjectFormationError, msg="value length"): + component(name="dialog", label="Dialog", value=STRING_3001_CHARS).to_dict() + + with self.assertRaises(SlackObjectFormationError, msg="min_length out of bounds"): + component( + name="dialog", + label="Dialog", + min_length=component.max_value_length + 1, + ).to_dict() + + with self.assertRaises(SlackObjectFormationError, msg="max_length out of bounds"): + component( + name="dialog", + label="Dialog", + max_length=component.max_value_length + 1, + ).to_dict() + + with self.assertRaises(SlackObjectFormationError, msg="min_length > max length"): + component(name="dialog", label="Dialog", min_length=100, max_length=50).to_dict() + + with self.assertRaises(SlackObjectFormationError, msg="subtype invalid"): + component(name="dialog", label="Dialog", subtype="abcdefg").to_dict() + + +class TextFieldComponentTests(unittest.TestCase): + def test_json(self): + self.assertDictEqual( + DialogTextField(name="dialog", label="Dialog").to_dict(), + { + "name": "dialog", + "label": "Dialog", + "min_length": 0, + "max_length": 150, + "optional": False, + "type": "text", + }, + ) + + def test_basic_json(self): + self.assertDictEqual( + DialogTextField( + name="dialog", + label="Dialog", + optional=True, + hint="Some hint", + max_length=100, + min_length=20, + ).to_dict(), + { + "min_length": 20, + "max_length": 100, + "name": "dialog", + "optional": True, + "label": "Dialog", + "type": "text", + "hint": "Some hint", + }, + ) + + +class TextAreaComponentTests(unittest.TestCase): + def test_basic_json_formation(self): + self.assertDictEqual( + DialogTextArea(name="dialog", label="Dialog").to_dict(), + { + "min_length": 0, + "max_length": 3000, + "name": "dialog", + "optional": False, + "label": "Dialog", + "type": "textarea", + }, + ) + + def test_complex_json_formation(self): + self.assertDictEqual( + DialogTextArea( + name="dialog", + label="Dialog", + optional=True, + hint="Some hint", + max_length=500, + min_length=100, + ).to_dict(), + { + "min_length": 100, + "max_length": 500, + "name": "dialog", + "optional": True, + "label": "Dialog", + "type": "textarea", + "hint": "Some hint", + }, + ) + + +class StaticDropdownTests(unittest.TestCase): + def test_basic_json_formation(self): + options = [ + Option.from_single_value("one"), + Option.from_single_value("two"), + Option.from_single_value("three"), + ] + self.assertDictEqual( + DialogStaticSelector(name="dialog", label="Dialog", options=options).to_dict(), + { + "optional": False, + "label": "Dialog", + "type": "select", + "name": "dialog", + "options": [ + {"label": "one", "value": "one"}, + {"label": "two", "value": "two"}, + {"label": "three", "value": "three"}, + ], + "data_source": "static", + }, + ) + + +class DynamicSelectorTests(unittest.TestCase): + selectors = {DialogUserSelector, DialogChannelSelector, DialogConversationSelector} + + def setUp(self) -> None: + self.selected_opt = Option.from_single_value("U12345") + + def test_json(self): + self.maxDiff = None + for component in self.selectors: + with self.subTest(msg=f"{component} json formation test"): + self.assertDictEqual( + component(name="select_1", label="selector_1").to_dict(), + { + "name": "select_1", + "label": "selector_1", + "type": "select", + "optional": False, + "data_source": component.data_source, + }, + ) + + passing_obj = component(name="select_1", label="selector_1", value=self.selected_opt).to_dict() + + passing_str = component(name="select_1", label="selector_1", value="U12345").to_dict() + + expected = { + "name": "select_1", + "label": "selector_1", + "type": "select", + "optional": False, + "data_source": component.data_source, + "value": "U12345", + } + self.assertDictEqual(passing_obj, expected) + self.assertDictEqual(passing_str, expected) + + +class ExternalSelectorTests(unittest.TestCase): + def test_basic_json_formation(self): + o = Option.from_single_value("one") + self.assertDictEqual( + DialogExternalSelector( + name="dialog", + label="Dialog", + value=o, + min_query_length=3, + optional=True, + placeholder="something", + ).to_dict(), + { + "optional": True, + "label": "Dialog", + "type": "select", + "name": "dialog", + "min_query_length": 3, + "placeholder": "something", + "selected_options": [o.to_dict("dialog")], + "data_source": "external", + }, + ) + + +class DialogBuilderTests(unittest.TestCase): + def setUp(self) -> None: + self.builder = ( + DialogBuilder() + .title("Dialog Title") + .callback_id("function_123") + .submit_label("SubmitDialog") + .notify_on_cancel(True) + .text_field( + name="signature", + label="Signature", + optional=True, + hint="Enter your signature", + ) + .text_area(name="message", label="Message", hint="Enter message to broadcast") + .conversation_selector(name="target", label="Choose Target") + ) + + def test_basic_methods(self): + self.assertEqual(self.builder._title, "Dialog Title") + self.assertEqual(self.builder._callback_id, "function_123") + self.assertEqual(self.builder._submit_label, "SubmitDialog") + self.assertTrue(self.builder._notify_on_cancel) + + def test_element_appending(self): + text_field, text_area, dropdown = self.builder._elements + + self.assertEqual(text_field.type, "text") + self.assertEqual(text_field.name, "signature") + self.assertEqual(text_field.label, "Signature") + self.assertTrue(text_field.optional) + self.assertEqual(text_field.hint, "Enter your signature") + + self.assertEqual(text_area.type, "textarea") + self.assertEqual(text_area.name, "message") + self.assertEqual(text_area.label, "Message") + self.assertEqual(text_area.hint, "Enter message to broadcast") + + self.assertEqual(dropdown.type, "select") + self.assertEqual(dropdown.name, "target") + self.assertEqual(dropdown.label, "Choose Target") + self.assertEqual(dropdown.data_source, "conversations") + + def test_build_without_errors(self): + valid = { + "title": "Dialog Title", + "callback_id": "function_123", + "elements": [ + { + "hint": "Enter your signature", + "min_length": 0, + "label": "Signature", + "name": "signature", + "optional": True, + "max_length": 150, + "type": "text", + }, + { + "hint": "Enter message to broadcast", + "min_length": 0, + "label": "Message", + "name": "message", + "optional": False, + "max_length": 3000, + "type": "textarea", + }, + { + "type": "select", + "label": "Choose Target", + "name": "target", + "optional": False, + "data_source": "conversations", + }, + ], + "notify_on_cancel": True, + "submit_label": "SubmitDialog", + } + + self.assertDictEqual(self.builder.to_dict(), valid) + + def test_build_validation(self): + empty_title = copy(self.builder) + # noinspection PyTypeChecker + empty_title.title(None) + with self.assertRaises(SlackObjectFormationError): + empty_title.to_dict() + + too_long_title = copy(self.builder) + too_long_title.title(STRING_51_CHARS) + with self.assertRaises(SlackObjectFormationError): + too_long_title.to_dict() + + empty_callback = copy(self.builder) + # noinspection PyTypeChecker + empty_callback.callback_id(None) + with self.assertRaises(SlackObjectFormationError): + empty_callback.to_dict() + + empty_dialog = copy(self.builder) + empty_dialog._elements = [] + with self.assertRaises(SlackObjectFormationError): + empty_dialog.to_dict() + + overfull_dialog = copy(self.builder) + for i in range(8): + overfull_dialog.text_field(name=f"element {i}", label="overflow") + with self.assertRaises(SlackObjectFormationError): + overfull_dialog.to_dict() diff --git a/tests/slack_sdk/models/test_elements.py b/tests/slack_sdk/models/test_elements.py new file mode 100644 index 000000000..2985228a2 --- /dev/null +++ b/tests/slack_sdk/models/test_elements.py @@ -0,0 +1,1416 @@ +import unittest + +from slack_sdk.errors import SlackObjectFormationError +from slack_sdk.models.blocks import ( + BlockElement, + ButtonElement, + ChannelMultiSelectElement, + ChannelSelectElement, + CheckboxesElement, + ConfirmObject, + ConversationMultiSelectElement, + ConversationSelectElement, + DatePickerElement, + ExternalDataMultiSelectElement, + ExternalDataSelectElement, + FeedbackButtonsElement, + IconButtonElement, + ImageElement, + InputInteractiveElement, + InteractiveElement, + LinkButtonElement, + Option, + OverflowMenuElement, + PlainTextInputElement, + PlainTextObject, + RadioButtonsElement, + RichTextBlock, + StaticMultiSelectElement, + StaticSelectElement, + TimePickerElement, + UserMultiSelectElement, + UserSelectElement, +) +from slack_sdk.models.blocks.basic_components import SlackFile +from slack_sdk.models.blocks.block_elements import ( + DateTimePickerElement, + EmailInputElement, + FileInputElement, + NumberInputElement, + RichTextElementParts, + RichTextInputElement, + RichTextSectionElement, + UrlInputElement, + WorkflowButtonElement, +) + +from . import STRING_301_CHARS, STRING_3001_CHARS + + +class BlockElementTests(unittest.TestCase): + def test_eq(self): + self.assertEqual(BlockElement(), BlockElement()) + self.assertEqual(BlockElement(type="test"), BlockElement(type="test")) + self.assertNotEqual(BlockElement(type="test"), BlockElement(type="another test")) + + def test_parse_timepicker(self): + timepicker = BlockElement.parse( + { + "type": "timepicker", + "action_id": "timepicker123", + "initial_time": "11:40", + "placeholder": { + "type": "plain_text", + "text": "Select a time", + }, + } + ) + self.assertIsNotNone(timepicker) + self.assertEqual(timepicker.type, TimePickerElement.type) + + +# ------------------------------------------------- +# Interactive Elements +# ------------------------------------------------- + + +class InteractiveElementTests(unittest.TestCase): + def test_with_interactive_element(self): + input = {"type": "plain_text_input", "action_id": "plain_input"} + # Any properties should not be lost + self.assertDictEqual(input, InteractiveElement(**input).to_dict()) + + def test_with_input_interactive_element(self): + input = { + "type": "plain_text_input", + "action_id": "plain_input", + "placeholder": {"type": "plain_text", "text": "Enter some plain text"}, + "focus_on_load": True, + } + # Any properties should not be lost + self.assertDictEqual(input, InputInteractiveElement(**input).to_dict()) + + +class ButtonElementTests(unittest.TestCase): + def test_document_1(self): + input = { + "type": "button", + "text": {"type": "plain_text", "text": "Click Me"}, + "value": "click_me_123", + "action_id": "button", + } + self.assertDictEqual(input, ButtonElement(**input).to_dict()) + + def test_document_2(self): + input = { + "type": "button", + "text": {"type": "plain_text", "text": "Save"}, + "style": "primary", + "value": "click_me_123", + "action_id": "button", + } + self.assertDictEqual(input, ButtonElement(**input).to_dict()) + + def test_document_3(self): + input = { + "type": "button", + "text": {"type": "plain_text", "text": "Link Button"}, + "url": "https://docs.slack.dev/block-kit/", + } + self.assertDictEqual(input, ButtonElement(**input).to_dict()) + self.assertDictEqual(input, LinkButtonElement(**input).to_dict()) + + def test_document_4(self): + input = { + "type": "button", + "text": {"type": "plain_text", "text": "Save"}, + "style": "primary", + "value": "click_me_123", + "action_id": "button", + "accessibility_label": "This label will be read out by screen readers", + } + self.assertDictEqual(input, ButtonElement(**input).to_dict()) + + def test_json(self): + self.assertDictEqual( + { + "text": {"emoji": True, "text": "button text", "type": "plain_text"}, + "action_id": "some_button", + "value": "button_123", + "type": "button", + }, + ButtonElement(text="button text", action_id="some_button", value="button_123").to_dict(), + ) + + confirm = ConfirmObject(title="really?", text="are you sure?") + self.assertDictEqual( + { + "text": {"emoji": True, "text": "button text", "type": "plain_text"}, + "action_id": "some_button", + "value": "button_123", + "type": "button", + "style": "primary", + "confirm": confirm.to_dict(), + }, + ButtonElement( + text="button text", + action_id="some_button", + value="button_123", + style="primary", + confirm=confirm, + ).to_dict(), + ) + + def test_text_length(self): + with self.assertRaises(SlackObjectFormationError): + ButtonElement(text=STRING_301_CHARS, action_id="button", value="click_me").to_dict() + + def test_action_id_length(self): + with self.assertRaises(SlackObjectFormationError): + ButtonElement(text="test", action_id="1234567890" * 26, value="click_me").to_dict() + + def test_value_length(self): + with self.assertRaises(SlackObjectFormationError): + ButtonElement(text="Button", action_id="button", value=STRING_3001_CHARS).to_dict() + + def test_invalid_style(self): + with self.assertRaises(SlackObjectFormationError): + ButtonElement(text="Button", action_id="button", value="button", style="invalid").to_dict() + + def test_accessibility_label_length(self): + with self.assertRaises(SlackObjectFormationError): + ButtonElement( + text="Hi there!", + action_id="button", + value="click_me", + accessibility_label=("1234567890" * 8), + ).to_dict() + + def test_action_id(self): + with self.assertRaises(SlackObjectFormationError): + ButtonElement(text="click me!", action_id=STRING_301_CHARS, value="clickable button").to_dict() + + +class LinkButtonElementTests(unittest.TestCase): + def test_json(self): + button = LinkButtonElement(action_id="test", text="button text", url="https://example.com") + self.assertDictEqual( + { + "text": {"emoji": True, "text": "button text", "type": "plain_text"}, + "url": "https://example.com", + "type": "button", + "action_id": button.action_id, + }, + button.to_dict(), + ) + + # https://github.com/slackapi/python-slack-sdk/issues/1178 + def test_text_patterns_issue_1178(self): + button = LinkButtonElement( + action_id="test", + text=PlainTextObject(text="button text"), + url="http://slack.com", + ) + self.assertDictEqual( + { + "text": {"text": "button text", "type": "plain_text"}, + "url": "http://slack.com", + "type": "button", + "action_id": button.action_id, + }, + button.to_dict(), + ) + + def test_url_length(self): + with self.assertRaises(SlackObjectFormationError): + LinkButtonElement(text="Button", url=STRING_3001_CHARS).to_dict() + + def test_action_id_length(self): + with self.assertRaises(SlackObjectFormationError): + LinkButtonElement( + text="test", + action_id="1234567890" * 26, + value="click_me", + url="https://slack.com/", + ).to_dict() + + +# ------------------------------------------------- +# Checkboxes +# ------------------------------------------------- + + +class CheckboxesElementTests(unittest.TestCase): + def test_document(self): + input = { + "type": "checkboxes", + "action_id": "this_is_an_action_id", + "initial_options": [{"value": "A1", "text": {"type": "plain_text", "text": "Checkbox 1"}}], + "options": [ + {"value": "A1", "text": {"type": "plain_text", "text": "Checkbox 1"}}, + {"value": "A2", "text": {"type": "plain_text", "text": "Checkbox 2"}}, + ], + } + self.assertDictEqual(input, CheckboxesElement(**input).to_dict()) + + def test_focus_on_load(self): + input = { + "type": "checkboxes", + "action_id": "this_is_an_action_id", + "initial_options": [{"value": "A1", "text": {"type": "plain_text", "text": "Checkbox 1"}}], + "options": [ + {"value": "A1", "text": {"type": "plain_text", "text": "Checkbox 1"}}, + ], + "focus_on_load": True, + } + self.assertDictEqual(input, CheckboxesElement(**input).to_dict()) + + +# ------------------------------------------------- +# DatePicker +# ------------------------------------------------- + + +class DatePickerElementTests(unittest.TestCase): + def test_document(self): + input = { + "type": "datepicker", + "action_id": "datepicker123", + "initial_date": "1990-04-28", + "placeholder": {"type": "plain_text", "text": "Select a date"}, + "confirm": { + "title": {"type": "plain_text", "text": "Are you sure?"}, + "text": { + "type": "mrkdwn", + "text": "Wouldn't you prefer a good game of _chess_?", + }, + "confirm": {"type": "plain_text", "text": "Do it"}, + "deny": {"type": "plain_text", "text": "Stop, I've changed my mind!"}, + }, + } + self.assertDictEqual(input, DatePickerElement(**input).to_dict()) + + def test_json(self): + for month in range(1, 12): + for day in range(1, 31): + date = f"2020-{month:02}-{day:02}" + self.assertDictEqual( + { + "action_id": "datepicker-action", + "initial_date": date, + "placeholder": { + "emoji": True, + "text": "Select a date", + "type": "plain_text", + }, + "type": "datepicker", + }, + DatePickerElement( + action_id="datepicker-action", + placeholder="Select a date", + initial_date=date, + ).to_dict(), + ) + + def test_issue_623(self): + elem = DatePickerElement(action_id="1", placeholder=None) + elem.to_dict() # no exception + elem = DatePickerElement(action_id="1") + elem.to_dict() # no exception + with self.assertRaises(SlackObjectFormationError): + elem = DatePickerElement(action_id="1", placeholder="12345" * 100) + elem.to_dict() + + def test_focus_on_load(self): + input = { + "type": "datepicker", + "action_id": "datepicker123", + "initial_date": "1990-04-28", + "placeholder": {"type": "plain_text", "text": "Select a date"}, + "focus_on_load": True, + } + self.assertDictEqual(input, DatePickerElement(**input).to_dict()) + + +# ------------------------------------------------- +# TimePicker +# ------------------------------------------------- + + +class TimePickerElementTests(unittest.TestCase): + def test_document(self): + input = { + "type": "timepicker", + "action_id": "timepicker123", + "initial_time": "11:40", + "placeholder": { + "type": "plain_text", + "text": "Select a time", + }, + } + self.assertDictEqual(input, TimePickerElement(**input).to_dict()) + + def test_json(self): + for hour in range(0, 23): + for minute in range(0, 59): + time = f"{hour:02}:{minute:02}" + self.assertDictEqual( + { + "action_id": "timepicker123", + "initial_time": time, + "placeholder": { + "emoji": True, + "type": "plain_text", + "text": "Select a time", + }, + "type": "timepicker", + }, + TimePickerElement( + action_id="timepicker123", + placeholder="Select a time", + initial_time=time, + ).to_dict(), + ) + + with self.assertRaises(SlackObjectFormationError): + TimePickerElement( + action_id="timepicker123", + placeholder="Select a time", + initial_time="25:00", + ).to_dict() + + def test_focus_on_load(self): + input = { + "type": "timepicker", + "action_id": "timepicker123", + "initial_time": "11:40", + "placeholder": { + "type": "plain_text", + "text": "Select a time", + }, + "focus_on_load": True, + } + self.assertDictEqual(input, TimePickerElement(**input).to_dict()) + + def test_timezone(self): + input = { + "type": "timepicker", + "action_id": "timepicker123", + "initial_time": "11:40", + "placeholder": { + "type": "plain_text", + "text": "Select a time", + }, + "timezone": "America/Los_Angeles", + } + self.assertDictEqual(input, TimePickerElement(**input).to_dict()) + + +# ------------------------------------------------- +# DateTimePicker +# ------------------------------------------------- + + +class DateTimePickerElementTests(unittest.TestCase): + def test_document(self): + input = {"type": "datetimepicker", "action_id": "datetimepicker123", "initial_date_time": 1628633820} + self.assertDictEqual(input, DateTimePickerElement(**input).to_dict()) + + def test_json(self): + for initial_date_time in [0, 9999999999]: + self.assertDictEqual( + { + "action_id": "datetimepicker123", + "initial_date_time": initial_date_time, + "type": "datetimepicker", + }, + DateTimePickerElement( + action_id="datetimepicker123", + initial_date_time=initial_date_time, + ).to_dict(), + ) + + with self.assertRaises(SlackObjectFormationError): + DateTimePickerElement( + action_id="datetimepicker123", + initial_date_time=10000000000, + ).to_dict() + + def test_focus_on_load(self): + input = { + "type": "datetimepicker", + "action_id": "datetimepicker123", + "initial_date_time": 1628633820, + "focus_on_load": True, + } + self.assertDictEqual(input, DateTimePickerElement(**input).to_dict()) + + +# ---------------------------------------------- +# FeedbackButtons +# ---------------------------------------------- + + +class FeedbackButtonsTests(unittest.TestCase): + def test_document(self): + input = { + "type": "feedback_buttons", + "action_id": "feedback-123", + "positive_button": { + "text": {"type": "plain_text", "text": "+1"}, + "accessibility_label": "Positive feedback", + "value": "positive", + }, + "negative_button": { + "text": {"type": "plain_text", "text": "-1"}, + "accessibility_label": "Negative feedback", + "value": "negative", + }, + } + self.assertDictEqual(input, FeedbackButtonsElement(**input).to_dict()) + + +# ---------------------------------------------- +# IconButton +# ---------------------------------------------- + + +class IconButtonTests(unittest.TestCase): + def test_document(self): + input = { + "type": "icon_button", + "action_id": "icon-123", + "icon": "trash", + "text": {"type": "plain_text", "text": "Delete"}, + "accessibility_label": "Delete item", + "value": "delete_item", + "visible_to_user_ids": ["U123456", "U789012"], + } + self.assertDictEqual(input, IconButtonElement(**input).to_dict()) + + def test_with_confirm(self): + input = { + "type": "icon_button", + "action_id": "icon-456", + "icon": "trash", + "text": {"type": "plain_text", "text": "Delete"}, + "value": "trash", + "confirm": { + "title": {"type": "plain_text", "text": "Are you sure?"}, + "text": {"type": "plain_text", "text": "This will send a warning."}, + "confirm": {"type": "plain_text", "text": "Yes"}, + "deny": {"type": "plain_text", "text": "No"}, + }, + } + icon_button = IconButtonElement(**input) + self.assertIsNotNone(icon_button.confirm) + self.assertDictEqual(input, icon_button.to_dict()) + + +# ------------------------------------------------- +# Image +# ------------------------------------------------- + + +class ImageElementTests(unittest.TestCase): + def test_document(self): + input = { + "type": "image", + "image_url": "http://placekitten.com/700/500", + "alt_text": "Multiple cute kittens", + } + self.assertDictEqual(input, ImageElement(**input).to_dict()) + + def test_json(self): + self.assertDictEqual( + { + "image_url": "https://example.com", + "alt_text": "not really an image", + "type": "image", + }, + ImageElement(image_url="https://example.com", alt_text="not really an image").to_dict(), + ) + + def test_image_url_length(self): + with self.assertRaises(SlackObjectFormationError): + ImageElement(image_url=STRING_3001_CHARS, alt_text="text").to_dict() + + def test_alt_text_length(self): + with self.assertRaises(SlackObjectFormationError): + ImageElement(image_url="https://example.com", alt_text=STRING_3001_CHARS).to_dict() + + def test_slack_file(self): + self.assertDictEqual( + { + "slack_file": {"id": "F11111"}, + "alt_text": "not really an image", + "type": "image", + }, + ImageElement(slack_file=SlackFile(id="F11111"), alt_text="not really an image").to_dict(), + ) + self.assertDictEqual( + { + "slack_file": {"url": "https://example.com"}, + "alt_text": "not really an image", + "type": "image", + }, + ImageElement(slack_file=SlackFile(url="https://example.com"), alt_text="not really an image").to_dict(), + ) + self.assertDictEqual( + { + "slack_file": {"id": "F11111"}, + "alt_text": "not really an image", + "type": "image", + }, + ImageElement(slack_file={"id": "F11111"}, alt_text="not really an image").to_dict(), + ) + self.assertDictEqual( + { + "slack_file": {"url": "https://example.com"}, + "alt_text": "not really an image", + "type": "image", + }, + ImageElement(slack_file={"url": "https://example.com"}, alt_text="not really an image").to_dict(), + ) + + +# ------------------------------------------------- +# Static Select +# ------------------------------------------------- + + +class StaticMultiSelectElementTests(unittest.TestCase): + maxDiff = None + + def test_document(self): + input = { + "action_id": "text1234", + "type": "multi_static_select", + "placeholder": {"type": "plain_text", "text": "Select items"}, + "options": [ + { + "text": {"type": "plain_text", "text": "*this is plain_text text*"}, + "value": "value-0", + }, + { + "text": {"type": "plain_text", "text": "*this is plain_text text*"}, + "value": "value-1", + }, + { + "text": {"type": "plain_text", "text": "*this is plain_text text*"}, + "value": "value-2", + }, + ], + "max_selected_items": 1, + } + self.assertDictEqual(input, StaticMultiSelectElement(**input).to_dict()) + + def test_focus_on_load(self): + input = { + "action_id": "text1234", + "type": "multi_static_select", + "placeholder": {"type": "plain_text", "text": "Select items"}, + "options": [ + { + "text": {"type": "plain_text", "text": "*this is plain_text text*"}, + "value": "value-0", + }, + { + "text": {"type": "plain_text", "text": "*this is plain_text text*"}, + "value": "value-1", + }, + { + "text": {"type": "plain_text", "text": "*this is plain_text text*"}, + "value": "value-2", + }, + ], + "max_selected_items": 1, + "focus_on_load": True, + } + self.assertDictEqual(input, StaticMultiSelectElement(**input).to_dict()) + + +class StaticSelectElementTests(unittest.TestCase): + maxDiff = None + + def test_document_options(self): + input = { + "action_id": "text1234", + "type": "static_select", + "placeholder": {"type": "plain_text", "text": "Select an item"}, + "options": [ + { + "text": {"type": "plain_text", "text": "*this is plain_text text*"}, + "value": "value-0", + }, + { + "text": {"type": "plain_text", "text": "*this is plain_text text*"}, + "value": "value-1", + }, + { + "text": {"type": "plain_text", "text": "*this is plain_text text*"}, + "value": "value-2", + }, + ], + } + self.assertDictEqual(input, StaticSelectElement(**input).to_dict()) + + def test_document_option_groups(self): + input = { + "action_id": "text1234", + "type": "static_select", + "placeholder": {"type": "plain_text", "text": "Select an item"}, + "option_groups": [ + { + "label": {"type": "plain_text", "text": "Group 1"}, + "options": [ + { + "text": { + "type": "plain_text", + "text": "*this is plain_text text*", + }, + "value": "value-0", + }, + { + "text": { + "type": "plain_text", + "text": "*this is plain_text text*", + }, + "value": "value-1", + }, + { + "text": { + "type": "plain_text", + "text": "*this is plain_text text*", + }, + "value": "value-2", + }, + ], + }, + { + "label": {"type": "plain_text", "text": "Group 2"}, + "options": [ + { + "text": { + "type": "plain_text", + "text": "*this is plain_text text*", + }, + "value": "value-3", + } + ], + }, + ], + } + self.assertDictEqual(input, StaticSelectElement(**input).to_dict()) + + option_one = Option.from_single_value("one") + option_two = Option.from_single_value("two") + options = [option_one, option_two, Option.from_single_value("three")] + + def test_json(self): + dict_options = [] + for o in self.options: + dict_options.append(o.to_dict()) + + self.assertDictEqual( + { + "placeholder": { + "emoji": True, + "text": "selectedValue", + "type": "plain_text", + }, + "action_id": "dropdown", + "options": dict_options, + "initial_option": self.option_two.to_dict(), + "type": "static_select", + }, + StaticSelectElement( + placeholder="selectedValue", + action_id="dropdown", + options=self.options, + initial_option=self.option_two, + ).to_dict(), + ) + + self.assertDictEqual( + { + "placeholder": { + "emoji": True, + "text": "selectedValue", + "type": "plain_text", + }, + "action_id": "dropdown", + "options": dict_options, + "confirm": ConfirmObject(title="title", text="text").to_dict("block"), + "type": "static_select", + }, + StaticSelectElement( + placeholder="selectedValue", + action_id="dropdown", + options=self.options, + confirm=ConfirmObject(title="title", text="text"), + ).to_dict(), + ) + + def test_options_length(self): + with self.assertRaises(SlackObjectFormationError): + StaticSelectElement( + placeholder="select", + action_id="selector", + options=[self.option_one] * 101, + ).to_dict() + + def test_focus_on_load(self): + input = { + "action_id": "text1234", + "type": "static_select", + "placeholder": {"type": "plain_text", "text": "Select an item"}, + "options": [ + { + "text": {"type": "plain_text", "text": "*this is plain_text text*"}, + "value": "value-0", + }, + ], + "focus_on_load": True, + } + self.assertDictEqual(input, StaticSelectElement(**input).to_dict()) + + def test_lists_and_tuples_serialize_to_dict_equally(self): + expected = { + "options": [ + { + "text": {"emoji": True, "text": "X", "type": "plain_text"}, + "value": "x", + } + ], + "type": "static_select", + } + option = Option(value="x", text="X") + # List + self.assertDictEqual( + expected, + StaticSelectElement(options=[option]).to_dict(), + ) + # Tuple (this pattern used to be failing) + self.assertDictEqual( + expected, + StaticSelectElement(options=(option,)).to_dict(), + ) + + +# ------------------------------------------------- +# External Data Source Select +# ------------------------------------------------- + + +class ExternalDataMultiSelectElementTests(unittest.TestCase): + maxDiff = None + + def test_document(self): + input = { + "action_id": "text1234", + "type": "multi_external_select", + "placeholder": {"type": "plain_text", "text": "Select items"}, + "min_query_length": 3, + } + self.assertDictEqual(input, ExternalDataMultiSelectElement(**input).to_dict()) + + def test_document_initial_options(self): + input = { + "action_id": "text1234", + "type": "multi_external_select", + "placeholder": {"type": "plain_text", "text": "Select items"}, + "initial_options": [ + { + "text": {"type": "plain_text", "text": "The default channel"}, + "value": "C1234567890", + } + ], + "min_query_length": 0, + "max_selected_items": 1, + } + self.assertDictEqual(input, ExternalDataMultiSelectElement(**input).to_dict()) + + def test_focus_on_load(self): + input = { + "action_id": "text1234", + "type": "multi_external_select", + "placeholder": {"type": "plain_text", "text": "Select items"}, + "focus_on_load": True, + } + self.assertDictEqual(input, ExternalDataMultiSelectElement(**input).to_dict()) + + +class ExternalDataSelectElementTests(unittest.TestCase): + maxDiff = None + + def test_document_1(self): + input = { + "action_id": "text1234", + "type": "external_select", + "placeholder": {"type": "plain_text", "text": "Select an item"}, + "min_query_length": 3, + } + self.assertDictEqual(input, ExternalDataSelectElement(**input).to_dict()) + + def test_document_2(self): + input = { + "action_id": "text1234", + "type": "external_select", + "placeholder": {"type": "plain_text", "text": "Select an item"}, + "initial_option": { + "text": {"type": "plain_text", "text": "The default channel"}, + "value": "C1234567890", + }, + "confirm": { + "title": {"type": "plain_text", "text": "Are you sure?"}, + "text": { + "type": "mrkdwn", + "text": "Wouldn't you prefer a good game of _chess_?", + }, + "confirm": {"type": "plain_text", "text": "Do it"}, + "deny": {"type": "plain_text", "text": "Stop, I've changed my mind!"}, + }, + "min_query_length": 3, + } + self.assertDictEqual(input, ExternalDataSelectElement(**input).to_dict()) + + def test_json(self): + self.assertDictEqual( + { + "placeholder": { + "emoji": True, + "text": "selectedValue", + "type": "plain_text", + }, + "action_id": "dropdown", + "min_query_length": 5, + "type": "external_select", + }, + ExternalDataSelectElement(placeholder="selectedValue", action_id="dropdown", min_query_length=5).to_dict(), + ) + self.assertDictEqual( + { + "placeholder": { + "emoji": True, + "text": "selectedValue", + "type": "plain_text", + }, + "action_id": "dropdown", + "confirm": ConfirmObject(title="title", text="text").to_dict("block"), + "type": "external_select", + }, + ExternalDataSelectElement( + placeholder="selectedValue", + action_id="dropdown", + confirm=ConfirmObject(title="title", text="text"), + ).to_dict(), + ) + + def test_focus_on_load(self): + input = { + "action_id": "text1234", + "type": "external_select", + "placeholder": {"type": "plain_text", "text": "Select an item"}, + "focus_on_load": True, + } + self.assertDictEqual(input, ExternalDataSelectElement(**input).to_dict()) + + +# ------------------------------------------------- +# Users Select +# ------------------------------------------------- + + +class UserSelectMultiElementTests(unittest.TestCase): + def test_document(self): + input = { + "action_id": "text1234", + "type": "multi_users_select", + "placeholder": {"type": "plain_text", "text": "Select users"}, + "initial_users": ["U123", "U234"], + "max_selected_items": 1, + } + self.assertDictEqual(input, UserMultiSelectElement(**input).to_dict()) + + +class UserSelectElementTests(unittest.TestCase): + def test_document(self): + input = { + "action_id": "text1234", + "type": "users_select", + "placeholder": {"type": "plain_text", "text": "Select an item"}, + "initial_user": "U123", + } + self.assertDictEqual(input, UserSelectElement(**input).to_dict()) + + def test_json(self): + self.assertDictEqual( + { + "action_id": "a-123", + "type": "users_select", + "initial_user": "U123", + "placeholder": { + "type": "plain_text", + "text": "abc", + "emoji": True, + }, + }, + UserSelectElement( + placeholder="abc", + action_id="a-123", + initial_user="U123", + ).to_dict(), + ) + + def test_focus_on_load(self): + input = { + "action_id": "text1234", + "type": "users_select", + "placeholder": {"type": "plain_text", "text": "Select an item"}, + "initial_user": "U123", + "focus_on_load": True, + } + self.assertDictEqual(input, UserSelectElement(**input).to_dict()) + + +# ------------------------------------------------- +# Conversations Select +# ------------------------------------------------- + + +class ConversationSelectMultiElementTests(unittest.TestCase): + def test_document(self): + input = { + "action_id": "text1234", + "type": "multi_conversations_select", + "placeholder": {"type": "plain_text", "text": "Select conversations"}, + "initial_conversations": ["C123", "C234"], + "max_selected_items": 2, + "default_to_current_conversation": True, + "filter": {"include": ["public", "mpim"], "exclude_bot_users": True}, + } + self.assertDictEqual(input, ConversationMultiSelectElement(**input).to_dict()) + + def test_focus_on_load(self): + input = { + "action_id": "text1234", + "type": "multi_conversations_select", + "placeholder": {"type": "plain_text", "text": "Select conversations"}, + "initial_conversations": ["C123", "C234"], + "focus_on_load": True, + } + self.assertDictEqual(input, ConversationMultiSelectElement(**input).to_dict()) + + +class ConversationSelectElementTests(unittest.TestCase): + def test_document(self): + input = { + "action_id": "text1234", + "type": "conversations_select", + "placeholder": {"type": "plain_text", "text": "Select an item"}, + "initial_conversation": "C123", + "response_url_enabled": True, + "default_to_current_conversation": True, + "filter": {"include": ["public", "mpim"], "exclude_bot_users": True}, + } + self.assertDictEqual(input, ConversationSelectElement(**input).to_dict()) + + def test_focus_on_load(self): + input = { + "action_id": "text1234", + "type": "conversations_select", + "placeholder": {"type": "plain_text", "text": "Select an item"}, + "initial_conversation": "C123", + "focus_on_load": True, + } + self.assertDictEqual(input, ConversationSelectElement(**input).to_dict()) + + +# ------------------------------------------------- +# Channels Select +# ------------------------------------------------- + + +class ChannelSelectMultiElementTests(unittest.TestCase): + def test_document(self): + input = { + "action_id": "text1234", + "type": "multi_channels_select", + "placeholder": {"type": "plain_text", "text": "Select channels"}, + "initial_channels": ["C123", "C234"], + "max_selected_items": 2, + } + self.assertDictEqual(input, ChannelMultiSelectElement(**input).to_dict()) + + def test_focus_on_load(self): + input = { + "action_id": "text1234", + "type": "multi_channels_select", + "placeholder": {"type": "plain_text", "text": "Select channels"}, + "initial_channels": ["C123", "C234"], + "focus_on_load": True, + } + self.assertDictEqual(input, ChannelMultiSelectElement(**input).to_dict()) + + +class ChannelSelectElementTests(unittest.TestCase): + def test_document(self): + input = { + "action_id": "text1234", + "type": "channels_select", + "placeholder": {"type": "plain_text", "text": "Select an item"}, + "response_url_enabled": True, + "initial_channel": "C123", + } + self.assertDictEqual(input, ChannelSelectElement(**input).to_dict()) + + def test_focus_on_load(self): + input = { + "action_id": "text1234", + "type": "channels_select", + "placeholder": {"type": "plain_text", "text": "Select an item"}, + "focus_on_load": True, + } + self.assertDictEqual(input, ChannelSelectElement(**input).to_dict()) + + +# ------------------------------------------------- +# Overflow Menu Select +# ------------------------------------------------- + + +class OverflowMenuElementTests(unittest.TestCase): + def test_document(self): + input = { + "type": "overflow", + "options": [ + { + "text": {"type": "plain_text", "text": "*this is plain_text text*"}, + "value": "value-0", + }, + { + "text": {"type": "plain_text", "text": "*this is plain_text text*"}, + "value": "value-1", + }, + { + "text": {"type": "plain_text", "text": "*this is plain_text text*"}, + "value": "value-2", + }, + { + "text": {"type": "plain_text", "text": "*this is plain_text text*"}, + "value": "value-3", + }, + { + "text": {"type": "plain_text", "text": "*this is plain_text text*"}, + # https://docs.slack.dev/reference/block-kit/composition-objects/option-object + "url": "https://www.example.com", + }, + ], + "action_id": "overflow", + } + self.assertDictEqual(input, OverflowMenuElement(**input).to_dict()) + + +# ------------------------------------------------- +# Input +# ------------------------------------------------- + + +class RichTextInputElementTests(unittest.TestCase): + def test_simple(self): + input = { + "type": "rich_text_input", + "action_id": "rich_input", + "placeholder": {"type": "plain_text", "text": "Enter some plain text"}, + } + self.assertDictEqual(input, RichTextInputElement(**input).to_dict()) + + def test_document(self): + input = { + "type": "rich_text_input", + "action_id": "rich_text_input-action", + "dispatch_action_config": {"trigger_actions_on": ["on_character_entered"]}, + "focus_on_load": True, + "placeholder": {"type": "plain_text", "text": "Enter text"}, + } + self.assertDictEqual(input, RichTextInputElement(**input).to_dict()) + + def test_issue_1571(self): + self.assertDictEqual( + RichTextInputElement( + action_id="contents", + initial_value=RichTextBlock( + elements=[ + RichTextSectionElement( + elements=[ + RichTextElementParts.Text(text="Hey, "), + RichTextElementParts.Text(text="this", style={"italic": True}), + RichTextElementParts.Text(text="is what you should be looking at. "), + RichTextElementParts.Text(text="Please", style={"bold": True}), + ] + ) + ], + ), + ).to_dict(), + { + "action_id": "contents", + "initial_value": { + "elements": [ + { + "elements": [ + {"text": "Hey, ", "type": "text"}, + {"style": {"italic": True}, "text": "this", "type": "text"}, + {"text": "is what you should be looking at. ", "type": "text"}, + {"style": {"bold": True}, "text": "Please", "type": "text"}, + ], + "type": "rich_text_section", + } + ], + "type": "rich_text", + }, + "type": "rich_text_input", + }, + ) + + +class PlainTextInputElementTests(unittest.TestCase): + def test_document_1(self): + input = { + "type": "plain_text_input", + "action_id": "plain_input", + "placeholder": {"type": "plain_text", "text": "Enter some plain text"}, + } + self.assertDictEqual(input, PlainTextInputElement(**input).to_dict()) + + def test_document_2(self): + input = { + "type": "plain_text_input", + "action_id": "plain_input", + "placeholder": {"type": "plain_text", "text": "Enter some plain text"}, + "initial_value": "TODO", + "multiline": True, + "min_length": 1, + "max_length": 10, + } + self.assertDictEqual(input, PlainTextInputElement(**input).to_dict()) + + def test_document_3(self): + input = { + "type": "plain_text_input", + "multiline": True, + "dispatch_action_config": {"trigger_actions_on": ["on_character_entered"]}, + } + self.assertDictEqual(input, PlainTextInputElement(**input).to_dict()) + + def test_focus_on_load(self): + input = { + "type": "plain_text_input", + "action_id": "plain_input", + "placeholder": {"type": "plain_text", "text": "Enter some plain text"}, + "focus_on_load": True, + } + self.assertDictEqual(input, PlainTextInputElement(**input).to_dict()) + + +# ------------------------------------------------- +# Email Input Element +# ------------------------------------------------- + + +class EmailInputElementTests(unittest.TestCase): + def test_element(self): + input = { + "type": "email_text_input", + "action_id": "email_text_input-action", + "placeholder": {"type": "plain_text", "text": "Enter some email"}, + } + self.assertDictEqual(input, EmailInputElement(**input).to_dict()) + + def test_initial_value(self): + input = { + "type": "email_text_input", + "action_id": "email_text_input-action", + "initial_value": "bill@slack.com", + "placeholder": {"type": "plain_text", "text": "Enter some email"}, + } + self.assertDictEqual(input, EmailInputElement(**input).to_dict()) + + def test_no_action_id(self): + input = { + "type": "email_text_input", + "dispatch_action_config": {"trigger_actions_on": ["on_character_entered"]}, + } + self.assertDictEqual(input, EmailInputElement(**input).to_dict()) + + def test_focus_on_load(self): + input = { + "type": "email_text_input", + "action_id": "email_text_input-action", + "placeholder": {"type": "plain_text", "text": "Enter some email"}, + "focus_on_load": True, + } + self.assertDictEqual(input, EmailInputElement(**input).to_dict()) + + +# ------------------------------------------------- +# Url Input Element +# ------------------------------------------------- + + +class UrlInputElementTests(unittest.TestCase): + def test_element(self): + input = { + "type": "url_text_input", + "action_id": "url_text_input-action", + "placeholder": {"type": "plain_text", "text": "Enter some url"}, + } + self.assertDictEqual(input, UrlInputElement(**input).to_dict()) + + def test_initial_value(self): + input = { + "type": "url_text_input", + "action_id": "url_text_input-action", + "initial_value": "https://bill.test.com", + "placeholder": {"type": "plain_text", "text": "Enter some url"}, + } + self.assertDictEqual(input, UrlInputElement(**input).to_dict()) + + def test_no_action_id(self): + input = { + "type": "url_text_input", + "dispatch_action_config": {"trigger_actions_on": ["on_character_entered"]}, + } + self.assertDictEqual(input, UrlInputElement(**input).to_dict()) + + def test_focus_on_load(self): + input = { + "type": "url_text_input", + "action_id": "url_text_input-action", + "placeholder": {"type": "plain_text", "text": "Enter some url"}, + "focus_on_load": True, + } + self.assertDictEqual(input, UrlInputElement(**input).to_dict()) + + +# ------------------------------------------------- +# Number Input Element +# ------------------------------------------------- + + +class NumberInputElementTests(unittest.TestCase): + def test_element(self): + input = { + "type": "number_input", + "action_id": "number_input-action", + "is_decimal_allowed": False, + "placeholder": {"type": "plain_text", "text": "Enter some plain text"}, + } + self.assertDictEqual(input, NumberInputElement(**input).to_dict()) + + def test_element_full(self): + input = { + "type": "number_input", + "action_id": "number_input-action", + "is_decimal_allowed": False, + "placeholder": {"type": "plain_text", "text": "Enter some plain text"}, + "initial_value": "7", + "min_value": "1", + "max_value": "10", + } + self.assertDictEqual(input, NumberInputElement(**input).to_dict()) + + def test_dispatch_action_config(self): + input = { + "type": "number_input", + "is_decimal_allowed": True, + "dispatch_action_config": {"trigger_actions_on": ["on_character_entered"]}, + } + self.assertDictEqual(input, NumberInputElement(**input).to_dict()) + + def test_focus_on_load(self): + input = { + "type": "number_input", + "action_id": "number_input-action", + "is_decimal_allowed": False, + "placeholder": {"type": "plain_text", "text": "Enter some plain text"}, + "focus_on_load": True, + } + self.assertDictEqual(input, NumberInputElement(**input).to_dict()) + + +# ------------------------------------------------- +# File Input Element +# ------------------------------------------------- + + +class FileInputElementTests(unittest.TestCase): + def test_element(self): + input = { + "type": "file_input", + "action_id": "file_input-action", + "filetypes": ["pdf", "txt"], + "max_files": 3, + } + self.assertDictEqual(input, FileInputElement(**input).to_dict()) + + +# ------------------------------------------------- +# Radio Buttons +# ------------------------------------------------- + + +class RadioButtonsElementTest(unittest.TestCase): + def test_document(self): + input = { + "type": "radio_buttons", + "action_id": "this_is_an_action_id", + "initial_option": { + "value": "A1", + "text": {"type": "plain_text", "text": "Radio 1"}, + }, + "options": [ + {"value": "A1", "text": {"type": "plain_text", "text": "Radio 1"}}, + {"value": "A2", "text": {"type": "plain_text", "text": "Radio 2"}}, + ], + } + self.assertDictEqual(input, RadioButtonsElement(**input).to_dict()) + + def test_focus_on_load(self): + input = { + "type": "radio_buttons", + "action_id": "this_is_an_action_id", + "initial_option": { + "value": "A1", + "text": {"type": "plain_text", "text": "Radio 1"}, + }, + "options": [ + {"value": "A1", "text": {"type": "plain_text", "text": "Radio 1"}}, + {"value": "A2", "text": {"type": "plain_text", "text": "Radio 2"}}, + ], + "focus_on_load": True, + } + self.assertDictEqual(input, RadioButtonsElement(**input).to_dict()) + + +# ------------------------------------------------- +# Workflow Button +# ------------------------------------------------- + + +class WorkflowButtonElementTests(unittest.TestCase): + def test_load(self): + input = { + "type": "workflow_button", + "text": {"type": "plain_text", "text": "Run Workflow"}, + "workflow": { + "trigger": { + "url": "https://slack.com/shortcuts/Ft0123ABC456/xyz...zyx", + "customizable_input_parameters": [ + {"name": "input_parameter_a", "value": "Value for input param A"}, + {"name": "input_parameter_b", "value": "Value for input param B"}, + ], + } + }, + } + self.assertDictEqual(input, WorkflowButtonElement(**input).to_dict()) diff --git a/tests/slack_sdk/models/test_init.py b/tests/slack_sdk/models/test_init.py new file mode 100644 index 000000000..3a76e30b9 --- /dev/null +++ b/tests/slack_sdk/models/test_init.py @@ -0,0 +1,26 @@ +import unittest + +from slack_sdk.models import extract_json +from slack_sdk.models.blocks import PlainTextObject, MarkdownTextObject + + +class TestInit(unittest.TestCase): + def test_from_list_of_json_objects(self): + json_objects = [ + PlainTextObject.from_str("foo"), + MarkdownTextObject.from_str("bar"), + ] + output = extract_json(json_objects) + expected = { + "result": [ + {"type": "plain_text", "text": "foo", "emoji": True}, + {"type": "mrkdwn", "text": "bar"}, + ] + } + self.assertDictEqual(expected, {"result": output}) + + def test_from_single_json_object(self): + single_json_object = PlainTextObject.from_str("foo") + output = extract_json(single_json_object) + expected = {"result": {"type": "plain_text", "text": "foo", "emoji": True}} + self.assertDictEqual(expected, {"result": output}) diff --git a/tests/slack_sdk/models/test_metadata.py b/tests/slack_sdk/models/test_metadata.py new file mode 100644 index 000000000..14635c661 --- /dev/null +++ b/tests/slack_sdk/models/test_metadata.py @@ -0,0 +1,307 @@ +import unittest + +from slack_sdk.models.metadata import ( + EventAndEntityMetadata, + EntityMetadata, + ExternalRef, + FileEntitySlackFile, + EntityIconField, + EntityEditTextConfig, + EntityEditSupport, + EntityFullSizePreviewError, + EntityFullSizePreview, + EntityUserIDField, + EntityUserField, + EntityTypedField, + EntityStringField, + EntityTimestampField, + EntityImageField, + EntityCustomField, + FileEntityFields, + TaskEntityFields, + EntityActionButton, + EntityTitle, + EntityAttributes, + EntityActions, + EntityPayload, +) + + +class EntityMetadataTests(unittest.TestCase): + maxDiff = None + + # ============================================================================ + # Entity JSON + # ============================================================================ + + task_entity_json = { + "app_unfurl_url": "https://myappdomain.com/123?myquery=param", + "entity_type": "slack#/entities/task", + "url": "https://myappdomain.com/123", + "external_ref": {"id": "123"}, + "entity_payload": { + "attributes": { + "title": {"text": "My Title"}, + "display_type": "Incident", + "display_id": "123", + "product_name": "My Product", + }, + "fields": { + "date_created": {"value": 1741164235}, + "status": {"value": "In Progress"}, + "description": { + "value": "My Description", + "long": True, + "edit": {"enabled": True, "text": {"min_length": 5, "max_length": 100}}, + }, + "due_date": {"value": "2026-06-06", "type": "slack#/types/date"}, + "created_by": {"type": "slack#/types/user", "user": {"user_id": "USLACKBOT"}}, + }, + "custom_fields": [ + { + "label": "My Users", + "key": "my-users", + "type": "array", + "item_type": "slack#/types/user", + "value": [ + {"type": "slack#/types/user", "user": {"user_id": "USLACKBOT"}}, + { + "type": "slack#/types/user", + "user": { + "text": "John Smith", + "email": "j@example.com", + "icon": {"alt_text": "Avatar", "url": "https://my-hosted-icon.com"}, + }, + }, + ], + } + ], + }, + } + + file_entity_json = { + "app_unfurl_url": "https://myappdomain.com/file/456?view=preview", + "entity_type": "slack#/entities/file", + "url": "https://myappdomain.com/file/456", + "external_ref": {"id": "456", "type": "DOC"}, + "entity_payload": { + "attributes": { + "title": {"text": "Q4 Product Roadmap"}, + "display_type": "PDF Document", + "display_id": "DOC-456", + "product_icon": {"alt_text": "Product Logo", "url": "https://myappdomain.com/icons/logo.png"}, + "product_name": "FileVault Pro", + "locale": "en-US", + "full_size_preview": { + "is_supported": True, + "preview_url": "https://myappdomain.com/previews/456/full.png", + "mime_type": "image/png", + }, + }, + "fields": { + "preview": { + "alt_text": "Document preview thumbnail", + "label": "Preview", + "image_url": "https://myappdomain.com/previews/456/thumb.png", + "type": "slack#/types/image", + }, + "date_created": {"value": 1709554321, "type": "slack#/types/timestamp"}, + "mime_type": {"value": "application/pdf"}, + }, + "slack_file": {"id": "F123ABC456", "type": "pdf"}, + "display_order": ["date_created", "mime_type", "preview"], + "actions": { + "primary_actions": [ + { + "text": "Open", + "action_id": "open_file", + "value": "456", + "style": "primary", + "url": "https://myappdomain.com/file/456/view", + } + ], + "overflow_actions": [{"text": "Delete", "action_id": "delete_file", "value": "456", "style": "danger"}], + }, + }, + } + + # ============================================================================ + # Methods returning re-usable metadata components + # ============================================================================ + + def attributes(self): + return EntityAttributes( + title=EntityTitle(text="My Title"), + product_name="My Product", + display_type="Incident", + display_id="123", + ) + + def sample_file_attributes(self): + return EntityAttributes( + title=EntityTitle(text="Q4 Product Roadmap"), + display_type="PDF Document", + display_id="DOC-456", + product_icon=EntityIconField(alt_text="Product Logo", url="https://myappdomain.com/icons/logo.png"), + product_name="FileVault Pro", + locale="en-US", + full_size_preview=EntityFullSizePreview( + is_supported=True, preview_url="https://myappdomain.com/previews/456/full.png", mime_type="image/png" + ), + ) + + def user_array_custom_field(self): + return EntityCustomField( + label="My Users", + key="my-users", + type="array", + item_type="slack#/types/user", + value=[ + EntityTypedField(type="slack#/types/user", user=EntityUserIDField(user_id="USLACKBOT")), + EntityTypedField( + type="slack#/types/user", + user=EntityUserField( + text="John Smith", + email="j@example.com", + icon=EntityIconField(alt_text="Avatar", url="https://my-hosted-icon.com"), + ), + ), + ], + ) + + def task_fields(self): + return TaskEntityFields( + date_created=EntityTimestampField(value=1741164235), + status=EntityStringField(value="In Progress"), + description=EntityStringField( + value="My Description", + long=True, + edit=EntityEditSupport(enabled=True, text=EntityEditTextConfig(min_length=5, max_length=100)), + ), + due_date=EntityTypedField(value="2026-06-06", type="slack#/types/date"), + created_by=EntityTypedField( + type="slack#/types/user", + user=EntityUserIDField(user_id="USLACKBOT"), + ), + ) + + def file_fields(self): + return FileEntityFields( + preview=EntityImageField( + type="slack#/types/image", + alt_text="Document preview thumbnail", + label="Preview", + image_url="https://myappdomain.com/previews/456/thumb.png", + ), + date_created=EntityTimestampField(value=1709554321, type="slack#/types/timestamp"), + mime_type=EntityStringField(value="application/pdf"), + ) + + def supported_full_size_preview(self): + return EntityFullSizePreview( + is_supported=True, preview_url="https://example.com/preview.jpg", mime_type="image/jpeg" + ) + + def sample_file_actions(self): + return EntityActions( + primary_actions=[ + EntityActionButton( + text="Open", + action_id="open_file", + value="456", + style="primary", + url="https://myappdomain.com/file/456/view", + ) + ], + overflow_actions=[EntityActionButton(text="Delete", action_id="delete_file", value="456", style="danger")], + ) + + # ============================================================================ + # Tests + # ============================================================================ + + def test_entity_full_size_preview_error(self): + error = EntityFullSizePreviewError(code="not_found", message="File not found") + self.assertDictEqual(error.to_dict(), {"code": "not_found", "message": "File not found"}) + + def test_entity_full_size_preview_with_error(self): + preview = EntityFullSizePreview( + is_supported=False, error=EntityFullSizePreviewError(code="invalid_format", message="File not found") + ) + result = preview.to_dict() + self.assertFalse(result["is_supported"]) + self.assertIn("error", result) + + def test_attributes(self): + self.assertDictEqual( + self.attributes().to_dict(), + self.task_entity_json["entity_payload"]["attributes"], + ) + + def test_sample_file_attributes(self): + self.assertDictEqual( + self.sample_file_attributes().to_dict(), + self.file_entity_json["entity_payload"]["attributes"], + ) + + def test_array_custom_field(self): + self.assertDictEqual( + self.user_array_custom_field().to_dict(), + self.task_entity_json["entity_payload"]["custom_fields"][0], + ) + + def test_task_fields(self): + self.assertDictEqual( + self.task_fields().to_dict(), + self.task_entity_json["entity_payload"]["fields"], + ) + + def test_file_fields(self): + self.assertDictEqual( + self.file_fields().to_dict(), + self.file_entity_json["entity_payload"]["fields"], + ) + + def test_sample_file_actions(self): + self.assertDictEqual( + self.sample_file_actions().to_dict(), + self.file_entity_json["entity_payload"]["actions"], + ) + + def test_complete_task_entity_metadata(self): + entity_metadata = EventAndEntityMetadata( + entities=[ + EntityMetadata( + entity_type="slack#/entities/task", + external_ref=ExternalRef(id="123"), + url="https://myappdomain.com/123", + app_unfurl_url="https://myappdomain.com/123?myquery=param", + entity_payload=EntityPayload( + attributes=self.attributes(), + fields=self.task_fields(), + custom_fields=[self.user_array_custom_field()], + ), + ) + ] + ) + self.assertDictEqual(entity_metadata.to_dict(), {"entities": [self.task_entity_json]}) + + def test_complete_file_entity_metadata(self): + entity_metadata = EventAndEntityMetadata( + entities=[ + EntityMetadata( + entity_type="slack#/entities/file", + external_ref=ExternalRef(id="456", type="DOC"), + url="https://myappdomain.com/file/456", + app_unfurl_url="https://myappdomain.com/file/456?view=preview", + entity_payload=EntityPayload( + attributes=self.sample_file_attributes(), + fields=self.file_fields(), + slack_file=FileEntitySlackFile(id="F123ABC456", type="pdf"), + display_order=["date_created", "mime_type", "preview"], + actions=self.sample_file_actions(), + ), + ) + ] + ) + self.assertDictEqual(entity_metadata.to_dict(), {"entities": [self.file_entity_json]}) diff --git a/tests/slack_sdk/models/test_objects.py b/tests/slack_sdk/models/test_objects.py new file mode 100644 index 000000000..30bcb7002 --- /dev/null +++ b/tests/slack_sdk/models/test_objects.py @@ -0,0 +1,634 @@ +import copy +import unittest +from typing import List, Optional, Union + +from slack_sdk.errors import SlackObjectFormationError +from slack_sdk.models import JsonObject, JsonValidator +from slack_sdk.models.blocks import ConfirmObject, MarkdownTextObject, Option, OptionGroup, PlainTextObject +from slack_sdk.models.blocks.basic_components import FeedbackButtonObject, Workflow, WorkflowTrigger +from slack_sdk.models.messages import ChannelLink, DateLink, EveryoneLink, HereLink, Link, ObjectLink + +from . import STRING_51_CHARS, STRING_301_CHARS + + +class SimpleJsonObject(JsonObject): + attributes = {"some", "test", "keys"} + + def __init__(self): + self.some = "this is" + self.test = "a test" + self.keys = "object" + + @JsonValidator("some validation message") + def test_valid(self): + return len(self.test) <= 10 + + @JsonValidator("this should never fail") + def always_valid_test(self): + return True + + +class KeyValueObject(JsonObject): + attributes = {"name", "value"} + + def __init__( + self, + *, + name: Optional[str] = None, + value: Optional[str] = None, + ): + self.name = name + self.value = value + + +class NestedObject(JsonObject): + attributes = {"initial", "options"} + + def __init__( + self, + *, + initial: Union[dict, KeyValueObject], + options: List[Union[dict, KeyValueObject]], + ): + self.initial = KeyValueObject(**initial) if isinstance(initial, dict) else initial + self.options = [KeyValueObject(**o) if isinstance(o, dict) else o for o in options] + + +class JsonObjectTests(unittest.TestCase): + def setUp(self) -> None: + self.good_test_object = SimpleJsonObject() + obj = SimpleJsonObject() + obj.test = STRING_51_CHARS + self.bad_test_object = obj + + def test_json_formation(self): + self.assertDictEqual( + self.good_test_object.to_dict(), + {"some": "this is", "test": "a test", "keys": "object"}, + ) + + def test_validate_json_fails(self): + with self.assertRaises(SlackObjectFormationError): + self.bad_test_object.validate_json() + + def test_to_dict_performs_validation(self): + with self.assertRaises(SlackObjectFormationError): + self.bad_test_object.to_dict() + + def test_get_non_null_attributes(self): + expected = {"name": "something"} + obj = KeyValueObject(name="something", value=None) + obj2 = copy.deepcopy(obj) + self.assertDictEqual(expected, obj.get_non_null_attributes()) + self.assertEqual(str(obj2), str(obj)) + + def test_get_non_null_attributes_nested(self): + expected = { + "initial": {"name": "something"}, + "options": [ + {"name": "something"}, + {"name": "message", "value": "That's great!"}, + ], + } + obj1 = KeyValueObject(name="something", value=None) + obj2 = KeyValueObject(name="message", value="That's great!") + options = [obj1, obj2] + nested = NestedObject(initial=obj1, options=options) + + self.assertEqual(type(obj1), KeyValueObject) + self.assertTrue(hasattr(obj1, "value")) + self.assertEqual(type(nested.initial), KeyValueObject) + + self.assertEqual(type(options[0]), KeyValueObject) + self.assertTrue(hasattr(options[0], "value")) + self.assertEqual(type(nested.options[0]), KeyValueObject) + self.assertTrue(hasattr(nested.options[0], "value")) + + dict_value = nested.get_non_null_attributes() + self.assertDictEqual(expected, dict_value) + + self.assertEqual(type(obj1), KeyValueObject) + self.assertTrue(hasattr(obj1, "value")) + self.assertEqual(type(nested.initial), KeyValueObject) + + self.assertEqual(type(options[0]), KeyValueObject) + self.assertTrue(hasattr(options[0], "value")) + self.assertEqual(type(nested.options[0]), KeyValueObject) + self.assertTrue(hasattr(nested.options[0], "value")) + + def test_get_non_null_attributes_nested_2(self): + expected = { + "initial": {"name": "something"}, + "options": [ + {"name": "something"}, + {"name": "message", "value": "That's great!"}, + ], + } + nested = NestedObject( + initial={"name": "something"}, + options=[ + {"name": "something"}, + {"name": "message", "value": "That's great!"}, + ], + ) + self.assertDictEqual(expected, nested.get_non_null_attributes()) + + def test_eq(self): + obj1 = SimpleJsonObject() + self.assertEqual(self.good_test_object, obj1) + + obj2 = SimpleJsonObject() + obj2.test = "another" + self.assertNotEqual(self.good_test_object, obj2) + + self.assertNotEqual(self.good_test_object, None) + + +class JsonValidatorTests(unittest.TestCase): + def setUp(self) -> None: + self.validator_instance = JsonValidator("message") + self.class_instance = SimpleJsonObject() + + def test_isolated_class(self): + def does_nothing(): + return False + + wrapped = self.validator_instance(does_nothing) + + # noinspection PyUnresolvedReferences + self.assertTrue(wrapped.validator) + + def test_wrapped_class(self): + for attribute in dir(self.class_instance): + attr = getattr(self.class_instance, attribute, None) + if attribute in ("test_valid", "always_valid_test"): + self.assertTrue(attr.validator) + else: + with self.assertRaises(AttributeError): + # noinspection PyStatementEffect + attr.validator + + +class LinkTests(unittest.TestCase): + def test_without_text(self): + link = Link(url="http://google.com", text="") + self.assertEqual(f"{link}", "") + + def test_with_text(self): + link = Link(url="http://google.com", text="google") + self.assertEqual(f"{link}", "") + + +class DateLinkTests(unittest.TestCase): + def setUp(self) -> None: + self.epoch = 1234567890 + + def test_simple_formation(self): + datelink = DateLink(date=self.epoch, date_format="{date_long}", fallback=f"{self.epoch}") + self.assertEqual(f"{datelink}", f"") + + def test_with_url(self): + datelink = DateLink( + date=self.epoch, + date_format="{date_long}", + link="http://google.com", + fallback=f"{self.epoch}", + ) + self.assertEqual( + f"{datelink}", + f"", + ) + + +class ObjectLinkTests(unittest.TestCase): + def test_channel(self): + objlink = ObjectLink(object_id="C12345") + self.assertEqual(f"{objlink}", "<#C12345>") + + def test_group_message(self): + objlink = ObjectLink(object_id="G12345") + self.assertEqual(f"{objlink}", "<#G12345>") + + def test_subteam_message(self): + objlink = ObjectLink(object_id="S12345") + self.assertEqual(f"{objlink}", "") + + def test_with_label(self): + objlink = ObjectLink(object_id="C12345", text="abc") + self.assertEqual(f"{objlink}", "<#C12345|abc>") + + def test_unknown_prefix(self): + objlink = ObjectLink(object_id="Z12345") + self.assertEqual(f"{objlink}", "<@Z12345>") + + +class SpecialLinkTests(unittest.TestCase): + def test_channel_link(self): + self.assertEqual(f"{ChannelLink()}", "") + + def test_here_link(self): + self.assertEqual(f"{HereLink()}", "") + + def test_everyone_link(self): + self.assertEqual(f"{EveryoneLink()}", "") + + +class PlainTextObjectTests(unittest.TestCase): + def test_basic_json(self): + self.assertDictEqual( + {"text": "some text", "type": "plain_text"}, + PlainTextObject(text="some text").to_dict(), + ) + + self.assertDictEqual( + {"text": "some text", "emoji": False, "type": "plain_text"}, + PlainTextObject(text="some text", emoji=False).to_dict(), + ) + + def test_from_string(self): + plaintext = PlainTextObject(text="some text", emoji=True) + self.assertDictEqual(plaintext.to_dict(), PlainTextObject.direct_from_string("some text")) + + +class MarkdownTextObjectTests(unittest.TestCase): + def test_basic_json(self): + self.assertDictEqual( + {"text": "some text", "type": "mrkdwn"}, + MarkdownTextObject(text="some text").to_dict(), + ) + + self.assertDictEqual( + {"text": "some text", "verbatim": True, "type": "mrkdwn"}, + MarkdownTextObject(text="some text", verbatim=True).to_dict(), + ) + + def test_from_string(self): + markdown = MarkdownTextObject(text="some text") + self.assertDictEqual(markdown.to_dict(), MarkdownTextObject.direct_from_string("some text")) + + +class ConfirmObjectTests(unittest.TestCase): + def test_basic_json(self): + expected = { + "confirm": {"emoji": True, "text": "Yes", "type": "plain_text"}, + "deny": {"emoji": True, "text": "No", "type": "plain_text"}, + "text": {"text": "are you sure?", "type": "mrkdwn"}, + "title": {"emoji": True, "text": "some title", "type": "plain_text"}, + } + simple_object = ConfirmObject(title="some title", text="are you sure?") + self.assertDictEqual(expected, simple_object.to_dict()) + self.assertDictEqual(expected, simple_object.to_dict("block")) + self.assertDictEqual( + { + "text": "are you sure?", + "title": "some title", + "ok_text": "Okay", + "dismiss_text": "Cancel", + }, + simple_object.to_dict("action"), + ) + + def test_confirm_overrides(self): + confirm = ConfirmObject( + title="some title", + text="are you sure?", + confirm="I'm really sure", + deny="Nevermind", + ) + expected = { + "confirm": {"text": "I'm really sure", "type": "plain_text", "emoji": True}, + "deny": {"text": "Nevermind", "type": "plain_text", "emoji": True}, + "text": {"text": "are you sure?", "type": "mrkdwn"}, + "title": {"text": "some title", "type": "plain_text", "emoji": True}, + } + self.assertDictEqual(expected, confirm.to_dict()) + self.assertDictEqual(expected, confirm.to_dict("block")) + self.assertDictEqual( + { + "text": "are you sure?", + "title": "some title", + "ok_text": "I'm really sure", + "dismiss_text": "Nevermind", + }, + confirm.to_dict("action"), + ) + + def test_passing_text_objects(self): + direct_construction = ConfirmObject(title="title", text="Are you sure?") + + mrkdwn = MarkdownTextObject(text="Are you sure?") + + preconstructed = ConfirmObject(title="title", text=mrkdwn) + + self.assertDictEqual(direct_construction.to_dict(), preconstructed.to_dict()) + + plaintext = PlainTextObject(text="Are you sure?", emoji=False) + + passed_plaintext = ConfirmObject(title="title", text=plaintext) + + self.assertDictEqual( + { + "confirm": {"emoji": True, "text": "Yes", "type": "plain_text"}, + "deny": {"emoji": True, "text": "No", "type": "plain_text"}, + "text": {"emoji": False, "text": "Are you sure?", "type": "plain_text"}, + "title": {"emoji": True, "text": "title", "type": "plain_text"}, + }, + passed_plaintext.to_dict(), + ) + + def test_title_length(self): + with self.assertRaises(SlackObjectFormationError): + ConfirmObject(title=STRING_301_CHARS, text="Are you sure?").to_dict() + + def test_text_length(self): + with self.assertRaises(SlackObjectFormationError): + ConfirmObject(title="title", text=STRING_301_CHARS).to_dict() + + def test_text_length_with_object(self): + with self.assertRaises(SlackObjectFormationError): + plaintext = PlainTextObject(text=STRING_301_CHARS) + ConfirmObject(title="title", text=plaintext).to_dict() + + with self.assertRaises(SlackObjectFormationError): + markdown = MarkdownTextObject(text=STRING_301_CHARS) + ConfirmObject(title="title", text=markdown).to_dict() + + def test_confirm_length(self): + with self.assertRaises(SlackObjectFormationError): + ConfirmObject(title="title", text="Are you sure?", confirm=STRING_51_CHARS).to_dict() + + def test_deny_length(self): + with self.assertRaises(SlackObjectFormationError): + ConfirmObject(title="title", text="Are you sure?", deny=STRING_51_CHARS).to_dict() + + +class FeedbackButtonObjectTests(unittest.TestCase): + def test_basic_json(self): + feedback_button = FeedbackButtonObject(text="+1", value="positive") + expected = {"text": {"emoji": True, "text": "+1", "type": "plain_text"}, "value": "positive"} + self.assertDictEqual(expected, feedback_button.to_dict()) + + def test_with_accessibility_label(self): + feedback_button = FeedbackButtonObject(text="+1", value="positive", accessibility_label="Positive feedback button") + expected = { + "text": {"emoji": True, "text": "+1", "type": "plain_text"}, + "value": "positive", + "accessibility_label": "Positive feedback button", + } + self.assertDictEqual(expected, feedback_button.to_dict()) + + def test_with_plain_text_object(self): + text_obj = PlainTextObject(text="-1", emoji=False) + feedback_button = FeedbackButtonObject(text=text_obj, value="negative") + expected = { + "text": {"emoji": False, "text": "-1", "type": "plain_text"}, + "value": "negative", + } + self.assertDictEqual(expected, feedback_button.to_dict()) + + def test_text_length_validation(self): + with self.assertRaises(SlackObjectFormationError): + FeedbackButtonObject(text="a" * 76, value="test").to_dict() + + def test_value_length_validation(self): + with self.assertRaises(SlackObjectFormationError): + FeedbackButtonObject(text="+1", value="a" * 2001).to_dict() + + def test_parse_from_dict(self): + data = {"text": "+1", "value": "positive", "accessibility_label": "Positive feedback"} + parsed = FeedbackButtonObject.parse(data) + self.assertIsInstance(parsed, FeedbackButtonObject) + expected = { + "text": {"emoji": True, "text": "+1", "type": "plain_text"}, + "value": "positive", + "accessibility_label": "Positive feedback", + } + self.assertDictEqual(expected, parsed.to_dict()) + + def test_parse_from_existing_object(self): + original = FeedbackButtonObject(text="-1", value="negative") + parsed = FeedbackButtonObject.parse(original) + self.assertIs(original, parsed) + + def test_parse_none(self): + self.assertIsNone(FeedbackButtonObject.parse(None)) + + +class OptionTests(unittest.TestCase): + def setUp(self) -> None: + self.common = Option(label="an option", value="option_1") + + def test_block_style_json(self): + expected = { + "text": {"type": "plain_text", "text": "an option", "emoji": True}, + "value": "option_1", + } + self.assertDictEqual(expected, self.common.to_dict("block")) + self.assertDictEqual(expected, self.common.to_dict()) + + def test_dialog_style_json(self): + expected = {"label": "an option", "value": "option_1"} + self.assertDictEqual(expected, self.common.to_dict("dialog")) + + def test_action_style_json(self): + expected = {"text": "an option", "value": "option_1"} + self.assertDictEqual(expected, self.common.to_dict("action")) + + def test_from_single_value(self): + option = Option(label="option_1", value="option_1") + self.assertDictEqual( + option.to_dict("text"), + option.from_single_value("option_1").to_dict("text"), + ) + + def test_label_length(self): + with self.assertRaises(SlackObjectFormationError): + Option(label=STRING_301_CHARS, value="option_1").to_dict("text") + + def test_value_length(self): + with self.assertRaises(SlackObjectFormationError): + Option(label="option_1", value=STRING_301_CHARS).to_dict("text") + + def test_valid_description_for_blocks(self): + option = Option(label="label", value="v", description="this is an option") + self.assertDictEqual( + option.to_dict(), + { + "text": { + "type": "plain_text", + "text": "label", + "emoji": True, + }, + "value": "v", + "description": { + "type": "plain_text", + "text": "this is an option", + "emoji": True, + }, + }, + ) + option = Option( + # Note that mrkdwn type is not allowed for this (as of April 2021) + text=PlainTextObject(text="label"), + value="v", + description="this is an option", + ) + self.assertDictEqual( + option.to_dict(), + { + "text": {"type": "plain_text", "text": "label"}, + "value": "v", + "description": { + "type": "plain_text", + "text": "this is an option", + "emoji": True, + }, + }, + ) + + def test_valid_description_for_attachments(self): + option = Option(label="label", value="v", description="this is an option") + # legacy message actions in attachments + self.assertDictEqual( + option.to_dict("action"), + { + "text": "label", + "value": "v", + "description": "this is an option", + }, + ) + self.assertDictEqual( + option.to_dict("attachment"), + { + "text": "label", + "value": "v", + "description": "this is an option", + }, + ) + + +class OptionGroupTests(unittest.TestCase): + maxDiff = None + + def setUp(self) -> None: + self.common_options = [ + Option.from_single_value("one"), + Option.from_single_value("two"), + Option.from_single_value("three"), + ] + + self.common = OptionGroup(label="an option", options=self.common_options) + + def test_block_style_json(self): + expected = { + "label": {"emoji": True, "text": "an option", "type": "plain_text"}, + "options": [ + { + "text": {"emoji": True, "text": "one", "type": "plain_text"}, + "value": "one", + }, + { + "text": {"emoji": True, "text": "two", "type": "plain_text"}, + "value": "two", + }, + { + "text": {"emoji": True, "text": "three", "type": "plain_text"}, + "value": "three", + }, + ], + } + self.assertDictEqual(expected, self.common.to_dict("block")) + self.assertDictEqual(expected, self.common.to_dict()) + + def test_dialog_style_json(self): + self.assertDictEqual( + { + "label": "an option", + "options": [ + {"label": "one", "value": "one"}, + {"label": "two", "value": "two"}, + {"label": "three", "value": "three"}, + ], + }, + self.common.to_dict("dialog"), + ) + + def test_action_style_json(self): + self.assertDictEqual( + { + "text": "an option", + "options": [ + {"text": "one", "value": "one"}, + {"text": "two", "value": "two"}, + {"text": "three", "value": "three"}, + ], + }, + self.common.to_dict("action"), + ) + + def test_label_length(self): + with self.assertRaises(SlackObjectFormationError): + OptionGroup(label=STRING_301_CHARS, options=self.common_options).to_dict("text") + + def test_options_length(self): + with self.assertRaises(SlackObjectFormationError): + OptionGroup(label="option_group", options=self.common_options * 34).to_dict("text") + + def test_confirm_style(self): + obj = ConfirmObject.parse( + { + "title": {"type": "plain_text", "text": "Are you sure?"}, + "text": { + "type": "mrkdwn", + "text": "Wouldn't you prefer a good game of _chess_?", + }, + "confirm": {"type": "plain_text", "text": "Do it"}, + "deny": {"type": "plain_text", "text": "Stop, I've changed my mind!"}, + "style": "primary", + } + ) + obj.validate_json() + self.assertEqual("primary", obj.style) + + def test_confirm_style_validation(self): + with self.assertRaises(SlackObjectFormationError): + ConfirmObject.parse( + { + "title": {"type": "plain_text", "text": "Are you sure?"}, + "text": { + "type": "mrkdwn", + "text": "Wouldn't you prefer a good game of _chess_?", + }, + "confirm": {"type": "plain_text", "text": "Do it"}, + "deny": { + "type": "plain_text", + "text": "Stop, I've changed my mind!", + }, + "style": "something-wrong", + } + ).validate_json() + + +class WorkflowTests(unittest.TestCase): + def test_creation(self): + workflow = Workflow( + trigger=WorkflowTrigger( + url="https://slack.com/shortcuts/Ft0123ABC456/xyz...zyx", + customizable_input_parameters=[ + {"name": "input_parameter_a", "value": "Value for input param A"}, + {"name": "input_parameter_b", "value": "Value for input param B"}, + ], + ) + ) + self.assertDictEqual( + workflow.to_dict(), + { + "trigger": { + "url": "https://slack.com/shortcuts/Ft0123ABC456/xyz...zyx", + "customizable_input_parameters": [ + {"name": "input_parameter_a", "value": "Value for input param A"}, + {"name": "input_parameter_b", "value": "Value for input param B"}, + ], + } + }, + ) diff --git a/tests/slack_sdk/models/test_options.py b/tests/slack_sdk/models/test_options.py new file mode 100644 index 000000000..37a165f58 --- /dev/null +++ b/tests/slack_sdk/models/test_options.py @@ -0,0 +1,44 @@ +import unittest + +from slack_sdk.models.blocks import ( + StaticSelectElement, + Option, +) + + +class TestOptions(unittest.TestCase): + def test_with_static_select_element(self): + self.maxDiff = None + + elem = StaticSelectElement( + action_id="action-id", + initial_option=Option(value="option-1", text="Option 1"), + options=[ + Option(value="option-1", text="Option 1"), + Option(value="option-2", text="Option 2"), + Option(value="option-3", text="Option 3"), + ], + ) + expected = { + "action_id": "action-id", + "initial_option": { + "text": {"emoji": True, "text": "Option 1", "type": "plain_text"}, + "value": "option-1", + }, + "options": [ + { + "text": {"emoji": True, "text": "Option 1", "type": "plain_text"}, + "value": "option-1", + }, + { + "text": {"emoji": True, "text": "Option 2", "type": "plain_text"}, + "value": "option-2", + }, + { + "text": {"emoji": True, "text": "Option 3", "type": "plain_text"}, + "value": "option-3", + }, + ], + "type": "static_select", + } + self.assertDictEqual(expected, elem.to_dict()) diff --git a/tests/slack_sdk/models/test_views.py b/tests/slack_sdk/models/test_views.py new file mode 100644 index 000000000..a8bdfb9ff --- /dev/null +++ b/tests/slack_sdk/models/test_views.py @@ -0,0 +1,495 @@ +import json +import logging +import unittest + +from slack_sdk.errors import SlackObjectFormationError +from slack_sdk.models.blocks import ( + InputBlock, + SectionBlock, + DividerBlock, + ActionsBlock, + ContextBlock, + PlainTextInputElement, + RadioButtonsElement, + CheckboxesElement, + ButtonElement, + ImageElement, + PlainTextObject, + Option, + MarkdownTextObject, +) +from slack_sdk.models.views import View, ViewState, ViewStateValue + + +class ViewTests(unittest.TestCase): + maxDiff = None + + def setUp(self) -> None: + self.logger = logging.getLogger(__name__) + + def verify_loaded_view_object(self, file): + input = json.load(file) + view = View(**input) + self.assertTrue(view.state is None or isinstance(view.state, ViewState)) + self.assertDictEqual(input, view.to_dict()) + + # -------------------------------- + # Modals + # -------------------------------- + + def test_valid_construction(self): + modal_view = View( + type="modal", + callback_id="modal-id", + title=PlainTextObject(text="Awesome Modal"), + submit=PlainTextObject(text="Submit"), + close=PlainTextObject(text="Cancel"), + blocks=[ + InputBlock( + block_id="b-id", + label=PlainTextObject(text="Input label"), + element=PlainTextInputElement(action_id="a-id"), + ), + InputBlock( + block_id="cb-id", + label=PlainTextObject(text="Label"), + element=CheckboxesElement( + action_id="a-cb-id", + options=[ + Option( + text=PlainTextObject(text="*this is plain_text text*"), + value="v1", + ), + Option( + text=MarkdownTextObject(text="*this is mrkdwn text*"), + value="v2", + ), + ], + ), + ), + SectionBlock( + block_id="sb-id", + text=MarkdownTextObject(text="This is a mrkdwn text section block."), + fields=[ + PlainTextObject(text="*this is plain_text text*", emoji=True), + MarkdownTextObject(text="*this is mrkdwn text*"), + PlainTextObject(text="*this is plain_text text*", emoji=True), + ], + ), + DividerBlock(), + SectionBlock( + block_id="rb-id", + text=MarkdownTextObject(text="This is a section block with radio button accessory"), + accessory=RadioButtonsElement( + initial_option=Option( + text=PlainTextObject(text="Option 1"), + value="option 1", + description=PlainTextObject(text="Description for option 1"), + ), + options=[ + Option( + text=PlainTextObject(text="Option 1"), + value="option 1", + description=PlainTextObject(text="Description for option 1"), + ), + Option( + text=PlainTextObject(text="Option 2"), + value="option 2", + description=PlainTextObject(text="Description for option 2"), + ), + ], + ), + ), + ], + state=ViewState( + values={ + "b1": {"a1": ViewStateValue(type="plain_text_input", value="Title")}, + "b2": {"a2": ViewStateValue(type="plain_text_input", value="Description")}, + } + ), + ) + modal_view.validate_json() + + def test_invalid_type_value(self): + modal_view = View( + type="modallll", + callback_id="modal-id", + title=PlainTextObject(text="Awesome Modal"), + submit=PlainTextObject(text="Submit"), + close=PlainTextObject(text="Cancel"), + blocks=[ + InputBlock( + block_id="b-id", + label=PlainTextObject(text="Input label"), + element=PlainTextInputElement(action_id="a-id"), + ), + ], + ) + with self.assertRaises(SlackObjectFormationError): + modal_view.validate_json() + + def test_simple_state_values(self): + expected = { + "values": { + "b1": {"a1": {"type": "plain_text_input", "value": "Title"}}, + "b2": {"a2": {"type": "plain_text_input", "value": "Description"}}, + } + } + state = ViewState( + values={ + "b1": {"a1": ViewStateValue(type="plain_text_input", value="Title")}, + "b2": {"a2": {"type": "plain_text_input", "value": "Description"}}, + } + ) + self.assertDictEqual(expected, ViewState(**expected).to_dict()) + self.assertDictEqual(expected, state.to_dict()) + + def test_all_state_values(self): + # Testing with + # {"type":"modal","title":{"type":"plain_text","text":"My App","emoji":true},"submit":{"type":"plain_text","text":"Submit","emoji":true},"close":{"type":"plain_text","text":"Cancel","emoji":true},"blocks":[{"type":"input","element":{"type":"plain_text_input"},"label":{"type":"plain_text","text":"Label","emoji":true}},{"type":"input","element":{"type":"plain_text_input","multiline":true},"label":{"type":"plain_text","text":"Label","emoji":true}},{"type":"input","element":{"type":"datepicker","initial_date":"1990-04-28","placeholder":{"type":"plain_text","text":"Select a date","emoji":true}},"label":{"type":"plain_text","text":"Label","emoji":true}},{"type":"input","element":{"type":"users_select","placeholder":{"type":"plain_text","text":"Select a user","emoji":true}},"label":{"type":"plain_text","text":"Label","emoji":true}},{"type":"input","element":{"type":"multi_static_select","placeholder":{"type":"plain_text","text":"Select options","emoji":true},"options":[{"text":{"type":"plain_text","text":"*this is plain_text text*","emoji":true},"value":"value-0"},{"text":{"type":"plain_text","text":"*this is plain_text text*","emoji":true},"value":"value-1"},{"text":{"type":"plain_text","text":"*this is plain_text text*","emoji":true},"value":"value-2"}]},"label":{"type":"plain_text","text":"Label","emoji":true}},{"type":"input","element":{"type":"checkboxes","options":[{"text":{"type":"plain_text","text":"*this is plain_text text*","emoji":true},"value":"value-0"},{"text":{"type":"plain_text","text":"*this is plain_text text*","emoji":true},"value":"value-1"},{"text":{"type":"plain_text","text":"*this is plain_text text*","emoji":true},"value":"value-2"}]},"label":{"type":"plain_text","text":"Label","emoji":true}},{"type":"input","element":{"type":"radio_buttons","initial_option":{"text":{"type":"plain_text","text":"Option 1"},"value":"option 1","description":{"type":"plain_text","text":"Description for option 1"}},"options":[{"text":{"type":"plain_text","text":"Option 1"},"value":"option 1","description":{"type":"plain_text","text":"Description for option 1"}},{"text":{"type":"plain_text","text":"Option 2"},"value":"option 2","description":{"type":"plain_text","text":"Description for option 2"}},{"text":{"type":"plain_text","text":"Option 3"},"value":"option 3","description":{"type":"plain_text","text":"Description for option 3"}}]},"label":{"type":"plain_text","text":"Label","emoji":true}}]} + expected = { + "values": { + "b1": {"a1": {"type": "datepicker", "selected_date": "1990-04-12"}}, + "b2": {"a2": {"type": "plain_text_input", "value": "This is a test"}}, + # multiline + "b3": { + "a3": { + "type": "plain_text_input", + "value": "Something wrong\nPlease help me!", + } + }, + "b4": {"a4": {"type": "users_select", "selected_user": "U123"}}, + "b4-2": { + "a4-2": { + "type": "multi_users_select", + "selected_users": ["U123", "U234"], + } + }, + "b5": { + "a5": { + "type": "conversations_select", + "selected_conversation": "C123", + } + }, + "b5-2": { + "a5-2": { + "type": "multi_conversations_select", + "selected_conversations": ["C123", "C234"], + } + }, + "b6": {"a6": {"type": "channels_select", "selected_channel": "C123"}}, + "b6-2": { + "a6-2": { + "type": "multi_channels_select", + "selected_channels": ["C123", "C234"], + } + }, + "b7": { + "a7": { + "type": "multi_static_select", + "selected_options": [ + { + "text": { + "type": "plain_text", + "text": "*this is plain_text text*", + "emoji": True, + }, + "value": "value-0", + }, + { + "text": { + "type": "plain_text", + "text": "*this is plain_text text*", + "emoji": True, + }, + "value": "value-1", + }, + ], + } + }, + "b8": { + "a8": { + "type": "checkboxes", + "selected_options": [ + { + "text": { + "type": "plain_text", + "text": "*this is plain_text text*", + "emoji": True, + }, + "value": "value-0", + }, + { + "text": { + "type": "plain_text", + "text": "*this is plain_text text*", + "emoji": True, + }, + "value": "value-1", + }, + ], + } + }, + "b9": { + "a9": { + "type": "radio_buttons", + "selected_option": { + "text": { + "type": "plain_text", + "text": "Option 1", + "emoji": True, + }, + "value": "option 1", + "description": { + "type": "plain_text", + "text": "Description for option 1", + "emoji": True, + }, + }, + } + }, + } + } + state = ViewState( + values={ + "b1": {"a1": ViewStateValue(type="datepicker", selected_date="1990-04-12")}, + "b2": {"a2": ViewStateValue(type="plain_text_input", value="This is a test")}, + "b3": { + "a3": ViewStateValue( + type="plain_text_input", + value="Something wrong\nPlease help me!", + ) + }, + "b4": {"a4": ViewStateValue(type="users_select", selected_user="U123")}, + "b4-2": {"a4-2": ViewStateValue(type="multi_users_select", selected_users=["U123", "U234"])}, + "b5": {"a5": ViewStateValue(type="conversations_select", selected_conversation="C123")}, + "b5-2": { + "a5-2": ViewStateValue( + type="multi_conversations_select", + selected_conversations=["C123", "C234"], + ) + }, + "b6": {"a6": ViewStateValue(type="channels_select", selected_channel="C123")}, + "b6-2": {"a6-2": ViewStateValue(type="multi_channels_select", selected_channels=["C123", "C234"])}, + "b7": { + "a7": ViewStateValue( + type="multi_static_select", + selected_options=[ + Option( + text=PlainTextObject(text="*this is plain_text text*", emoji=True), + value="value-0", + ), + Option( + text=PlainTextObject(text="*this is plain_text text*", emoji=True), + value="value-1", + ), + ], + ) + }, + "b8": { + "a8": ViewStateValue( + type="checkboxes", + selected_options=[ + Option( + text=PlainTextObject(text="*this is plain_text text*", emoji=True), + value="value-0", + ), + Option( + text=PlainTextObject(text="*this is plain_text text*", emoji=True), + value="value-1", + ), + ], + ) + }, + "b9": { + "a9": ViewStateValue( + type="radio_buttons", + selected_option=Option( + text=PlainTextObject(text="Option 1", emoji=True), + value="option 1", + description=PlainTextObject(text="Description for option 1", emoji=True), + ), + ) + }, + } + ) + self.assertDictEqual(expected, ViewState(**expected).to_dict()) + self.assertDictEqual(expected, state.to_dict()) + + def test_view_state_value_empty_selected_options(self): + input = {"type": "checkboxes", "selected_options": []} + + view_state_value = ViewStateValue(**input) + assert view_state_value.selected_options == [] + + def test_view_state_value_with_selected_options(self): + expected = { + "type": "checkboxes", + "selected_options": [ + { + "text": {"type": "plain_text", "text": "test_option_text"}, + "value": "test_option_value", + } + ], + } + + input = { + "type": "checkboxes", + "selected_options": [ + { + "text": {"type": "plain_text", "text": "test_option_text"}, + "value": "test_option_value", + } + ], + } + + self.assertDictEqual(expected, ViewStateValue(**input).to_dict()) + + def test_load_modal_view_001(self): + with open("tests/slack_sdk_fixture/view_modal_001.json") as file: + self.verify_loaded_view_object(file) + + def test_load_modal_view_002(self): + with open("tests/slack_sdk_fixture/view_modal_002.json") as file: + self.verify_loaded_view_object(file) + + def test_load_modal_view_003(self): + with open("tests/slack_sdk_fixture/view_modal_003.json") as file: + self.verify_loaded_view_object(file) + + def test_load_modal_view_004(self): + with open("tests/slack_sdk_fixture/view_modal_004.json") as file: + self.verify_loaded_view_object(file) + + def test_load_modal_view_005(self): + with open("tests/slack_sdk_fixture/view_modal_005.json") as file: + self.verify_loaded_view_object(file) + + def test_load_modal_view_006(self): + with open("tests/slack_sdk_fixture/view_modal_006.json") as file: + self.verify_loaded_view_object(file) + + def test_load_modal_view_007(self): + with open("tests/slack_sdk_fixture/view_modal_007.json") as file: + self.verify_loaded_view_object(file) + + def test_load_modal_view_008(self): + with open("tests/slack_sdk_fixture/view_modal_008.json") as file: + self.verify_loaded_view_object(file) + + def test_load_modal_view_009(self): + with open("tests/slack_sdk_fixture/view_modal_009.json") as file: + self.verify_loaded_view_object(file) + + def test_load_modal_view_010(self): + with open("tests/slack_sdk_fixture/view_modal_010.json") as file: + self.verify_loaded_view_object(file) + + # -------------------------------- + # Home Tabs + # -------------------------------- + + def test_home_tab_construction(self): + home_tab_view = View( + type="home", + blocks=[ + SectionBlock( + text=MarkdownTextObject(text="*Here's what you can do with Project Tracker:*"), + ), + ActionsBlock( + elements=[ + ButtonElement( + text=PlainTextObject(text="Create New Task", emoji=True), + style="primary", + value="create_task", + ), + ButtonElement( + text=PlainTextObject(text="Create New Project", emoji=True), + value="create_project", + ), + ButtonElement( + text=PlainTextObject(text="Help", emoji=True), + value="help", + ), + ], + ), + ContextBlock( + elements=[ + ImageElement( + image_url="https://api.slack.com/img/blocks/bkb_template_images/placeholder.png", + alt_text="placeholder", + ), + ], + ), + SectionBlock( + text=MarkdownTextObject(text="*Your Configurations*"), + ), + DividerBlock(), + SectionBlock( + text=MarkdownTextObject( + text="*#public-relations*\n posts new tasks, comments, and project updates to " + ), + accessory=ButtonElement( + text=PlainTextObject(text="Edit", emoji=True), + value="public-relations", + ), + ), + ], + ) + home_tab_view.validate_json() + + def test_submit_in_home_tab(self): + modal_view = View( + type="home", + callback_id="home-tab-id", + submit=PlainTextObject(text="Submit"), + blocks=[DividerBlock()], + ) + with self.assertRaises(SlackObjectFormationError): + modal_view.validate_json() + + def test_close_in_home_tab(self): + modal_view = View( + type="home", + callback_id="home-tab-id", + close=PlainTextObject(text="Cancel"), + blocks=[DividerBlock()], + ) + with self.assertRaises(SlackObjectFormationError): + modal_view.validate_json() + + def test_load_home_tab_view_001(self): + with open("tests/slack_sdk_fixture/view_home_001.json") as file: + self.verify_loaded_view_object(file) + + def test_load_home_tab_view_002(self): + with open("tests/slack_sdk_fixture/view_home_002.json") as file: + self.verify_loaded_view_object(file) + + def test_load_home_tab_view_003(self): + with open("tests/slack_sdk_fixture/view_home_003.json") as file: + self.verify_loaded_view_object(file) + + def test_load_home_tab_view_004(self): + with open("tests/slack_sdk_fixture/view_home_004.json") as file: + self.verify_loaded_view_object(file) + + def test_load_home_tab_view_005(self): + with open("tests/slack_sdk_fixture/view_home_005.json") as file: + self.verify_loaded_view_object(file) + + def test_load_home_tab_view_006(self): + with open("tests/slack_sdk_fixture/view_home_006.json") as file: + self.verify_loaded_view_object(file) + + def test_eq(self): + input = { + "type": "modal", + "blocks": [DividerBlock()], + } + another_input = { + "type": "modal", + "blocks": [DividerBlock(), DividerBlock()], + } + self.assertEqual(View(**input), View(**input)) + self.assertNotEqual(View(**input), View(**another_input)) diff --git a/tests/slack_sdk/my_retry_handler.py b/tests/slack_sdk/my_retry_handler.py new file mode 100644 index 000000000..601900a0d --- /dev/null +++ b/tests/slack_sdk/my_retry_handler.py @@ -0,0 +1,33 @@ +from http.client import RemoteDisconnected +from typing import Optional +from slack_sdk.http_retry.interval_calculator import RetryIntervalCalculator +from slack_sdk.http_retry.state import RetryState +from slack_sdk.http_retry.request import HttpRequest +from slack_sdk.http_retry.response import HttpResponse +from slack_sdk.http_retry.handler import RetryHandler, default_interval_calculator + + +class MyRetryHandler(RetryHandler): + def __init__( + self, + max_retry_count: int = 1, + interval_calculator: RetryIntervalCalculator = default_interval_calculator, + ): + super().__init__(max_retry_count, interval_calculator) + self.call_count = 0 + + def _can_retry( + self, + *, + state: RetryState, + request: HttpRequest, + response: Optional[HttpResponse], + error: Optional[Exception], + ) -> bool: + self.call_count += 1 + if error is None: + return False + for error_type in [ConnectionResetError, RemoteDisconnected]: + if isinstance(error, error_type): + return True + return False diff --git a/tests/slack_sdk/oauth/__init__.py b/tests/slack_sdk/oauth/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/slack_sdk/oauth/authorize_url_generator/__init__.py b/tests/slack_sdk/oauth/authorize_url_generator/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/slack_sdk/oauth/authorize_url_generator/test_generator.py b/tests/slack_sdk/oauth/authorize_url_generator/test_generator.py new file mode 100644 index 000000000..a4973c335 --- /dev/null +++ b/tests/slack_sdk/oauth/authorize_url_generator/test_generator.py @@ -0,0 +1,72 @@ +import unittest + +from slack_sdk.oauth import AuthorizeUrlGenerator, OpenIDConnectAuthorizeUrlGenerator + + +class TestGenerator(unittest.TestCase): + def setUp(self): + pass + + def tearDown(self): + pass + + def test_default(self): + generator = AuthorizeUrlGenerator( + client_id="111.222", + scopes=["chat:write", "commands"], + user_scopes=["search:read"], + ) + url = generator.generate("state-value") + expected = "https://slack.com/oauth/v2/authorize?state=state-value&client_id=111.222&scope=chat:write,commands&user_scope=search:read" + self.assertEqual(expected, url) + + def test_base_url(self): + generator = AuthorizeUrlGenerator( + client_id="111.222", + scopes=["chat:write", "commands"], + user_scopes=["search:read"], + authorization_url="https://www.example.com/authorize", + ) + url = generator.generate("state-value") + expected = ( + "https://www.example.com/authorize" + "?state=state-value" + "&client_id=111.222" + "&scope=chat:write,commands" + "&user_scope=search:read" + ) + self.assertEqual(expected, url) + + def test_team(self): + generator = AuthorizeUrlGenerator( + client_id="111.222", + scopes=["chat:write", "commands"], + user_scopes=["search:read"], + ) + url = generator.generate(state="state-value", team="T12345") + expected = ( + "https://slack.com/oauth/v2/authorize" + "?state=state-value" + "&client_id=111.222" + "&scope=chat:write,commands&user_scope=search:read" + "&team=T12345" + ) + self.assertEqual(expected, url) + + def test_openid_connect(self): + generator = OpenIDConnectAuthorizeUrlGenerator( + client_id="111.222", + redirect_uri="https://www.example.com/oidc/callback", + scopes=["openid"], + ) + url = generator.generate(state="state-value", nonce="nnn", team="T12345") + expected = ( + "https://slack.com/openid/connect/authorize" + "?response_type=code&state=state-value" + "&client_id=111.222" + "&scope=openid" + "&redirect_uri=https://www.example.com/oidc/callback" + "&team=T12345" + "&nonce=nnn" + ) + self.assertEqual(expected, url) diff --git a/tests/slack_sdk/oauth/installation_store/__init__.py b/tests/slack_sdk/oauth/installation_store/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/slack_sdk/oauth/installation_store/test_amazon_s3.py b/tests/slack_sdk/oauth/installation_store/test_amazon_s3.py new file mode 100644 index 000000000..a51f37e9d --- /dev/null +++ b/tests/slack_sdk/oauth/installation_store/test_amazon_s3.py @@ -0,0 +1,303 @@ +import unittest + +import boto3 + +try: + from moto import mock_aws +except ImportError: + from moto import mock_s3 as mock_aws +from slack_sdk.oauth.installation_store import Installation +from slack_sdk.oauth.installation_store.amazon_s3 import AmazonS3InstallationStore + + +class TestAmazonS3(unittest.TestCase): + mock_aws = mock_aws() + bucket_name = "test-bucket" + + def setUp(self): + self.mock_aws.start() + s3 = boto3.resource("s3") + bucket = s3.Bucket(self.bucket_name) + bucket.create(CreateBucketConfiguration={"LocationConstraint": "af-south-1"}) + + def tearDown(self): + self.mock_aws.stop() + + def build_store(self) -> AmazonS3InstallationStore: + return AmazonS3InstallationStore( + s3_client=boto3.client("s3"), + bucket_name=self.bucket_name, + client_id="111.222", + ) + + def test_instance(self): + store = self.build_store() + self.assertIsNotNone(store) + + def test_save_and_find(self): + store = self.build_store() + installation = Installation( + app_id="A111", + enterprise_id="E111", + team_id="T111", + user_id="U111", + bot_id="B111", + bot_token="xoxb-111", + bot_scopes=["chat:write"], + bot_user_id="U222", + ) + store.save(installation) + + # find bots + bot = store.find_bot(enterprise_id="E111", team_id="T111") + self.assertIsNotNone(bot) + bot = store.find_bot(enterprise_id="E111", team_id="T222") + self.assertIsNone(bot) + bot = store.find_bot(enterprise_id=None, team_id="T111") + self.assertIsNone(bot) + + # delete bots + store.delete_bot(enterprise_id="E111", team_id="T222") + bot = store.find_bot(enterprise_id="E111", team_id="T222") + self.assertIsNone(bot) + + # find installations + i = store.find_installation(enterprise_id="E111", team_id="T111") + self.assertIsNotNone(i) + i = store.find_installation(enterprise_id="E111", team_id="T222") + self.assertIsNone(i) + i = store.find_installation(enterprise_id=None, team_id="T111") + self.assertIsNone(i) + + i = store.find_installation(enterprise_id="E111", team_id="T111", user_id="U111") + self.assertIsNotNone(i) + i = store.find_installation(enterprise_id="E111", team_id="T111", user_id="U222") + self.assertIsNone(i) + i = store.find_installation(enterprise_id="E111", team_id="T222", user_id="U111") + self.assertIsNone(i) + + # delete installations + store.delete_installation(enterprise_id="E111", team_id="T111", user_id="U111") + i = store.find_installation(enterprise_id="E111", team_id="T111", user_id="U111") + self.assertIsNone(i) + i = store.find_installation(enterprise_id="E111", team_id="T111") + self.assertIsNone(i) + + # delete all + store.save(installation) + store.delete_all(enterprise_id="E111", team_id="T111") + + i = store.find_installation(enterprise_id="E111", team_id="T111") + self.assertIsNone(i) + i = store.find_installation(enterprise_id="E111", team_id="T111", user_id="U111") + self.assertIsNone(i) + bot = store.find_bot(enterprise_id="E111", team_id="T222") + self.assertIsNone(bot) + + def test_org_installation(self): + store = self.build_store() + installation = Installation( + app_id="AO111", + enterprise_id="EO111", + user_id="UO111", + bot_id="BO111", + bot_token="xoxb-O111", + bot_scopes=["chat:write"], + bot_user_id="UO222", + is_enterprise_install=True, + ) + store.save(installation) + + # find bots + bot = store.find_bot(enterprise_id="EO111", team_id=None) + self.assertIsNotNone(bot) + bot = store.find_bot(enterprise_id="EO111", team_id="TO222", is_enterprise_install=True) + self.assertIsNotNone(bot) + bot = store.find_bot(enterprise_id="EO111", team_id="TO222") + self.assertIsNone(bot) + bot = store.find_bot(enterprise_id=None, team_id="TO111") + self.assertIsNone(bot) + + # delete bots + store.delete_bot(enterprise_id="EO111", team_id="TO222") + bot = store.find_bot(enterprise_id="EO111", team_id=None) + self.assertIsNotNone(bot) + + store.delete_bot(enterprise_id="EO111", team_id=None) + bot = store.find_bot(enterprise_id="EO111", team_id=None) + self.assertIsNone(bot) + + # find installations + i = store.find_installation(enterprise_id="EO111", team_id=None) + self.assertIsNotNone(i) + i = store.find_installation(enterprise_id="EO111", team_id="T111", is_enterprise_install=True) + self.assertIsNotNone(i) + i = store.find_installation(enterprise_id="EO111", team_id="T222") + self.assertIsNone(i) + i = store.find_installation(enterprise_id=None, team_id="T111") + self.assertIsNone(i) + + i = store.find_installation(enterprise_id="EO111", team_id=None, user_id="UO111") + self.assertIsNotNone(i) + i = store.find_installation( + enterprise_id="E111", + team_id="T111", + is_enterprise_install=True, + user_id="U222", + ) + self.assertIsNone(i) + i = store.find_installation(enterprise_id=None, team_id="T222", user_id="U111") + self.assertIsNone(i) + + # delete installations + store.delete_installation(enterprise_id="E111", team_id=None) + i = store.find_installation(enterprise_id="E111", team_id=None) + self.assertIsNone(i) + + # delete all + store.save(installation) + store.delete_all(enterprise_id="E111", team_id=None) + + i = store.find_installation(enterprise_id="E111", team_id=None) + self.assertIsNone(i) + i = store.find_installation(enterprise_id="E111", team_id=None, user_id="U111") + self.assertIsNone(i) + bot = store.find_bot(enterprise_id=None, team_id="T222") + self.assertIsNone(bot) + + def test_save_and_find_token_rotation(self): + store = self.build_store() + installation = Installation( + app_id="A111", + enterprise_id="E111", + team_id="T111", + user_id="U111", + bot_id="B111", + bot_token="xoxe.xoxp-1-initial", + bot_scopes=["chat:write"], + bot_user_id="U222", + bot_refresh_token="xoxe-1-initial", + bot_token_expires_in=43200, + ) + store.save(installation) + + bot = store.find_bot(enterprise_id="E111", team_id="T111") + self.assertIsNotNone(bot) + self.assertEqual(bot.bot_refresh_token, "xoxe-1-initial") + + # Update the existing data + refreshed_installation = Installation( + app_id="A111", + enterprise_id="E111", + team_id="T111", + user_id="U111", + bot_id="B111", + bot_token="xoxe.xoxp-1-refreshed", + bot_scopes=["chat:write"], + bot_user_id="U222", + bot_refresh_token="xoxe-1-refreshed", + bot_token_expires_in=43200, + ) + store.save(refreshed_installation) + + # find bots + bot = store.find_bot(enterprise_id="E111", team_id="T111") + self.assertIsNotNone(bot) + self.assertEqual(bot.bot_refresh_token, "xoxe-1-refreshed") + bot = store.find_bot(enterprise_id="E111", team_id="T222") + self.assertIsNone(bot) + bot = store.find_bot(enterprise_id=None, team_id="T111") + self.assertIsNone(bot) + + # delete bots + store.delete_bot(enterprise_id="E111", team_id="T222") + bot = store.find_bot(enterprise_id="E111", team_id="T222") + self.assertIsNone(bot) + + # find installations + i = store.find_installation(enterprise_id="E111", team_id="T111") + self.assertIsNotNone(i) + i = store.find_installation(enterprise_id="E111", team_id="T222") + self.assertIsNone(i) + i = store.find_installation(enterprise_id=None, team_id="T111") + self.assertIsNone(i) + + i = store.find_installation(enterprise_id="E111", team_id="T111", user_id="U111") + self.assertIsNotNone(i) + i = store.find_installation(enterprise_id="E111", team_id="T111", user_id="U222") + self.assertIsNone(i) + i = store.find_installation(enterprise_id="E111", team_id="T222", user_id="U111") + self.assertIsNone(i) + + # delete installations + store.delete_installation(enterprise_id="E111", team_id="T111", user_id="U111") + i = store.find_installation(enterprise_id="E111", team_id="T111", user_id="U111") + self.assertIsNone(i) + i = store.find_installation(enterprise_id="E111", team_id="T111") + self.assertIsNone(i) + + # delete all + store.save(installation) + store.delete_all(enterprise_id="E111", team_id="T111") + + i = store.find_installation(enterprise_id="E111", team_id="T111") + self.assertIsNone(i) + i = store.find_installation(enterprise_id="E111", team_id="T111", user_id="U111") + self.assertIsNone(i) + bot = store.find_bot(enterprise_id="E111", team_id="T222") + self.assertIsNone(bot) + + def test_issue_1441_mixing_user_and_bot_installations(self): + store = self.build_store() + + bot_installation = Installation( + app_id="A111", + enterprise_id="E111", + team_id="T111", + user_id="U111", + bot_id="B111", + bot_token="xoxb-111", + bot_scopes=["chat:write"], + bot_user_id="U222", + ) + store.save(bot_installation) + + # find bots + bot = store.find_bot(enterprise_id="E111", team_id="T111") + self.assertIsNotNone(bot) + bot = store.find_bot(enterprise_id="E111", team_id="T222") + self.assertIsNone(bot) + bot = store.find_bot(enterprise_id=None, team_id="T111") + self.assertIsNone(bot) + + installation = store.find_installation(enterprise_id="E111", team_id="T111") + self.assertIsNotNone(installation.bot_token) + installation = store.find_installation(enterprise_id="E111", team_id="T222") + self.assertIsNone(installation) + installation = store.find_installation(enterprise_id=None, team_id="T111") + self.assertIsNone(installation) + + user_installation = Installation( + app_id="A111", + enterprise_id="E111", + team_id="T111", + user_id="U111", + bot_scopes=["openid"], + user_token="xoxp-111", + ) + store.save(user_installation) + + # find bots + bot = store.find_bot(enterprise_id="E111", team_id="T111") + self.assertIsNotNone(bot.bot_token) + bot = store.find_bot(enterprise_id="E111", team_id="T222") + self.assertIsNone(bot) + bot = store.find_bot(enterprise_id=None, team_id="T111") + self.assertIsNone(bot) + + installation = store.find_installation(enterprise_id="E111", team_id="T111") + self.assertIsNotNone(installation.bot_token) + installation = store.find_installation(enterprise_id="E111", team_id="T222") + self.assertIsNone(installation) + installation = store.find_installation(enterprise_id=None, team_id="T111") + self.assertIsNone(installation) diff --git a/tests/slack_sdk/oauth/installation_store/test_async_sqlalchemy.py b/tests/slack_sdk/oauth/installation_store/test_async_sqlalchemy.py new file mode 100644 index 000000000..35aa79623 --- /dev/null +++ b/tests/slack_sdk/oauth/installation_store/test_async_sqlalchemy.py @@ -0,0 +1,298 @@ +import unittest +from tests.helpers import async_test +from sqlalchemy.ext.asyncio import AsyncEngine, create_async_engine + +from slack_sdk.oauth.installation_store import Installation +from slack_sdk.oauth.installation_store.sqlalchemy import AsyncSQLAlchemyInstallationStore + + +class TestAsyncSQLAlchemy(unittest.TestCase): + engine: AsyncEngine + + @async_test + async def setUp(self): + self.engine = create_async_engine("sqlite+aiosqlite:///:memory:") + self.store = AsyncSQLAlchemyInstallationStore(client_id="111.222", engine=self.engine) + async with self.engine.begin() as conn: + await conn.run_sync(self.store.metadata.create_all) + + @async_test + async def tearDown(self): + async with self.engine.begin() as conn: + await conn.run_sync(self.store.metadata.drop_all) + await self.engine.dispose() + + @async_test + async def test_save_and_find(self): + installation = Installation( + app_id="A111", + enterprise_id="E111", + team_id="T111", + user_id="U111", + bot_id="B111", + bot_token="xoxb-111", + bot_scopes=["chat:write"], + bot_user_id="U222", + ) + await self.store.async_save(installation) + + store = self.store + + # find bots + bot = await store.async_find_bot(enterprise_id="E111", team_id="T111") + self.assertIsNotNone(bot) + bot = await store.async_find_bot(enterprise_id="E111", team_id="T222") + self.assertIsNone(bot) + bot = await store.async_find_bot(enterprise_id=None, team_id="T111") + self.assertIsNone(bot) + + # delete bots + await store.async_delete_bot(enterprise_id="E111", team_id="T222") + bot = await store.async_find_bot(enterprise_id="E111", team_id="T222") + self.assertIsNone(bot) + + # find installations + i = await store.async_find_installation(enterprise_id="E111", team_id="T111") + self.assertIsNotNone(i) + i = await store.async_find_installation(enterprise_id="E111", team_id="T222") + self.assertIsNone(i) + i = await store.async_find_installation(enterprise_id=None, team_id="T111") + self.assertIsNone(i) + + i = await store.async_find_installation(enterprise_id="E111", team_id="T111", user_id="U111") + self.assertIsNotNone(i) + i = await store.async_find_installation(enterprise_id="E111", team_id="T111", user_id="U222") + self.assertIsNone(i) + i = await store.async_find_installation(enterprise_id="E111", team_id="T222", user_id="U111") + self.assertIsNone(i) + + # delete installations + await store.async_delete_installation(enterprise_id="E111", team_id="T111", user_id="U111") + i = await store.async_find_installation(enterprise_id="E111", team_id="T111", user_id="U111") + self.assertIsNone(i) + i = await store.async_find_installation(enterprise_id="E111", team_id="T111") + self.assertIsNone(i) + + # delete all + await store.async_save(installation) + await store.async_delete_all(enterprise_id="E111", team_id="T111") + + i = await store.async_find_installation(enterprise_id="E111", team_id="T111") + self.assertIsNone(i) + i = await store.async_find_installation(enterprise_id="E111", team_id="T111", user_id="U111") + self.assertIsNone(i) + bot = await store.async_find_bot(enterprise_id="E111", team_id="T222") + self.assertIsNone(bot) + + @async_test + async def test_org_installation(self): + installation = Installation( + app_id="AO111", + enterprise_id="EO111", + user_id="UO111", + bot_id="BO111", + bot_token="xoxb-O111", + bot_scopes=["chat:write"], + bot_user_id="UO222", + is_enterprise_install=True, + ) + await self.store.async_save(installation) + + store = self.store + + # find bots + bot = await store.async_find_bot(enterprise_id="EO111", team_id=None) + self.assertIsNotNone(bot) + bot = await store.async_find_bot(enterprise_id="EO111", team_id="TO222", is_enterprise_install=True) + self.assertIsNotNone(bot) + bot = await store.async_find_bot(enterprise_id="EO111", team_id="TO222") + self.assertIsNone(bot) + bot = await store.async_find_bot(enterprise_id=None, team_id="TO111") + self.assertIsNone(bot) + + # delete bots + await store.async_delete_bot(enterprise_id="EO111", team_id="TO222") + bot = await store.async_find_bot(enterprise_id="EO111", team_id=None) + self.assertIsNotNone(bot) + + await store.async_delete_bot(enterprise_id="EO111", team_id=None) + bot = await store.async_find_bot(enterprise_id="EO111", team_id=None) + self.assertIsNone(bot) + + # find installations + i = await store.async_find_installation(enterprise_id="EO111", team_id=None) + self.assertIsNotNone(i) + i = await store.async_find_installation(enterprise_id="EO111", team_id="T111", is_enterprise_install=True) + self.assertIsNotNone(i) + i = await store.async_find_installation(enterprise_id="EO111", team_id="T222") + self.assertIsNone(i) + i = await store.async_find_installation(enterprise_id=None, team_id="T111") + self.assertIsNone(i) + + i = await store.async_find_installation(enterprise_id="EO111", team_id=None, user_id="UO111") + self.assertIsNotNone(i) + i = await store.async_find_installation( + enterprise_id="E111", + team_id="T111", + is_enterprise_install=True, + user_id="U222", + ) + self.assertIsNone(i) + i = await store.async_find_installation(enterprise_id=None, team_id="T222", user_id="U111") + self.assertIsNone(i) + + # delete installations + await store.async_delete_installation(enterprise_id="E111", team_id=None) + i = await store.async_find_installation(enterprise_id="E111", team_id=None) + self.assertIsNone(i) + + # delete all + await store.async_save(installation) + await store.async_delete_all(enterprise_id="E111", team_id=None) + + i = await store.async_find_installation(enterprise_id="E111", team_id=None) + self.assertIsNone(i) + i = await store.async_find_installation(enterprise_id="E111", team_id=None, user_id="U111") + self.assertIsNone(i) + bot = await store.async_find_bot(enterprise_id=None, team_id="T222") + self.assertIsNone(bot) + + @async_test + async def test_save_and_find_token_rotation(self): + store = self.store + + installation = Installation( + app_id="A111", + enterprise_id="E111", + team_id="T111", + user_id="U111", + bot_id="B111", + bot_token="xoxe.xoxp-1-initial", + bot_scopes=["chat:write"], + bot_user_id="U222", + bot_refresh_token="xoxe-1-initial", + bot_token_expires_in=43200, + ) + await store.async_save(installation) + + bot = await store.async_find_bot(enterprise_id="E111", team_id="T111") + self.assertIsNotNone(bot) + self.assertEqual(bot.bot_refresh_token, "xoxe-1-initial") + + # Update the existing data + refreshed_installation = Installation( + app_id="A111", + enterprise_id="E111", + team_id="T111", + user_id="U111", + bot_id="B111", + bot_token="xoxe.xoxp-1-refreshed", + bot_scopes=["chat:write"], + bot_user_id="U222", + bot_refresh_token="xoxe-1-refreshed", + bot_token_expires_in=43200, + ) + await store.async_save(refreshed_installation) + + # find bots + bot = await store.async_find_bot(enterprise_id="E111", team_id="T111") + self.assertIsNotNone(bot) + self.assertEqual(bot.bot_refresh_token, "xoxe-1-refreshed") + bot = await store.async_find_bot(enterprise_id="E111", team_id="T222") + self.assertIsNone(bot) + bot = await store.async_find_bot(enterprise_id=None, team_id="T111") + self.assertIsNone(bot) + + # delete bots + await store.async_delete_bot(enterprise_id="E111", team_id="T222") + bot = await store.async_find_bot(enterprise_id="E111", team_id="T222") + self.assertIsNone(bot) + + # find installations + i = await store.async_find_installation(enterprise_id="E111", team_id="T111") + self.assertIsNotNone(i) + i = await store.async_find_installation(enterprise_id="E111", team_id="T222") + self.assertIsNone(i) + i = await store.async_find_installation(enterprise_id=None, team_id="T111") + self.assertIsNone(i) + + i = await store.async_find_installation(enterprise_id="E111", team_id="T111", user_id="U111") + self.assertIsNotNone(i) + i = await store.async_find_installation(enterprise_id="E111", team_id="T111", user_id="U222") + self.assertIsNone(i) + i = await store.async_find_installation(enterprise_id="E111", team_id="T222", user_id="U111") + self.assertIsNone(i) + + # delete installations + await store.async_delete_installation(enterprise_id="E111", team_id="T111", user_id="U111") + i = await store.async_find_installation(enterprise_id="E111", team_id="T111", user_id="U111") + self.assertIsNone(i) + i = await store.async_find_installation(enterprise_id="E111", team_id="T111") + self.assertIsNone(i) + + # delete all + await store.async_save(installation) + await store.async_delete_all(enterprise_id="E111", team_id="T111") + + i = await store.async_find_installation(enterprise_id="E111", team_id="T111") + self.assertIsNone(i) + i = await store.async_find_installation(enterprise_id="E111", team_id="T111", user_id="U111") + self.assertIsNone(i) + bot = await store.async_find_bot(enterprise_id="E111", team_id="T222") + self.assertIsNone(bot) + + @async_test + async def test_issue_1441_mixing_user_and_bot_installations(self): + store = self.store + + bot_installation = Installation( + app_id="A111", + enterprise_id="E111", + team_id="T111", + user_id="U111", + bot_id="B111", + bot_token="xoxb-111", + bot_scopes=["chat:write"], + bot_user_id="U222", + ) + await store.async_save(bot_installation) + + # find bots + bot = await store.async_find_bot(enterprise_id="E111", team_id="T111") + self.assertIsNotNone(bot) + bot = await store.async_find_bot(enterprise_id="E111", team_id="T222") + self.assertIsNone(bot) + bot = await store.async_find_bot(enterprise_id=None, team_id="T111") + self.assertIsNone(bot) + + installation = await store.async_find_installation(enterprise_id="E111", team_id="T111") + self.assertIsNotNone(installation.bot_token) + installation = await store.async_find_installation(enterprise_id="E111", team_id="T222") + self.assertIsNone(installation) + installation = await store.async_find_installation(enterprise_id=None, team_id="T111") + self.assertIsNone(installation) + + user_installation = Installation( + app_id="A111", + enterprise_id="E111", + team_id="T111", + user_id="U111", + bot_scopes=["openid"], + user_token="xoxp-111", + ) + await store.async_save(user_installation) + + # find bots + bot = await store.async_find_bot(enterprise_id="E111", team_id="T111") + self.assertIsNotNone(bot.bot_token) + bot = await store.async_find_bot(enterprise_id="E111", team_id="T222") + self.assertIsNone(bot) + bot = await store.async_find_bot(enterprise_id=None, team_id="T111") + self.assertIsNone(bot) + + installation = await store.async_find_installation(enterprise_id="E111", team_id="T111") + self.assertIsNotNone(installation.bot_token) + installation = await store.async_find_installation(enterprise_id="E111", team_id="T222") + self.assertIsNone(installation) + installation = await store.async_find_installation(enterprise_id=None, team_id="T111") + self.assertIsNone(installation) diff --git a/tests/slack_sdk/oauth/installation_store/test_file.py b/tests/slack_sdk/oauth/installation_store/test_file.py new file mode 100644 index 000000000..74675452a --- /dev/null +++ b/tests/slack_sdk/oauth/installation_store/test_file.py @@ -0,0 +1,205 @@ +import unittest + +from slack_sdk.oauth.installation_store import Installation, FileInstallationStore + + +class TestFile(unittest.TestCase): + def setUp(self): + pass + + def tearDown(self): + pass + + def test_instance(self): + store = FileInstallationStore(client_id="111.222") + self.assertIsNotNone(store) + + def test_save_and_find(self): + store = FileInstallationStore(client_id="111.222") + installation = Installation( + app_id="A111", + enterprise_id="E111", + team_id="T111", + user_id="U111", + bot_id="B111", + bot_token="xoxb-111", + bot_scopes=["chat:write"], + bot_user_id="U222", + ) + store.save(installation) + + # find bots + bot = store.find_bot(enterprise_id="E111", team_id="T111") + self.assertIsNotNone(bot) + bot = store.find_bot(enterprise_id="E111", team_id="T222") + self.assertIsNone(bot) + bot = store.find_bot(enterprise_id=None, team_id="T111") + self.assertIsNone(bot) + + # delete bots + store.delete_bot(enterprise_id="E111", team_id="T222") + bot = store.find_bot(enterprise_id="E111", team_id="T222") + self.assertIsNone(bot) + + # find installations + i = store.find_installation(enterprise_id="E111", team_id="T111") + self.assertIsNotNone(i) + i = store.find_installation(enterprise_id="E111", team_id="T222") + self.assertIsNone(i) + i = store.find_installation(enterprise_id=None, team_id="T111") + self.assertIsNone(i) + + i = store.find_installation(enterprise_id="E111", team_id="T111", user_id="U111") + self.assertIsNotNone(i) + i = store.find_installation(enterprise_id="E111", team_id="T111", user_id="U222") + self.assertIsNone(i) + i = store.find_installation(enterprise_id="E111", team_id="T222", user_id="U111") + self.assertIsNone(i) + + # delete installations + store.delete_installation(enterprise_id="E111", team_id="T111", user_id="U111") + i = store.find_installation(enterprise_id="E111", team_id="T111", user_id="U111") + self.assertIsNone(i) + i = store.find_installation(enterprise_id="E111", team_id="T111") + self.assertIsNotNone(i) + + store.delete_installation(enterprise_id="E111", team_id="T111") + i = store.find_installation(enterprise_id="E111", team_id="T111") + self.assertIsNone(i) + + # delete all + store.save(installation) + store.delete_all(enterprise_id="E111", team_id="T111") + + i = store.find_installation(enterprise_id="E111", team_id="T111") + self.assertIsNone(i) + i = store.find_installation(enterprise_id="E111", team_id="T111", user_id="U111") + self.assertIsNone(i) + bot = store.find_bot(enterprise_id="E111", team_id="T222") + self.assertIsNone(bot) + + def test_org_installation(self): + store = FileInstallationStore(client_id="111.222") + installation = Installation( + app_id="AO111", + enterprise_id="EO111", + user_id="UO111", + bot_id="BO111", + bot_token="xoxb-O111", + bot_scopes=["chat:write"], + bot_user_id="UO222", + is_enterprise_install=True, + ) + store.save(installation) + + # find bots + bot = store.find_bot(enterprise_id="EO111", team_id=None) + self.assertIsNotNone(bot) + bot = store.find_bot(enterprise_id="EO111", team_id="TO222", is_enterprise_install=True) + self.assertIsNotNone(bot) + bot = store.find_bot(enterprise_id="EO111", team_id="TO222") + self.assertIsNone(bot) + bot = store.find_bot(enterprise_id=None, team_id="TO111") + self.assertIsNone(bot) + + # delete bots + store.delete_bot(enterprise_id="EO111", team_id="TO222") + bot = store.find_bot(enterprise_id="EO111", team_id=None) + self.assertIsNotNone(bot) + + store.delete_bot(enterprise_id="EO111", team_id=None) + bot = store.find_bot(enterprise_id="EO111", team_id=None) + self.assertIsNone(bot) + + # find installations + i = store.find_installation(enterprise_id="EO111", team_id=None) + self.assertIsNotNone(i) + i = store.find_installation(enterprise_id="EO111", team_id="T111", is_enterprise_install=True) + self.assertIsNotNone(i) + i = store.find_installation(enterprise_id="EO111", team_id="T222") + self.assertIsNone(i) + i = store.find_installation(enterprise_id=None, team_id="T111") + self.assertIsNone(i) + + i = store.find_installation(enterprise_id="EO111", team_id=None, user_id="UO111") + self.assertIsNotNone(i) + i = store.find_installation( + enterprise_id="E111", + team_id="T111", + is_enterprise_install=True, + user_id="U222", + ) + self.assertIsNone(i) + i = store.find_installation(enterprise_id=None, team_id="T222", user_id="U111") + self.assertIsNone(i) + + # delete installations + store.delete_installation(enterprise_id="E111", team_id=None) + i = store.find_installation(enterprise_id="E111", team_id=None) + self.assertIsNone(i) + + # delete all + store.save(installation) + store.delete_all(enterprise_id="E111", team_id=None) + + i = store.find_installation(enterprise_id="E111", team_id=None) + self.assertIsNone(i) + i = store.find_installation(enterprise_id="E111", team_id=None, user_id="U111") + self.assertIsNone(i) + bot = store.find_bot(enterprise_id=None, team_id="T222") + self.assertIsNone(bot) + + def test_issue_1441_mixing_user_and_bot_installations(self): + store = FileInstallationStore(client_id="111.222") + + bot_installation = Installation( + app_id="A111", + enterprise_id="E111", + team_id="T111", + user_id="U111", + bot_id="B111", + bot_token="xoxb-111", + bot_scopes=["chat:write"], + bot_user_id="U222", + ) + store.save(bot_installation) + + # find bots + bot = store.find_bot(enterprise_id="E111", team_id="T111") + self.assertIsNotNone(bot) + bot = store.find_bot(enterprise_id="E111", team_id="T222") + self.assertIsNone(bot) + bot = store.find_bot(enterprise_id=None, team_id="T111") + self.assertIsNone(bot) + + installation = store.find_installation(enterprise_id="E111", team_id="T111") + self.assertIsNotNone(installation.bot_token) + installation = store.find_installation(enterprise_id="E111", team_id="T222") + self.assertIsNone(installation) + installation = store.find_installation(enterprise_id=None, team_id="T111") + self.assertIsNone(installation) + + user_installation = Installation( + app_id="A111", + enterprise_id="E111", + team_id="T111", + user_id="U111", + bot_scopes=["openid"], + user_token="xoxp-111", + ) + store.save(user_installation) + + # find bots + bot = store.find_bot(enterprise_id="E111", team_id="T111") + self.assertIsNotNone(bot.bot_token) + bot = store.find_bot(enterprise_id="E111", team_id="T222") + self.assertIsNone(bot) + bot = store.find_bot(enterprise_id=None, team_id="T111") + self.assertIsNone(bot) + + installation = store.find_installation(enterprise_id="E111", team_id="T111") + self.assertIsNotNone(installation.bot_token) + installation = store.find_installation(enterprise_id="E111", team_id="T222") + self.assertIsNone(installation) + installation = store.find_installation(enterprise_id=None, team_id="T111") + self.assertIsNone(installation) diff --git a/tests/slack_sdk/oauth/installation_store/test_interface.py b/tests/slack_sdk/oauth/installation_store/test_interface.py new file mode 100644 index 000000000..554f849d0 --- /dev/null +++ b/tests/slack_sdk/oauth/installation_store/test_interface.py @@ -0,0 +1,22 @@ +import unittest + +from slack_sdk.oauth.installation_store import InstallationStore +from slack_sdk.oauth.installation_store.async_installation_store import ( + AsyncInstallationStore, +) + + +class TestInterface(unittest.TestCase): + def setUp(self): + pass + + def tearDown(self): + pass + + def test_sync(self): + store = InstallationStore() + self.assertIsNotNone(store) + + def test_async(self): + store = AsyncInstallationStore() + self.assertIsNotNone(store) diff --git a/tests/slack_sdk/oauth/installation_store/test_internals.py b/tests/slack_sdk/oauth/installation_store/test_internals.py new file mode 100644 index 000000000..74cb935fc --- /dev/null +++ b/tests/slack_sdk/oauth/installation_store/test_internals.py @@ -0,0 +1,49 @@ +import sys +import unittest +from datetime import datetime, timezone + +import pytest + +from slack_sdk.oauth.installation_store import Installation, FileInstallationStore +from slack_sdk.oauth.installation_store.internals import _from_iso_format_to_datetime, _timestamp_to_type + + +class TestFile(unittest.TestCase): + def setUp(self): + pass + + def tearDown(self): + pass + + def test_iso_format(self): + dt = _from_iso_format_to_datetime("2021-07-14 08:00:17") + self.assertEqual(dt.timestamp(), 1626249617.0) + + +@pytest.mark.parametrize( + "ts,target_type,expected_result", + [ + (1701209097, int, 1701209097), + (datetime(2023, 11, 28, 22, 9, 7, tzinfo=timezone.utc), int, 1701209347), + ("1701209605", int, 1701209605), + ("2023-11-28 22:11:19", int, 1701209479), + (1701209998.3429494, float, 1701209998.3429494), + (datetime(2023, 11, 28, 22, 20, 25, 262571, tzinfo=timezone.utc), float, 1701210025.262571), + ("1701210054.4672053", float, 1701210054.4672053), + ("2023-11-28 22:21:14.745556", float, 1701210074.745556), + ], +) +def test_timestamp_to_type(ts, target_type, expected_result): + result = _timestamp_to_type(ts, target_type) + assert result == expected_result + + +def test_timestamp_to_type_invalid_str(): + match = "Invalid isoformat string" + with pytest.raises(ValueError, match=match): + _timestamp_to_type("not-a-timestamp", int) + + +def test_timestamp_to_type_unsupported_format(): + with pytest.raises(ValueError, match="Unsupported data format"): + _timestamp_to_type({}, int) diff --git a/tests/slack_sdk/oauth/installation_store/test_models.py b/tests/slack_sdk/oauth/installation_store/test_models.py new file mode 100644 index 000000000..43b1ec7b2 --- /dev/null +++ b/tests/slack_sdk/oauth/installation_store/test_models.py @@ -0,0 +1,129 @@ +import time +from datetime import datetime, timezone +import unittest + +from slack_sdk.oauth.installation_store import Installation, FileInstallationStore, Bot + + +class TestModels(unittest.TestCase): + def setUp(self): + pass + + def tearDown(self): + pass + + def test_bot(self): + bot = Bot( + bot_token="xoxb-", + bot_id="B111", + bot_user_id="U111", + installed_at=time.time(), + ) + self.assertIsNotNone(bot) + self.assertIsNotNone(bot.to_dict()) + self.assertIsNotNone(bot.to_dict_for_copying()) + + def test_bot_custom_fields(self): + bot = Bot( + bot_token="xoxb-", + bot_id="B111", + bot_user_id="U111", + installed_at=time.time(), + ) + bot.set_custom_value("service_user_id", "XYZ123") + # the same names in custom_values are ignored + bot.set_custom_value("app_id", "A222") + self.assertEqual(bot.get_custom_value("service_user_id"), "XYZ123") + self.assertEqual(bot.to_dict().get("service_user_id"), "XYZ123") + self.assertEqual(bot.to_dict_for_copying().get("custom_values").get("service_user_id"), "XYZ123") + + def test_bot_datetime_manipulation(self): + expected_timestamp = datetime.now(tz=timezone.utc) + bot = Bot( + bot_token="xoxb-", + bot_id="B111", + bot_user_id="U111", + bot_token_expires_at=expected_timestamp, + installed_at=expected_timestamp, + ) + bot_dict = bot.to_dict() + self.assertIsNotNone(bot_dict) + self.assertEqual( + bot_dict.get("bot_token_expires_at").isoformat(), expected_timestamp.strftime("%Y-%m-%dT%H:%M:%S+00:00") + ) + self.assertEqual(bot_dict.get("installed_at"), expected_timestamp) + + def test_installation(self): + installation = Installation( + app_id="A111", + enterprise_id="E111", + team_id="T111", + user_id="U111", + bot_id="B111", + bot_token="xoxb-111", + bot_scopes=["chat:write"], + bot_user_id="U222", + ) + self.assertIsNotNone(installation) + self.assertEqual(installation.app_id, "A111") + + self.assertIsNotNone(installation.to_bot()) + self.assertIsNotNone(installation.to_bot().app_id, "A111") + + self.assertIsNotNone(installation.to_dict()) + self.assertEqual(installation.to_dict().get("app_id"), "A111") + + def test_installation_custom_fields(self): + installation = Installation( + app_id="A111", + enterprise_id="E111", + team_id="T111", + user_id="U111", + bot_id="B111", + bot_token="xoxb-111", + bot_scopes=["chat:write"], + bot_user_id="U222", + ) + self.assertIsNotNone(installation) + + installation.set_custom_value("service_user_id", "XYZ123") + # the same names in custom_values are ignored + installation.set_custom_value("app_id", "A222") + self.assertEqual(installation.get_custom_value("service_user_id"), "XYZ123") + self.assertEqual(installation.to_dict().get("service_user_id"), "XYZ123") + self.assertEqual(installation.to_dict().get("app_id"), "A111") + self.assertEqual(installation.to_dict_for_copying().get("custom_values").get("app_id"), "A222") + + bot = installation.to_bot() + self.assertEqual(bot.app_id, "A111") + self.assertEqual(bot.get_custom_value("service_user_id"), "XYZ123") + + self.assertEqual(bot.to_dict().get("app_id"), "A111") + self.assertEqual(bot.to_dict().get("service_user_id"), "XYZ123") + self.assertEqual(bot.to_dict_for_copying().get("custom_values").get("app_id"), "A222") + + def test_installation_datetime_manipulation(self): + expected_timestamp = datetime.now(tz=timezone.utc) + installation = Installation( + app_id="A111", + enterprise_id="E111", + team_id="T111", + user_id="U111", + bot_id="B111", + bot_token="xoxb-111", + bot_scopes=["chat:write"], + bot_user_id="U222", + bot_token_expires_at=expected_timestamp, + user_token_expires_at=expected_timestamp, + installed_at=expected_timestamp, + ) + installation_dict = installation.to_dict() + self.assertIsNotNone(installation_dict) + self.assertEqual( + installation_dict.get("bot_token_expires_at").isoformat(), expected_timestamp.strftime("%Y-%m-%dT%H:%M:%S+00:00") + ) + self.assertEqual( + installation_dict.get("user_token_expires_at").isoformat(), + expected_timestamp.strftime("%Y-%m-%dT%H:%M:%S+00:00"), + ) + self.assertEqual(installation_dict.get("installed_at"), expected_timestamp) diff --git a/tests/slack_sdk/oauth/installation_store/test_simple_cache.py b/tests/slack_sdk/oauth/installation_store/test_simple_cache.py new file mode 100644 index 000000000..2af633430 --- /dev/null +++ b/tests/slack_sdk/oauth/installation_store/test_simple_cache.py @@ -0,0 +1,137 @@ +import os +import unittest + +from slack_sdk.oauth.installation_store import Installation +from slack_sdk.oauth.installation_store.cacheable_installation_store import ( + CacheableInstallationStore, +) +from slack_sdk.oauth.installation_store.sqlite3 import SQLite3InstallationStore + + +class TestCacheable(unittest.TestCase): + def test_save_and_find(self): + sqlite3_store = SQLite3InstallationStore(database="logs/cacheable.db", client_id="111.222") + sqlite3_store.init() + store = CacheableInstallationStore(sqlite3_store) + + installation = Installation( + app_id="A111", + enterprise_id="E111", + team_id="T111", + user_id="U111", + bot_id="B111", + bot_token="xoxb-111", + bot_scopes=["chat:write"], + bot_user_id="U222", + ) + store.save(installation) + + bot = store.find_bot(enterprise_id="E111", team_id="T111") + self.assertIsNotNone(bot) + + os.remove("logs/cacheable.db") + + bot = sqlite3_store.find_bot(enterprise_id="E111", team_id="T111") + self.assertIsNone(bot) + bot = store.find_bot(enterprise_id="E111", team_id="T111") + self.assertIsNotNone(bot) + + # delete and find + sqlite3_store = SQLite3InstallationStore(database="logs/cacheable.db", client_id="111.222") + sqlite3_store.init() + store = CacheableInstallationStore(sqlite3_store) + + store.save(installation) + bot = store.find_bot(enterprise_id="E111", team_id="T111") + self.assertIsNotNone(bot) + + os.remove("logs/cacheable.db") + bot = sqlite3_store.find_bot(enterprise_id="E111", team_id="T111") + self.assertIsNone(bot) + bot = store.find_bot(enterprise_id="E111", team_id="T111") + self.assertIsNotNone(bot) + + def test_save_and_find_token_rotation(self): + sqlite3_store = SQLite3InstallationStore(database="logs/cacheable.db", client_id="111.222") + sqlite3_store.init() + store = CacheableInstallationStore(sqlite3_store) + + installation = Installation( + app_id="A111", + enterprise_id="E111", + team_id="T111", + user_id="U111", + bot_id="B111", + bot_token="xoxe.xoxp-1-initial", + bot_scopes=["chat:write"], + bot_user_id="U222", + bot_refresh_token="xoxe-1-initial", + bot_token_expires_in=43200, + ) + store.save(installation) + + bot = store.find_bot(enterprise_id="E111", team_id="T111") + self.assertIsNotNone(bot) + self.assertEqual(bot.bot_refresh_token, "xoxe-1-initial") + + # Update the existing data + refreshed_installation = Installation( + app_id="A111", + enterprise_id="E111", + team_id="T111", + user_id="U111", + bot_id="B111", + bot_token="xoxe.xoxp-1-refreshed", + bot_scopes=["chat:write"], + bot_user_id="U222", + bot_refresh_token="xoxe-1-refreshed", + bot_token_expires_in=43200, + ) + store.save(refreshed_installation) + + # find bots + bot = store.find_bot(enterprise_id="E111", team_id="T111") + self.assertIsNotNone(bot) + self.assertEqual(bot.bot_refresh_token, "xoxe-1-refreshed") + bot = store.find_bot(enterprise_id="E111", team_id="T222") + self.assertIsNone(bot) + bot = store.find_bot(enterprise_id=None, team_id="T111") + self.assertIsNone(bot) + + # delete bots + store.delete_bot(enterprise_id="E111", team_id="T222") + bot = store.find_bot(enterprise_id="E111", team_id="T222") + self.assertIsNone(bot) + + # find installations + i = store.find_installation(enterprise_id="E111", team_id="T111") + self.assertIsNotNone(i) + i = store.find_installation(enterprise_id="E111", team_id="T222") + self.assertIsNone(i) + i = store.find_installation(enterprise_id=None, team_id="T111") + self.assertIsNone(i) + + i = store.find_installation(enterprise_id="E111", team_id="T111", user_id="U111") + self.assertIsNotNone(i) + i = store.find_installation(enterprise_id="E111", team_id="T111", user_id="U222") + self.assertIsNone(i) + i = store.find_installation(enterprise_id="E111", team_id="T222", user_id="U111") + self.assertIsNone(i) + + # delete installations + store.delete_installation(enterprise_id="E111", team_id="T111", user_id="U111") + i = store.find_installation(enterprise_id="E111", team_id="T111", user_id="U111") + self.assertIsNone(i) + i = store.find_installation(enterprise_id="E111", team_id="T111") + self.assertIsNone(i) + + # delete all + store.save(installation) + store.delete_all(enterprise_id="E111", team_id="T111") + + i = store.find_installation(enterprise_id="E111", team_id="T111") + self.assertIsNone(i) + i = store.find_installation(enterprise_id="E111", team_id="T111", user_id="U111") + self.assertIsNone(i) + bot = store.find_bot(enterprise_id="E111", team_id="T222") + self.assertIsNone(bot) diff --git a/tests/slack_sdk/oauth/installation_store/test_sqlalchemy.py b/tests/slack_sdk/oauth/installation_store/test_sqlalchemy.py new file mode 100644 index 000000000..4d827f70b --- /dev/null +++ b/tests/slack_sdk/oauth/installation_store/test_sqlalchemy.py @@ -0,0 +1,291 @@ +import unittest + +import sqlalchemy +from sqlalchemy.engine import Engine + +from slack_sdk.oauth.installation_store import Installation +from slack_sdk.oauth.installation_store.sqlalchemy import SQLAlchemyInstallationStore + + +class TestSQLAlchemy(unittest.TestCase): + engine: Engine + + def setUp(self): + self.engine = sqlalchemy.create_engine("sqlite:///:memory:") + self.store = SQLAlchemyInstallationStore(client_id="111.222", engine=self.engine) + self.store.metadata.create_all(self.engine) + + def tearDown(self): + self.store.metadata.drop_all(self.engine) + self.engine.dispose() + + def test_save_and_find(self): + installation = Installation( + app_id="A111", + enterprise_id="E111", + team_id="T111", + user_id="U111", + bot_id="B111", + bot_token="xoxb-111", + bot_scopes=["chat:write"], + bot_user_id="U222", + ) + self.store.save(installation) + + store = self.store + + # find bots + bot = store.find_bot(enterprise_id="E111", team_id="T111") + self.assertIsNotNone(bot) + bot = store.find_bot(enterprise_id="E111", team_id="T222") + self.assertIsNone(bot) + bot = store.find_bot(enterprise_id=None, team_id="T111") + self.assertIsNone(bot) + + # delete bots + store.delete_bot(enterprise_id="E111", team_id="T222") + bot = store.find_bot(enterprise_id="E111", team_id="T222") + self.assertIsNone(bot) + + # find installations + i = store.find_installation(enterprise_id="E111", team_id="T111") + self.assertIsNotNone(i) + i = store.find_installation(enterprise_id="E111", team_id="T222") + self.assertIsNone(i) + i = store.find_installation(enterprise_id=None, team_id="T111") + self.assertIsNone(i) + + i = store.find_installation(enterprise_id="E111", team_id="T111", user_id="U111") + self.assertIsNotNone(i) + i = store.find_installation(enterprise_id="E111", team_id="T111", user_id="U222") + self.assertIsNone(i) + i = store.find_installation(enterprise_id="E111", team_id="T222", user_id="U111") + self.assertIsNone(i) + + # delete installations + store.delete_installation(enterprise_id="E111", team_id="T111", user_id="U111") + i = store.find_installation(enterprise_id="E111", team_id="T111", user_id="U111") + self.assertIsNone(i) + i = store.find_installation(enterprise_id="E111", team_id="T111") + self.assertIsNone(i) + + # delete all + store.save(installation) + store.delete_all(enterprise_id="E111", team_id="T111") + + i = store.find_installation(enterprise_id="E111", team_id="T111") + self.assertIsNone(i) + i = store.find_installation(enterprise_id="E111", team_id="T111", user_id="U111") + self.assertIsNone(i) + bot = store.find_bot(enterprise_id="E111", team_id="T222") + self.assertIsNone(bot) + + def test_org_installation(self): + installation = Installation( + app_id="AO111", + enterprise_id="EO111", + user_id="UO111", + bot_id="BO111", + bot_token="xoxb-O111", + bot_scopes=["chat:write"], + bot_user_id="UO222", + is_enterprise_install=True, + ) + self.store.save(installation) + + store = self.store + + # find bots + bot = store.find_bot(enterprise_id="EO111", team_id=None) + self.assertIsNotNone(bot) + bot = store.find_bot(enterprise_id="EO111", team_id="TO222", is_enterprise_install=True) + self.assertIsNotNone(bot) + bot = store.find_bot(enterprise_id="EO111", team_id="TO222") + self.assertIsNone(bot) + bot = store.find_bot(enterprise_id=None, team_id="TO111") + self.assertIsNone(bot) + + # delete bots + store.delete_bot(enterprise_id="EO111", team_id="TO222") + bot = store.find_bot(enterprise_id="EO111", team_id=None) + self.assertIsNotNone(bot) + + store.delete_bot(enterprise_id="EO111", team_id=None) + bot = store.find_bot(enterprise_id="EO111", team_id=None) + self.assertIsNone(bot) + + # find installations + i = store.find_installation(enterprise_id="EO111", team_id=None) + self.assertIsNotNone(i) + i = store.find_installation(enterprise_id="EO111", team_id="T111", is_enterprise_install=True) + self.assertIsNotNone(i) + i = store.find_installation(enterprise_id="EO111", team_id="T222") + self.assertIsNone(i) + i = store.find_installation(enterprise_id=None, team_id="T111") + self.assertIsNone(i) + + i = store.find_installation(enterprise_id="EO111", team_id=None, user_id="UO111") + self.assertIsNotNone(i) + i = store.find_installation( + enterprise_id="E111", + team_id="T111", + is_enterprise_install=True, + user_id="U222", + ) + self.assertIsNone(i) + i = store.find_installation(enterprise_id=None, team_id="T222", user_id="U111") + self.assertIsNone(i) + + # delete installations + store.delete_installation(enterprise_id="E111", team_id=None) + i = store.find_installation(enterprise_id="E111", team_id=None) + self.assertIsNone(i) + + # delete all + store.save(installation) + store.delete_all(enterprise_id="E111", team_id=None) + + i = store.find_installation(enterprise_id="E111", team_id=None) + self.assertIsNone(i) + i = store.find_installation(enterprise_id="E111", team_id=None, user_id="U111") + self.assertIsNone(i) + bot = store.find_bot(enterprise_id=None, team_id="T222") + self.assertIsNone(bot) + + def test_save_and_find_token_rotation(self): + store = self.store + + installation = Installation( + app_id="A111", + enterprise_id="E111", + team_id="T111", + user_id="U111", + bot_id="B111", + bot_token="xoxe.xoxp-1-initial", + bot_scopes=["chat:write"], + bot_user_id="U222", + bot_refresh_token="xoxe-1-initial", + bot_token_expires_in=43200, + ) + store.save(installation) + + bot = store.find_bot(enterprise_id="E111", team_id="T111") + self.assertIsNotNone(bot) + self.assertEqual(bot.bot_refresh_token, "xoxe-1-initial") + + # Update the existing data + refreshed_installation = Installation( + app_id="A111", + enterprise_id="E111", + team_id="T111", + user_id="U111", + bot_id="B111", + bot_token="xoxe.xoxp-1-refreshed", + bot_scopes=["chat:write"], + bot_user_id="U222", + bot_refresh_token="xoxe-1-refreshed", + bot_token_expires_in=43200, + ) + store.save(refreshed_installation) + + # find bots + bot = store.find_bot(enterprise_id="E111", team_id="T111") + self.assertIsNotNone(bot) + self.assertEqual(bot.bot_refresh_token, "xoxe-1-refreshed") + bot = store.find_bot(enterprise_id="E111", team_id="T222") + self.assertIsNone(bot) + bot = store.find_bot(enterprise_id=None, team_id="T111") + self.assertIsNone(bot) + + # delete bots + store.delete_bot(enterprise_id="E111", team_id="T222") + bot = store.find_bot(enterprise_id="E111", team_id="T222") + self.assertIsNone(bot) + + # find installations + i = store.find_installation(enterprise_id="E111", team_id="T111") + self.assertIsNotNone(i) + i = store.find_installation(enterprise_id="E111", team_id="T222") + self.assertIsNone(i) + i = store.find_installation(enterprise_id=None, team_id="T111") + self.assertIsNone(i) + + i = store.find_installation(enterprise_id="E111", team_id="T111", user_id="U111") + self.assertIsNotNone(i) + i = store.find_installation(enterprise_id="E111", team_id="T111", user_id="U222") + self.assertIsNone(i) + i = store.find_installation(enterprise_id="E111", team_id="T222", user_id="U111") + self.assertIsNone(i) + + # delete installations + store.delete_installation(enterprise_id="E111", team_id="T111", user_id="U111") + i = store.find_installation(enterprise_id="E111", team_id="T111", user_id="U111") + self.assertIsNone(i) + i = store.find_installation(enterprise_id="E111", team_id="T111") + self.assertIsNone(i) + + # delete all + store.save(installation) + store.delete_all(enterprise_id="E111", team_id="T111") + + i = store.find_installation(enterprise_id="E111", team_id="T111") + self.assertIsNone(i) + i = store.find_installation(enterprise_id="E111", team_id="T111", user_id="U111") + self.assertIsNone(i) + bot = store.find_bot(enterprise_id="E111", team_id="T222") + self.assertIsNone(bot) + + def test_issue_1441_mixing_user_and_bot_installations(self): + store = self.store + + bot_installation = Installation( + app_id="A111", + enterprise_id="E111", + team_id="T111", + user_id="U111", + bot_id="B111", + bot_token="xoxb-111", + bot_scopes=["chat:write"], + bot_user_id="U222", + ) + store.save(bot_installation) + + # find bots + bot = store.find_bot(enterprise_id="E111", team_id="T111") + self.assertIsNotNone(bot) + bot = store.find_bot(enterprise_id="E111", team_id="T222") + self.assertIsNone(bot) + bot = store.find_bot(enterprise_id=None, team_id="T111") + self.assertIsNone(bot) + + installation = store.find_installation(enterprise_id="E111", team_id="T111") + self.assertIsNotNone(installation.bot_token) + installation = store.find_installation(enterprise_id="E111", team_id="T222") + self.assertIsNone(installation) + installation = store.find_installation(enterprise_id=None, team_id="T111") + self.assertIsNone(installation) + + user_installation = Installation( + app_id="A111", + enterprise_id="E111", + team_id="T111", + user_id="U111", + bot_scopes=["openid"], + user_token="xoxp-111", + ) + store.save(user_installation) + + # find bots + bot = store.find_bot(enterprise_id="E111", team_id="T111") + self.assertIsNotNone(bot.bot_token) + bot = store.find_bot(enterprise_id="E111", team_id="T222") + self.assertIsNone(bot) + bot = store.find_bot(enterprise_id=None, team_id="T111") + self.assertIsNone(bot) + + installation = store.find_installation(enterprise_id="E111", team_id="T111") + self.assertIsNotNone(installation.bot_token) + installation = store.find_installation(enterprise_id="E111", team_id="T222") + self.assertIsNone(installation) + installation = store.find_installation(enterprise_id=None, team_id="T111") + self.assertIsNone(installation) diff --git a/tests/slack_sdk/oauth/installation_store/test_sqlite3.py b/tests/slack_sdk/oauth/installation_store/test_sqlite3.py new file mode 100644 index 000000000..e616c20bc --- /dev/null +++ b/tests/slack_sdk/oauth/installation_store/test_sqlite3.py @@ -0,0 +1,292 @@ +import unittest + +from slack_sdk.oauth.installation_store import Installation +from slack_sdk.oauth.installation_store.sqlite3 import SQLite3InstallationStore + + +class TestSQLite3(unittest.TestCase): + def setUp(self): + pass + + def tearDown(self): + pass + + def test_instance(self): + store = SQLite3InstallationStore(database="logs/test.db", client_id="111.222") + self.assertIsNotNone(store) + conn = store.connect() + self.assertIsNotNone(conn) + conn.close() + + def test_init(self): + store = SQLite3InstallationStore(database="logs/test.db", client_id="111.222") + store.init() + + def test_save_and_find(self): + store = SQLite3InstallationStore(database="logs/test.db", client_id="111.222") + installation = Installation( + app_id="A111", + enterprise_id="E111", + team_id="T111", + user_id="U111", + bot_id="B111", + bot_token="xoxb-111", + bot_scopes=["chat:write"], + bot_user_id="U222", + ) + store.save(installation) + + # find bots + bot = store.find_bot(enterprise_id="E111", team_id="T111") + self.assertIsNotNone(bot) + bot = store.find_bot(enterprise_id="E111", team_id="T222") + self.assertIsNone(bot) + bot = store.find_bot(enterprise_id=None, team_id="T111") + self.assertIsNone(bot) + + # delete bots + store.delete_bot(enterprise_id="E111", team_id="T222") + bot = store.find_bot(enterprise_id="E111", team_id="T222") + self.assertIsNone(bot) + + # find installations + i = store.find_installation(enterprise_id="E111", team_id="T111") + self.assertIsNotNone(i) + i = store.find_installation(enterprise_id="E111", team_id="T222") + self.assertIsNone(i) + i = store.find_installation(enterprise_id=None, team_id="T111") + self.assertIsNone(i) + + i = store.find_installation(enterprise_id="E111", team_id="T111", user_id="U111") + self.assertIsNotNone(i) + i = store.find_installation(enterprise_id="E111", team_id="T111", user_id="U222") + self.assertIsNone(i) + i = store.find_installation(enterprise_id="E111", team_id="T222", user_id="U111") + self.assertIsNone(i) + + # delete installations + store.delete_installation(enterprise_id="E111", team_id="T111", user_id="U111") + i = store.find_installation(enterprise_id="E111", team_id="T111", user_id="U111") + self.assertIsNone(i) + i = store.find_installation(enterprise_id="E111", team_id="T111") + self.assertIsNone(i) + + # delete all + store.save(installation) + store.delete_all(enterprise_id="E111", team_id="T111") + + i = store.find_installation(enterprise_id="E111", team_id="T111") + self.assertIsNone(i) + i = store.find_installation(enterprise_id="E111", team_id="T111", user_id="U111") + self.assertIsNone(i) + bot = store.find_bot(enterprise_id="E111", team_id="T222") + self.assertIsNone(bot) + + def test_org_installation(self): + store = SQLite3InstallationStore(database="logs/test.db", client_id="111.222") + installation = Installation( + app_id="AO111", + enterprise_id="EO111", + user_id="UO111", + bot_id="BO111", + bot_token="xoxb-O111", + bot_scopes=["chat:write"], + bot_user_id="UO222", + is_enterprise_install=True, + ) + store.save(installation) + + # find bots + bot = store.find_bot(enterprise_id="EO111", team_id=None) + self.assertIsNotNone(bot) + bot = store.find_bot(enterprise_id="EO111", team_id="TO222", is_enterprise_install=True) + self.assertIsNotNone(bot) + bot = store.find_bot(enterprise_id="EO111", team_id="TO222") + self.assertIsNone(bot) + bot = store.find_bot(enterprise_id=None, team_id="TO111") + self.assertIsNone(bot) + + # delete bots + store.delete_bot(enterprise_id="EO111", team_id="TO222") + bot = store.find_bot(enterprise_id="EO111", team_id=None) + self.assertIsNotNone(bot) + + store.delete_bot(enterprise_id="EO111", team_id=None) + bot = store.find_bot(enterprise_id="EO111", team_id=None) + self.assertIsNone(bot) + + # find installations + i = store.find_installation(enterprise_id="EO111", team_id=None) + self.assertIsNotNone(i) + i = store.find_installation(enterprise_id="EO111", team_id="T111", is_enterprise_install=True) + self.assertIsNotNone(i) + i = store.find_installation(enterprise_id="EO111", team_id="T222") + self.assertIsNone(i) + i = store.find_installation(enterprise_id=None, team_id="T111") + self.assertIsNone(i) + + i = store.find_installation(enterprise_id="EO111", team_id=None, user_id="UO111") + self.assertIsNotNone(i) + i = store.find_installation( + enterprise_id="E111", + team_id="T111", + is_enterprise_install=True, + user_id="U222", + ) + self.assertIsNone(i) + i = store.find_installation(enterprise_id=None, team_id="T222", user_id="U111") + self.assertIsNone(i) + + # delete installations + store.delete_installation(enterprise_id="E111", team_id=None) + i = store.find_installation(enterprise_id="E111", team_id=None) + self.assertIsNone(i) + + # delete all + store.save(installation) + store.delete_all(enterprise_id="E111", team_id=None) + + i = store.find_installation(enterprise_id="E111", team_id=None) + self.assertIsNone(i) + i = store.find_installation(enterprise_id="E111", team_id=None, user_id="U111") + self.assertIsNone(i) + bot = store.find_bot(enterprise_id=None, team_id="T222") + self.assertIsNone(bot) + + def test_save_and_find_token_rotation(self): + store = SQLite3InstallationStore(database="logs/test.db", client_id="111.222") + + installation = Installation( + app_id="A111", + enterprise_id="E111", + team_id="T111", + user_id="U111", + bot_id="B111", + bot_token="xoxe.xoxp-1-initial", + bot_scopes=["chat:write"], + bot_user_id="U222", + bot_refresh_token="xoxe-1-initial", + bot_token_expires_in=43200, + ) + store.save(installation) + + bot = store.find_bot(enterprise_id="E111", team_id="T111") + self.assertIsNotNone(bot) + self.assertEqual(bot.bot_refresh_token, "xoxe-1-initial") + + # Update the existing data + refreshed_installation = Installation( + app_id="A111", + enterprise_id="E111", + team_id="T111", + user_id="U111", + bot_id="B111", + bot_token="xoxe.xoxp-1-refreshed", + bot_scopes=["chat:write"], + bot_user_id="U222", + bot_refresh_token="xoxe-1-refreshed", + bot_token_expires_in=43200, + ) + store.save(refreshed_installation) + + # find bots + bot = store.find_bot(enterprise_id="E111", team_id="T111") + self.assertIsNotNone(bot) + self.assertEqual(bot.bot_refresh_token, "xoxe-1-refreshed") + bot = store.find_bot(enterprise_id="E111", team_id="T222") + self.assertIsNone(bot) + bot = store.find_bot(enterprise_id=None, team_id="T111") + self.assertIsNone(bot) + + # delete bots + store.delete_bot(enterprise_id="E111", team_id="T222") + bot = store.find_bot(enterprise_id="E111", team_id="T222") + self.assertIsNone(bot) + + # find installations + i = store.find_installation(enterprise_id="E111", team_id="T111") + self.assertIsNotNone(i) + i = store.find_installation(enterprise_id="E111", team_id="T222") + self.assertIsNone(i) + i = store.find_installation(enterprise_id=None, team_id="T111") + self.assertIsNone(i) + + i = store.find_installation(enterprise_id="E111", team_id="T111", user_id="U111") + self.assertIsNotNone(i) + i = store.find_installation(enterprise_id="E111", team_id="T111", user_id="U222") + self.assertIsNone(i) + i = store.find_installation(enterprise_id="E111", team_id="T222", user_id="U111") + self.assertIsNone(i) + + # delete installations + store.delete_installation(enterprise_id="E111", team_id="T111", user_id="U111") + i = store.find_installation(enterprise_id="E111", team_id="T111", user_id="U111") + self.assertIsNone(i) + i = store.find_installation(enterprise_id="E111", team_id="T111") + self.assertIsNone(i) + + # delete all + store.save(installation) + store.delete_all(enterprise_id="E111", team_id="T111") + + i = store.find_installation(enterprise_id="E111", team_id="T111") + self.assertIsNone(i) + i = store.find_installation(enterprise_id="E111", team_id="T111", user_id="U111") + self.assertIsNone(i) + bot = store.find_bot(enterprise_id="E111", team_id="T222") + self.assertIsNone(bot) + + def test_issue_1441_mixing_user_and_bot_installations(self): + store = SQLite3InstallationStore(database="logs/test.db", client_id="111.222") + + bot_installation = Installation( + app_id="A111", + enterprise_id="E111", + team_id="T111", + user_id="U111", + bot_id="B111", + bot_token="xoxb-111", + bot_scopes=["chat:write"], + bot_user_id="U222", + ) + store.save(bot_installation) + + # find bots + bot = store.find_bot(enterprise_id="E111", team_id="T111") + self.assertIsNotNone(bot) + bot = store.find_bot(enterprise_id="E111", team_id="T222") + self.assertIsNone(bot) + bot = store.find_bot(enterprise_id=None, team_id="T111") + self.assertIsNone(bot) + + installation = store.find_installation(enterprise_id="E111", team_id="T111") + self.assertIsNotNone(installation.bot_token) + installation = store.find_installation(enterprise_id="E111", team_id="T222") + self.assertIsNone(installation) + installation = store.find_installation(enterprise_id=None, team_id="T111") + self.assertIsNone(installation) + + user_installation = Installation( + app_id="A111", + enterprise_id="E111", + team_id="T111", + user_id="U111", + bot_scopes=["openid"], + user_token="xoxp-111", + ) + store.save(user_installation) + + # find bots + bot = store.find_bot(enterprise_id="E111", team_id="T111") + self.assertIsNotNone(bot.bot_token) + bot = store.find_bot(enterprise_id="E111", team_id="T222") + self.assertIsNone(bot) + bot = store.find_bot(enterprise_id=None, team_id="T111") + self.assertIsNone(bot) + + installation = store.find_installation(enterprise_id="E111", team_id="T111") + self.assertIsNotNone(installation.bot_token) + installation = store.find_installation(enterprise_id="E111", team_id="T222") + self.assertIsNone(installation) + installation = store.find_installation(enterprise_id=None, team_id="T111") + self.assertIsNone(installation) diff --git a/tests/slack_sdk/oauth/redirect_uri_page_renderer/__init__.py b/tests/slack_sdk/oauth/redirect_uri_page_renderer/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/slack_sdk/oauth/redirect_uri_page_renderer/test_init.py b/tests/slack_sdk/oauth/redirect_uri_page_renderer/test_init.py new file mode 100644 index 000000000..1b5310d3f --- /dev/null +++ b/tests/slack_sdk/oauth/redirect_uri_page_renderer/test_init.py @@ -0,0 +1,30 @@ +import unittest + +from slack_sdk.oauth import RedirectUriPageRenderer + + +class TestInit(unittest.TestCase): + def setUp(self): + pass + + def tearDown(self): + pass + + def test_render_failure_page(self): + renderer = RedirectUriPageRenderer( + install_path="/slack/install", + redirect_uri_path="/slack/oauth_redirect", + ) + page = renderer.render_failure_page("something-wrong") + self.assertTrue("Something Went Wrong!" in page) + + def test_render_success_page(self): + renderer = RedirectUriPageRenderer( + install_path="/slack/install", + redirect_uri_path="/slack/oauth_redirect", + ) + page = renderer.render_success_page(app_id="A111", team_id="T111") + self.assertTrue("slack://app?team=T111&id=A111" in page) + + page = renderer.render_success_page(app_id="A111", team_id=None) + self.assertTrue("slack://open" in page) diff --git a/tests/slack_sdk/oauth/state_store/__init__.py b/tests/slack_sdk/oauth/state_store/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/slack_sdk/oauth/state_store/test_amazon_s3.py b/tests/slack_sdk/oauth/state_store/test_amazon_s3.py new file mode 100644 index 000000000..277b8b83a --- /dev/null +++ b/tests/slack_sdk/oauth/state_store/test_amazon_s3.py @@ -0,0 +1,43 @@ +import unittest + +import boto3 + +try: + from moto import mock_aws +except ImportError: + from moto import mock_s3 as mock_aws +from slack_sdk.oauth.state_store.amazon_s3 import AmazonS3OAuthStateStore + + +class TestAmazonS3(unittest.TestCase): + mock_aws = mock_aws() + bucket_name = "test-bucket" + + def setUp(self): + self.mock_aws.start() + s3 = boto3.resource("s3") + bucket = s3.Bucket(self.bucket_name) + bucket.create(CreateBucketConfiguration={"LocationConstraint": "af-south-1"}) + + def tearDown(self): + self.mock_aws.stop() + + def test_instance(self): + store = AmazonS3OAuthStateStore( + s3_client=boto3.client("s3"), + bucket_name=self.bucket_name, + expiration_seconds=10, + ) + self.assertIsNotNone(store) + + def test_issue_and_consume(self): + store = AmazonS3OAuthStateStore( + s3_client=boto3.client("s3"), + bucket_name=self.bucket_name, + expiration_seconds=10, + ) + state = store.issue() + result = store.consume(state) + self.assertTrue(result) + result = store.consume(state) + self.assertFalse(result) diff --git a/tests/slack_sdk/oauth/state_store/test_async_sqlalchemy.py b/tests/slack_sdk/oauth/state_store/test_async_sqlalchemy.py new file mode 100644 index 000000000..87886c6ee --- /dev/null +++ b/tests/slack_sdk/oauth/state_store/test_async_sqlalchemy.py @@ -0,0 +1,38 @@ +import asyncio +import unittest +from tests.helpers import async_test +from sqlalchemy.ext.asyncio import AsyncEngine, create_async_engine + +from slack_sdk.oauth.state_store.sqlalchemy import AsyncSQLAlchemyOAuthStateStore + + +class TestSQLAlchemy(unittest.TestCase): + engine: AsyncEngine + + @async_test + async def setUp(self): + self.engine = create_async_engine("sqlite+aiosqlite:///:memory:") + self.store = AsyncSQLAlchemyOAuthStateStore(engine=self.engine, expiration_seconds=2) + async with self.engine.begin() as conn: + await conn.run_sync(self.store.metadata.create_all) + + @async_test + async def tearDown(self): + async with self.engine.begin() as conn: + await conn.run_sync(self.store.metadata.drop_all) + await self.engine.dispose() + + @async_test + async def test_issue_and_consume(self): + state = await self.store.async_issue() + result = await self.store.async_consume(state) + self.assertTrue(result) + result = await self.store.async_consume(state) + self.assertFalse(result) + + @async_test + async def test_expiration(self): + state = await self.store.async_issue() + await asyncio.sleep(3) + result = await self.store.async_consume(state) + self.assertFalse(result) diff --git a/tests/slack_sdk/oauth/state_store/test_file.py b/tests/slack_sdk/oauth/state_store/test_file.py new file mode 100644 index 000000000..49e430cc8 --- /dev/null +++ b/tests/slack_sdk/oauth/state_store/test_file.py @@ -0,0 +1,27 @@ +import unittest + +from slack_sdk.oauth.state_store import FileOAuthStateStore + + +class TestFile(unittest.TestCase): + def setUp(self): + pass + + def tearDown(self): + pass + + def test_instance(self): + store = FileOAuthStateStore(expiration_seconds=10) + self.assertIsNotNone(store) + + def test_issue_and_consume(self): + store = FileOAuthStateStore(expiration_seconds=10) + state = store.issue() + result = store.consume(state) + self.assertTrue(result) + result = store.consume(state) + self.assertFalse(result) + + def test_kwargs(self): + store = FileOAuthStateStore(expiration_seconds=10) + store.issue(foo=123, bar="baz") diff --git a/tests/slack_sdk/oauth/state_store/test_sqlalchemy.py b/tests/slack_sdk/oauth/state_store/test_sqlalchemy.py new file mode 100644 index 000000000..441400d60 --- /dev/null +++ b/tests/slack_sdk/oauth/state_store/test_sqlalchemy.py @@ -0,0 +1,33 @@ +import time +import unittest + +import sqlalchemy +from sqlalchemy.engine import Engine + +from slack_sdk.oauth.state_store.sqlalchemy import SQLAlchemyOAuthStateStore + + +class TestSQLAlchemy(unittest.TestCase): + engine: Engine + + def setUp(self): + self.engine = sqlalchemy.create_engine("sqlite:///:memory:") + self.store = SQLAlchemyOAuthStateStore(engine=self.engine, expiration_seconds=2) + self.store.metadata.create_all(self.engine) + + def tearDown(self): + self.store.metadata.drop_all(self.engine) + self.engine.dispose() + + def test_issue_and_consume(self): + state = self.store.issue() + result = self.store.consume(state) + self.assertTrue(result) + result = self.store.consume(state) + self.assertFalse(result) + + def test_expiration(self): + state = self.store.issue() + time.sleep(3) + result = self.store.consume(state) + self.assertFalse(result) diff --git a/tests/slack_sdk/oauth/state_store/test_sqlite3.py b/tests/slack_sdk/oauth/state_store/test_sqlite3.py new file mode 100644 index 000000000..7a34dd704 --- /dev/null +++ b/tests/slack_sdk/oauth/state_store/test_sqlite3.py @@ -0,0 +1,29 @@ +import unittest + +from slack_sdk.oauth.state_store.sqlite3 import SQLite3OAuthStateStore + + +class TestSQLite3(unittest.TestCase): + def setUp(self): + pass + + def tearDown(self): + pass + + def test_instance(self): + store = SQLite3OAuthStateStore( + database="logs/test.db", + expiration_seconds=10, + ) + self.assertIsNotNone(store) + + def test_issue_and_consume(self): + store = SQLite3OAuthStateStore( + database="logs/test.db", + expiration_seconds=10, + ) + state = store.issue() + result = store.consume(state) + self.assertTrue(result) + result = store.consume(state) + self.assertFalse(result) diff --git a/tests/slack_sdk/oauth/state_utils/__init__.py b/tests/slack_sdk/oauth/state_utils/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/slack_sdk/oauth/state_utils/test_utils.py b/tests/slack_sdk/oauth/state_utils/test_utils.py new file mode 100644 index 000000000..add49e3bc --- /dev/null +++ b/tests/slack_sdk/oauth/state_utils/test_utils.py @@ -0,0 +1,36 @@ +import unittest + +from slack_sdk.oauth import OAuthStateUtils + + +class TestUtils(unittest.TestCase): + def setUp(self): + pass + + def tearDown(self): + pass + + def test_is_valid_browser(self): + utils = OAuthStateUtils() + cookie_name = OAuthStateUtils.default_cookie_name + result = utils.is_valid_browser("state-value", {"cookie": f"{cookie_name}=state-value"}) + self.assertTrue(result) + result = utils.is_valid_browser("state-value", {"cookie": f"{cookie_name}=xxx"}) + self.assertFalse(result) + + result = utils.is_valid_browser("state-value", {"cookie": [f"{cookie_name}=state-value"]}) + self.assertTrue(result) + result = utils.is_valid_browser("state-value", {"cookie": [f"{cookie_name}=xxx"]}) + self.assertFalse(result) + + def test_build_set_cookie_for_new_state(self): + utils = OAuthStateUtils() + value = utils.build_set_cookie_for_new_state("state-value") + expected = "slack-app-oauth-state=state-value; Secure; HttpOnly; Path=/; Max-Age=600" + self.assertEqual(expected, value) + + def test_build_set_cookie_for_deletion(self): + utils = OAuthStateUtils() + value = utils.build_set_cookie_for_deletion() + expected = "slack-app-oauth-state=deleted; Secure; HttpOnly; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT" + self.assertEqual(expected, value) diff --git a/tests/slack_sdk/oauth/test_init.py b/tests/slack_sdk/oauth/test_init.py new file mode 100644 index 000000000..8e76ef566 --- /dev/null +++ b/tests/slack_sdk/oauth/test_init.py @@ -0,0 +1,21 @@ +import unittest + +from slack_sdk.oauth import AuthorizeUrlGenerator + + +class TestInit(unittest.TestCase): + def setUp(self): + pass + + def tearDown(self): + pass + + def test_generator(self): + generator = AuthorizeUrlGenerator( + client_id="111.222", + scopes=["chat:write", "commands"], + user_scopes=["search:read"], + ) + url = generator.generate("state-value") + expected = "https://slack.com/oauth/v2/authorize?state=state-value&client_id=111.222&scope=chat:write,commands&user_scope=search:read" + self.assertEqual(expected, url) diff --git a/tests/slack_sdk/oauth/token_rotation/__init__.py b/tests/slack_sdk/oauth/token_rotation/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/slack_sdk/oauth/token_rotation/test_token_rotator.py b/tests/slack_sdk/oauth/token_rotation/test_token_rotator.py new file mode 100644 index 000000000..d599e3af0 --- /dev/null +++ b/tests/slack_sdk/oauth/token_rotation/test_token_rotator.py @@ -0,0 +1,112 @@ +import unittest + +from slack_sdk.errors import SlackTokenRotationError +from slack_sdk.oauth.installation_store import Installation +from slack_sdk.oauth.token_rotation import TokenRotator +from slack_sdk.web import WebClient +from tests.slack_sdk.web.mock_web_api_handler import MockHandler +from tests.mock_web_api_server import setup_mock_web_api_server, cleanup_mock_web_api_server + + +class TestTokenRotator(unittest.TestCase): + def setUp(self): + setup_mock_web_api_server(self, MockHandler) + self.token_rotator = TokenRotator( + client=WebClient(base_url="http://localhost:8888", token=None), + client_id="111.222", + client_secret="token_rotation_secret", + ) + + def tearDown(self): + cleanup_mock_web_api_server(self) + + def test_refresh(self): + installation = Installation( + app_id="A111", + enterprise_id="E111", + team_id="T111", + user_id="U111", + bot_id="B111", + bot_token="xoxe.xoxp-1-initial", + bot_scopes=["chat:write"], + bot_user_id="U222", + bot_refresh_token="xoxe-1-initial", + bot_token_expires_in=43200, + ) + refreshed = self.token_rotator.perform_token_rotation( + installation=installation, minutes_before_expiration=60 * 24 * 365 + ) + self.assertIsNotNone(refreshed) + + should_not_be_refreshed = self.token_rotator.perform_token_rotation( + installation=installation, minutes_before_expiration=1 + ) + self.assertIsNone(should_not_be_refreshed) + + def test_refresh_with_custom_values(self): + installation = Installation( + app_id="A111", + enterprise_id="E111", + team_id="T111", + user_id="U111", + bot_id="B111", + bot_token="xoxe.xoxp-1-initial", + bot_scopes=["chat:write"], + bot_user_id="U222", + bot_refresh_token="xoxe-1-initial", + bot_token_expires_in=43200, + custom_values={"foo": "bar"}, + ) + refreshed = self.token_rotator.perform_token_rotation( + installation=installation, minutes_before_expiration=60 * 24 * 365 + ) + self.assertIsNotNone(refreshed) + self.assertIsNotNone(refreshed.custom_values) + + should_not_be_refreshed = self.token_rotator.perform_token_rotation( + installation=installation, minutes_before_expiration=1 + ) + self.assertIsNone(should_not_be_refreshed) + + def test_token_rotation_disabled(self): + installation = Installation( + app_id="A111", + enterprise_id="E111", + team_id="T111", + user_id="U111", + bot_id="B111", + bot_token="xoxe.xoxp-1-initial", + bot_scopes=["chat:write"], + bot_user_id="U222", + ) + should_not_be_refreshed = self.token_rotator.perform_token_rotation( + installation=installation, minutes_before_expiration=60 * 24 * 365 + ) + self.assertIsNone(should_not_be_refreshed) + + should_not_be_refreshed = self.token_rotator.perform_token_rotation( + installation=installation, minutes_before_expiration=1 + ) + self.assertIsNone(should_not_be_refreshed) + + def test_refresh_error(self): + token_rotator = TokenRotator( + client=WebClient(base_url="http://localhost:8888", token=None), + client_id="111.222", + client_secret="invalid_value", + ) + + installation = Installation( + app_id="A111", + enterprise_id="E111", + team_id="T111", + user_id="U111", + bot_id="B111", + bot_token="xoxe.xoxp-1-initial", + bot_scopes=["chat:write"], + bot_user_id="U222", + bot_refresh_token="xoxe-1-initial", + bot_token_expires_in=43200, + ) + with self.assertRaises(SlackTokenRotationError): + token_rotator.perform_token_rotation(installation=installation, minutes_before_expiration=60 * 24 * 365) diff --git a/tests/slack_sdk/rtm_v2/__init__.py b/tests/slack_sdk/rtm_v2/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/slack_sdk/rtm_v2/test_rtm_v2.py b/tests/slack_sdk/rtm_v2/test_rtm_v2.py new file mode 100644 index 000000000..e2d290fd4 --- /dev/null +++ b/tests/slack_sdk/rtm_v2/test_rtm_v2.py @@ -0,0 +1,99 @@ +import unittest + +from slack_sdk.rtm_v2 import RTMClient +from slack_sdk import errors as e +from tests.rtm.mock_web_api_server import ( + setup_mock_web_api_server, + cleanup_mock_web_api_server, +) + + +class TestRTMClient(unittest.TestCase): + def setUp(self): + setup_mock_web_api_server(self) + self.rtm = RTMClient( + token="xoxp-1234", + base_url="http://localhost:8888", + auto_reconnect_enabled=False, + ) + + def tearDown(self): + cleanup_mock_web_api_server(self) + + def test_run_on_returns_callback(self): + def fn1(client, payload): + pass + + @self.rtm.on("message") + def fn2(client, payload): + pass + + self.assertIsNotNone(fn1) + self.assertIsNotNone(fn2) + self.assertEqual(fn2.__name__, "fn2") + + def test_run_on_annotation_sets_callbacks(self): + @self.rtm.on("message") + def say_run_on(client, payload): + pass + + self.assertTrue(len(self.rtm.message_listeners) == 2) + + def test_on_sets_callbacks(self): + def say_on(client, payload): + pass + + self.rtm.on("message")(say_on) + self.assertTrue(len(self.rtm.message_listeners) == 2) + + def test_on_accepts_a_list_of_callbacks(self): + def say_on(client, payload): + pass + + def say_off(client, payload): + pass + + self.rtm.on("message")(say_on) + self.rtm.on("message")(say_off) + self.assertEqual(len(self.rtm.message_listeners), 3) + + def test_on_raises_when_not_callable(self): + invalid_callback = "a" + + with self.assertRaises(e.SlackClientError) as context: + self.rtm.on("message")(invalid_callback) + + expected_error = "The listener 'a' is not a Callable (actual: str)" + error = str(context.exception) + self.assertIn(expected_error, error) + + def test_on_raises_when_kwargs_not_accepted(self): + def invalid_cb(): + pass + + with self.assertRaises(e.SlackClientError) as context: + self.rtm.on("message")(invalid_cb) + + error = str(context.exception) + self.assertIn( + "The listener 'invalid_cb' must accept two args: client, event (actual: )", + error, + ) + + def test_send_over_websocket_raises_when_not_connected(self): + with self.assertRaises(e.SlackClientError) as context: + self.rtm.send(payload={}) + + expected_error = "The RTM client is not connected to the Slack servers" + error = str(context.exception) + self.assertIn(expected_error, error) + + def test_start_raises_an_error_if_rtm_ws_url_is_not_returned(self): + with self.assertRaises(e.SlackApiError) as context: + RTMClient(token="xoxp-1234", auto_reconnect_enabled=False).start() + + expected_error = ( + "The request to the Slack API failed. (url: https://slack.com/api/auth.test)\n" + "The server responded with: {'ok': False, 'error': 'invalid_auth'}" + ) + self.assertIn(expected_error, str(context.exception)) diff --git a/tests/slack_sdk/scim/__init__.py b/tests/slack_sdk/scim/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/slack_sdk/scim/mock_web_api_handler.py b/tests/slack_sdk/scim/mock_web_api_handler.py new file mode 100644 index 000000000..6b31f4b1e --- /dev/null +++ b/tests/slack_sdk/scim/mock_web_api_handler.py @@ -0,0 +1,64 @@ +import logging +import re +from http import HTTPStatus +from http.server import SimpleHTTPRequestHandler + + +class MockHandler(SimpleHTTPRequestHandler): + protocol_version = "HTTP/1.1" + default_request_version = "HTTP/1.1" + logger = logging.getLogger(__name__) + + pattern_for_language = re.compile("python/(\\S+)", re.IGNORECASE) + pattern_for_package_identifier = re.compile("slackclient/(\\S+)") + + def is_valid_user_agent(self): + user_agent = self.headers["User-Agent"] + return self.pattern_for_language.search(user_agent) and self.pattern_for_package_identifier.search(user_agent) + + def is_valid_token(self): + if self.path.startswith("oauth"): + return True + return "Authorization" in self.headers and str(self.headers["Authorization"]).startswith("Bearer xoxp-") + + def set_common_headers(self): + self.send_header("content-type", "application/json;charset=utf-8") + self.send_header("connection", "close") + self.end_headers() + + def _handle(self): + try: + # put_nowait is common between Queue & asyncio.Queue, it does not need to be awaited + self.server.queue.put_nowait(self.path) + header = self.headers["Authorization"] + if header is not None and "xoxp-" in header: + pattern = str(header).split("xoxp-", 1)[1] + if "remote_disconnected" in pattern: + # http.client.RemoteDisconnected + self.finish() + return + if "ratelimited" in pattern: + self.send_response(429) + self.send_header("retry-after", 1) + self.set_common_headers() + self.wfile.write("""{"ok": false, "error": "ratelimited"}""".encode("utf-8")) + return + + if self.is_valid_token() and self.is_valid_user_agent(): + self.send_response(HTTPStatus.OK) + self.set_common_headers() + self.wfile.close() + else: + self.send_response(HTTPStatus.BAD_REQUEST) + self.set_common_headers() + self.wfile.close() + + except Exception as e: + self.logger.error(str(e), exc_info=True) + raise + + def do_GET(self): + self._handle() + + def do_POST(self): + self._handle() diff --git a/tests/slack_sdk/scim/test_client.py b/tests/slack_sdk/scim/test_client.py new file mode 100644 index 000000000..2ba68b1d5 --- /dev/null +++ b/tests/slack_sdk/scim/test_client.py @@ -0,0 +1,72 @@ +import time +import unittest + +from slack_sdk.scim import SCIMClient, User, Group +from slack_sdk.scim.v1.group import GroupMember +from slack_sdk.scim.v1.user import UserName, UserEmail +from tests.slack_sdk.scim.mock_web_api_handler import MockHandler +from tests.mock_web_api_server import setup_mock_web_api_server, cleanup_mock_web_api_server + + +class TestSCIMClient(unittest.TestCase): + def setUp(self): + setup_mock_web_api_server(self, MockHandler) + + def tearDown(self): + cleanup_mock_web_api_server(self) + + def test_users(self): + client = SCIMClient(base_url="http://localhost:8888/", token="xoxp-valid") + client.search_users(start_index=0, count=1) + client.read_user("U111") + + now = str(time.time())[:10] + user = User( + user_name=f"user_{now}", + name=UserName(given_name="Kaz", family_name="Sera"), + emails=[UserEmail(value=f"seratch+{now}@example.com")], + schemas=["urn:scim:schemas:core:1.0"], + ) + client.create_user(user) + # The mock server does not work for PATH requests + try: + client.patch_user("U111", partial_user=User(user_name="foo")) + except: + pass + user.id = "U111" + user.user_name = "updated" + try: + client.update_user(user) + except: + pass + try: + client.delete_user("U111") + except: + pass + + def test_groups(self): + client = SCIMClient(base_url="http://localhost:8888/", token="xoxp-valid") + client.search_groups(start_index=0, count=1) + client.read_group("S111") + + now = str(time.time())[:10] + group = Group( + display_name=f"TestGroup_{now}", + members=[GroupMember(value="U111")], + ) + client.create_group(group) + # The mock server does not work for PATH requests + try: + client.patch_group("S111", partial_group=Group(display_name=f"TestGroup_{now}_2")) + except: + pass + group.id = "S111" + group.display_name = "updated" + try: + client.update_group(group) + except: + pass + try: + client.delete_group("S111") + except: + pass diff --git a/tests/slack_sdk/scim/test_client_http_retry.py b/tests/slack_sdk/scim/test_client_http_retry.py new file mode 100644 index 000000000..47d7adfc3 --- /dev/null +++ b/tests/slack_sdk/scim/test_client_http_retry.py @@ -0,0 +1,42 @@ +import unittest + +from slack_sdk.http_retry import RateLimitErrorRetryHandler +from slack_sdk.scim import SCIMClient +from tests.slack_sdk.scim.mock_web_api_handler import MockHandler +from tests.mock_web_api_server import setup_mock_web_api_server, cleanup_mock_web_api_server +from ..my_retry_handler import MyRetryHandler + + +class TestSCIMClient(unittest.TestCase): + def setUp(self): + setup_mock_web_api_server(self, MockHandler) + + def tearDown(self): + cleanup_mock_web_api_server(self) + + def test_retries(self): + retry_handler = MyRetryHandler(max_retry_count=2) + client = SCIMClient( + base_url="http://localhost:8888/", + token="xoxp-remote_disconnected", + retry_handlers=[retry_handler], + ) + + try: + client.search_users(start_index=0, count=1) + self.fail("An exception is expected") + except Exception as _: + pass + + self.assertEqual(2, retry_handler.call_count) + + def test_ratelimited(self): + client = SCIMClient( + base_url="http://localhost:8888/", + token="xoxp-ratelimited", + ) + client.retry_handlers.append(RateLimitErrorRetryHandler()) + + response = client.search_users(start_index=0, count=1) + # Just running retries; no assertions for call count so far + self.assertEqual(429, response.status_code) diff --git a/tests/slack_sdk/scim/test_internals.py b/tests/slack_sdk/scim/test_internals.py new file mode 100644 index 000000000..d0a27b17b --- /dev/null +++ b/tests/slack_sdk/scim/test_internals.py @@ -0,0 +1,18 @@ +import json +import unittest + +from slack_sdk.scim.v1.internal_utils import _to_snake_cased + + +class TEstInternals(unittest.TestCase): + def setUp(self): + pass + + def tearDown(self): + pass + + def test_snake_cased(self): + response_body = """{"totalResults":441,"itemsPerPage":1,"startIndex":1,"schemas":["urn:scim:schemas:core:1.0"],"Resources":[{"schemas":["urn:scim:schemas:core:1.0"],"id":"W111","externalId":"","meta":{"created":"2020-08-13T04:15:35-07:00","location":"https://api.slack.com/scim/v1/Users/W111"},"userName":"test-app","nickName":"test-app","name":{"givenName":"","familyName":""},"displayName":"","profileUrl":"https://test-test-test.enterprise.slack.com/team/test-app","title":"","timezone":"America/Los_Angeles","active":true,"emails":[{"value":"botuser@slack-bots.com","primary":true}],"photos":[{"value":"https://secure.gravatar.com/avatar/xxx.jpg","type":"photo"}],"groups":[]}]}""" + result = _to_snake_cased(json.loads(response_body)) + self.assertEqual(result["start_index"], 1) + self.assertIsNotNone(result["resources"][0]["id"]) diff --git a/tests/slack_sdk/scim/test_response.py b/tests/slack_sdk/scim/test_response.py new file mode 100644 index 000000000..781f45ad3 --- /dev/null +++ b/tests/slack_sdk/scim/test_response.py @@ -0,0 +1,79 @@ +import json +import unittest + +from slack_sdk.scim import SearchUsersResponse, SCIMResponse +from slack_sdk.scim.v1.internal_utils import _to_snake_cased + + +class TEstInternals(unittest.TestCase): + def setUp(self): + pass + + def tearDown(self): + pass + + def test_snake_cased(self): + response_body = """{ + "totalResults": 441, + "itemsPerPage": 1, + "startIndex": 1, + "schemas": [ + "urn:scim:schemas:core:1.0" + ], + "Resources": [ + { + "schemas": [ + "urn:scim:schemas:core:1.0" + ], + "id": "W111", + "externalId": "", + "meta": { + "created": "2020-08-13T04:15:35-07:00", + "location": "https://api.slack.com/scim/v1/Users/W111", + "newAttribute": "this should be just accepted as unknown attribute" + }, + "userName": "test-app", + "nickName": "test-app", + "name": { + "givenName": "", + "familyName": "", + "newAttribute": "this should be just accepted as unknown attribute" + }, + "displayName": "", + "profileUrl": "https://test-test-test.enterprise.slack.com/team/test-app", + "title": "", + "timezone": "America/Los_Angeles", + "active": true, + "emails": [ + { + "value": "botuser@slack-bots.com", + "primary": true, + "newAttribute": "this should be just accepted as unknown attribute" + } + ], + "photos": [ + { + "value": "https://secure.gravatar.com/avatar/xxx.jpg", + "type": "photo", + "newAttribute": "this should be just accepted as unknown attribute" + } + ], + "groups": [], + "newAttribute": "this should be just accepted as unknown attribute" + } + ], + "newAttribute": "this should be just accepted as unknown attribute" +} +""" + response = SearchUsersResponse( + SCIMResponse( + url="https://www.example.com", + status_code=200, + raw_body=response_body, + headers={}, + ) + ) + user = response.users[0] + self.assertIsNotNone(user.unknown_fields.get("new_attribute")) + # the unknown fields need to be also camel-cased + self.assertIsNotNone(user.to_dict().get("newAttribute")) diff --git a/tests/slack_sdk/signature/__init__.py b/tests/slack_sdk/signature/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/slack_sdk/signature/test_signature_verifier.py b/tests/slack_sdk/signature/test_signature_verifier.py new file mode 100644 index 000000000..1635c1f62 --- /dev/null +++ b/tests/slack_sdk/signature/test_signature_verifier.py @@ -0,0 +1,99 @@ +import unittest + +from slack_sdk.signature import SignatureVerifier + + +class MockClock: + def now(self) -> float: + return 1531420618 + + +class TestSignatureVerifier(unittest.TestCase): + def setUp(self): + pass + + def tearDown(self): + pass + + # https://docs.slack.dev/authentication/verifying-requests-from-slack/ + signing_secret = "8f742231b10e8888abcd99yyyzzz85a5" + + body = "token=xyzz0WbapA4vBCDEFasx0q6G&team_id=T1DC2JH3J&team_domain=testteamnow&channel_id=G8PSS9T3V&channel_name=foobar&user_id=U2CERLKJA&user_name=roadrunner&command=%2Fwebhook-collect&text=&response_url=https%3A%2F%2Fhooks.slack.com%2Fcommands%2FT1DC2JH3J%2F397700885554%2F96rGlfmibIGlgcZRskXaIFfN&trigger_id=398738663015.47445629121.803a0bc887a14d10d2c447fce8b6703c" + + timestamp = "1531420618" + valid_signature = "v0=a2114d57b48eac39b9ad189dd8316235a7b4a8d21a10bd27519666489c69b503" + + headers = { + "X-Slack-Request-Timestamp": timestamp, + "X-Slack-Signature": valid_signature, + } + + def test_generate_signature(self): + verifier = SignatureVerifier(self.signing_secret) + signature = verifier.generate_signature(timestamp=self.timestamp, body=self.body) + self.assertEqual(self.valid_signature, signature) + + def test_generate_signature_body_as_bytes(self): + verifier = SignatureVerifier(self.signing_secret) + signature = verifier.generate_signature(timestamp=self.timestamp, body=self.body.encode("utf-8")) + self.assertEqual(self.valid_signature, signature) + + def test_is_valid_request(self): + verifier = SignatureVerifier(signing_secret=self.signing_secret, clock=MockClock()) + self.assertTrue(verifier.is_valid_request(self.body, self.headers)) + + def test_is_valid_request_body_as_bytes(self): + verifier = SignatureVerifier(signing_secret=self.signing_secret, clock=MockClock()) + self.assertTrue(verifier.is_valid_request(self.body.encode("utf-8"), self.headers)) + + def test_is_valid_request_invalid_body(self): + verifier = SignatureVerifier( + signing_secret=self.signing_secret, + clock=MockClock(), + ) + modified_body = self.body + "------" + self.assertFalse(verifier.is_valid_request(modified_body, self.headers)) + + def test_is_valid_request_invalid_body_as_bytes(self): + verifier = SignatureVerifier( + signing_secret=self.signing_secret, + clock=MockClock(), + ) + modified_body = self.body + "------" + self.assertFalse(verifier.is_valid_request(modified_body.encode("utf-8"), self.headers)) + + def test_is_valid_request_expiration(self): + verifier = SignatureVerifier( + signing_secret=self.signing_secret, + ) + self.assertFalse(verifier.is_valid_request(self.body, self.headers)) + + def test_is_valid_request_none(self): + verifier = SignatureVerifier( + signing_secret=self.signing_secret, + clock=MockClock(), + ) + self.assertFalse(verifier.is_valid_request(None, self.headers)) + self.assertFalse(verifier.is_valid_request(self.body, None)) + self.assertFalse(verifier.is_valid_request(None, None)) + + def test_is_valid(self): + verifier = SignatureVerifier( + signing_secret=self.signing_secret, + clock=MockClock(), + ) + self.assertTrue(verifier.is_valid(self.body, self.timestamp, self.valid_signature)) + self.assertTrue(verifier.is_valid(self.body, 1531420618, self.valid_signature)) + + def test_is_valid_none(self): + verifier = SignatureVerifier( + signing_secret=self.signing_secret, + clock=MockClock(), + ) + self.assertFalse(verifier.is_valid(None, self.timestamp, self.valid_signature)) + self.assertFalse(verifier.is_valid(self.body, None, self.valid_signature)) + self.assertFalse(verifier.is_valid(self.body, self.timestamp, None)) + self.assertFalse(verifier.is_valid(None, None, self.valid_signature)) + self.assertFalse(verifier.is_valid(None, self.timestamp, None)) + self.assertFalse(verifier.is_valid(self.body, None, None)) + self.assertFalse(verifier.is_valid(None, None, None)) diff --git a/tests/slack_sdk/socket_mode/__init__.py b/tests/slack_sdk/socket_mode/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/slack_sdk/socket_mode/logger/__init__.py b/tests/slack_sdk/socket_mode/logger/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/slack_sdk/socket_mode/logger/test_messages.py b/tests/slack_sdk/socket_mode/logger/test_messages.py new file mode 100644 index 000000000..e241f9b77 --- /dev/null +++ b/tests/slack_sdk/socket_mode/logger/test_messages.py @@ -0,0 +1,26 @@ +import unittest + +from slack_sdk.socket_mode.logger.messages import debug_redacted_message_string + + +class TestRequest(unittest.TestCase): + def setUp(self): + pass + + def tearDown(self): + pass + + def test_debug_redacted_message_string(self): + message = """{"envelope_id":"abc-123","payload":{"token":"xxx","team_id":"T123","api_app_id":"A123","event":{"type":"function_executed","function":{"id":"Fn123","callback_id":"sample_function","title":"Sample function","description":"","type":"app","input_parameters":[],"output_parameters":[],"app_id":"A123","date_created":1719416102,"date_released":0,"date_updated":1719426759,"date_deleted":0,"form_enabled":false},"inputs":{"user_id":"U123"},"function_execution_id":"Fx123","workflow_execution_id":"Wx079QN9CT8E","event_ts":"1719427571.129426","bot_access_token":"xwfp-123-abc"},"type":"event_callback","event_id":"Ev123","event_time":1719427571},"type":"events_api","accepts_response_payload":false,"retry_attempt":0,"retry_reason":""}""" + redacted_message = debug_redacted_message_string(message) + self.assertEqual(redacted_message.count('"bot_access_token":[[REDACTED]]'), 1) + + def test_debug_redacted_message_string_no_changes(self): + message = """{"envelope_id":"abc-123","payload":{"token":"xxx","team_id":"T123","api_app_id":"A123","event":{"type":"function_executed","function":{"id":"Fn123","callback_id":"sample_function","title":"Sample function","description":"","type":"app","input_parameters":[],"output_parameters":[],"app_id":"A123","date_created":1719416102,"date_released":0,"date_updated":1719426759,"date_deleted":0,"form_enabled":false},"inputs":{"user_id":"U123"},"function_execution_id":"Fx123","workflow_execution_id":"Wx079QN9CT8E","event_ts":"1719427571.129426"},"type":"event_callback","event_id":"Ev123","event_time":1719427571},"type":"events_api","accepts_response_payload":false,"retry_attempt":0,"retry_reason":""}""" + redacted_message = debug_redacted_message_string(message) + self.assertEqual(redacted_message.count('"bot_access_token":[[REDACTED]]'), 0) + + def test_debug_redacted_message_string_simple(self): + message = '"bot_access_token": "xwfp-123-abc"' + redacted_message = debug_redacted_message_string(message) + self.assertEqual(redacted_message.count('"bot_access_token": [[REDACTED]]'), 1) diff --git a/tests/slack_sdk/socket_mode/mock_socket_mode_server.py b/tests/slack_sdk/socket_mode/mock_socket_mode_server.py new file mode 100644 index 000000000..561cef8f2 --- /dev/null +++ b/tests/slack_sdk/socket_mode/mock_socket_mode_server.py @@ -0,0 +1,160 @@ +import asyncio +import logging +import os +import threading +import time +from typing import Any, Dict +from urllib.error import URLError +from urllib.request import urlopen +from unittest import TestCase + +from aiohttp import WSMsgType, web + + +socket_mode_envelopes = [ + """{"envelope_id":"1d3c79ab-0ffb-41f3-a080-d19e85f53649","payload":{"token":"verification-token","team_id":"T111","team_domain":"xxx","channel_id":"C111","channel_name":"random","user_id":"U111","user_name":"testxyz","command":"/hello-socket-mode","text":"","api_app_id":"A111","response_url":"https://hooks.slack.com/commands/T111/111/xxx","trigger_id":"111.222.xxx"},"type":"slash_commands","accepts_response_payload":true}""", + """{"envelope_id":"cda4159a-72a5-4744-aba3-4d66eb52682b","payload":{"token":"verification-token","team_id":"T111","api_app_id":"A111","event":{"client_msg_id":"f0582a78-72db-4feb-b2f3-1e47d66365c8","type":"app_mention","text":"<@U111>","user":"U222","ts":"1610241741.000200","team":"T111","blocks":[{"type":"rich_text","block_id":"Sesm","elements":[{"type":"rich_text_section","elements":[{"type":"user","user_id":"U111"}]}]}],"channel":"C111","event_ts":"1610241741.000200"},"type":"event_callback","event_id":"Ev111","event_time":1610241741,"authorizations":[{"enterprise_id":null,"team_id":"T111","user_id":"U222","is_bot":true,"is_enterprise_install":false}],"is_ext_shared_channel":false,"event_context":"1-app_mention-T111-C111"},"type":"events_api","accepts_response_payload":false,"retry_attempt":0,"retry_reason":""}""", + """{"envelope_id":"57d6a792-4d35-4d0b-b6aa-3361493e1caf","payload":{"type":"shortcut","token":"verification-token","action_ts":"1610198080.300836","team":{"id":"T111","domain":"testxyz"},"user":{"id":"U111","username":"testxyz","team_id":"T111"},"is_enterprise_install":false,"enterprise":null,"callback_id":"do-something","trigger_id":"111.222.xxx"},"type":"interactive","accepts_response_payload":false}""", + """{"envelope_id":"ac2cfd40-6f8c-4d5e-a1ad-646e532baa19","payload":{"token":"verification-token","team_id":"T111","api_app_id":"A111","event":{"client_msg_id":"f0582a78-72db-4feb-b2f3-1e47d66365c8","type":"message","text":"<@U111> Hi here!","user":"U222","ts":"1610241741.000200","team":"T111","channel":"C111","event_ts":"1610241741.000200","channel_type":"channel"},"type":"event_callback","event_id":"Ev111","event_time":1610241741,"authorizations":[{"enterprise_id":null,"team_id":"T111","user_id":"U333","is_bot":true,"is_enterprise_install":false}],"is_ext_shared_channel":false,"event_context":"1-message-T111-C111"},"type":"events_api","accepts_response_payload":false,"retry_attempt":0,"retry_reason":""}""", + """{"envelope_id":"dfb98ec6-45a8-4200-98af-7d05fb6fb44f","payload":{"type":"block_actions","user":{"id":"U12345678","username":"testxyz","name":"testxyz","team_id":"T12345678"},"api_app_id":"A1234567890","token":"123456789012345678901234","container":{"type":"view","view_id":"V1234567890"},"trigger_id":"3701415066658.3485157640.72f11de3d52bb9593d6931af772a323e","team":{"id":"T12345678","domain":"testxyz"},"enterprise":null,"is_enterprise_install":false,"view":{"id":"V1234567890","team_id":"T12345678","type":"modal","blocks":[{"type":"actions","block_id":"block","elements":[{"type":"static_select","action_id":"a","option_groups":[{"label":{"type":"plain_text","text":"LabelLabelLabelLabelLabelLabelLabelLabel","emoji":true},"options":[{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}]},{"label":{"type":"plain_text","text":"LabelLabelLabelLabelLabelLabelLabelLabel","emoji":true},"options":[{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}]},{"label":{"type":"plain_text","text":"LabelLabelLabelLabelLabelLabelLabelLabel","emoji":true},"options":[{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}]},{"label":{"type":"plain_text","text":"LabelLabelLabelLabelLabelLabelLabelLabel","emoji":true},"options":[{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}]},{"label":{"type":"plain_text","text":"LabelLabelLabelLabelLabelLabelLabelLabel","emoji":true},"options":[{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}]},{"label":{"type":"plain_text","text":"LabelLabelLabelLabelLabelLabelLabelLabel","emoji":true},"options":[{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}]},{"label":{"type":"plain_text","text":"LabelLabelLabelLabelLabelLabelLabelLabel","emoji":true},"options":[{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}]},{"label":{"type":"plain_text","text":"LabelLabelLabelLabelLabelLabelLabelLabel","emoji":true},"options":[{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}]},{"label":{"type":"plain_text","text":"LabelLabelLabelLabelLabelLabelLabelLabel","emoji":true},"options":[{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}]},{"label":{"type":"plain_text","text":"LabelLabelLabelLabelLabelLabelLabelLabel","emoji":true},"options":[{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}]}]}]}],"private_metadata":"","callback_id":"","state":{"values":{"block":{"a":{"type":"static_select","selected_option":{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}}}}},"hash":"1655863316.uFehmGP0","title":{"type":"plain_text","text":"My title","emoji":true},"clear_on_close":false,"notify_on_close":false,"close":{"type":"plain_text","text":"Cancel","emoji":true},"submit":{"type":"plain_text","text":"Submit","emoji":true},"previous_view_id":null,"root_view_id":"V1234567890","app_id":"A1234567890","external_id":"","app_installed_team_id":"T12345678","bot_id":"B1234567890"},"actions":[{"type":"static_select","action_id":"a","block_id":"block","selected_option":{"text":{"type":"plain_text","text":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","emoji":true},"value":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},"action_ts":"1655863389.628431"}]},"type":"interactive","accepts_response_payload":false}""", +] + +if os.environ.get("CI_LARGE_SOCKET_MODE_PAYLOAD_TESTING_DISABLED") == "1": + # Remove the large payload data testing because it can be very unstable in GitHub Actions container environment + socket_mode_envelopes = [ + """{"envelope_id":"1d3c79ab-0ffb-41f3-a080-d19e85f53649","payload":{"token":"verification-token","team_id":"T111","team_domain":"xxx","channel_id":"C111","channel_name":"random","user_id":"U111","user_name":"testxyz","command":"/hello-socket-mode","text":"","api_app_id":"A111","response_url":"https://hooks.slack.com/commands/T111/111/xxx","trigger_id":"111.222.xxx"},"type":"slash_commands","accepts_response_payload":true}""", + """{"envelope_id":"cda4159a-72a5-4744-aba3-4d66eb52682b","payload":{"token":"verification-token","team_id":"T111","api_app_id":"A111","event":{"client_msg_id":"f0582a78-72db-4feb-b2f3-1e47d66365c8","type":"app_mention","text":"<@U111>","user":"U222","ts":"1610241741.000200","team":"T111","blocks":[{"type":"rich_text","block_id":"Sesm","elements":[{"type":"rich_text_section","elements":[{"type":"user","user_id":"U111"}]}]}],"channel":"C111","event_ts":"1610241741.000200"},"type":"event_callback","event_id":"Ev111","event_time":1610241741,"authorizations":[{"enterprise_id":null,"team_id":"T111","user_id":"U222","is_bot":true,"is_enterprise_install":false}],"is_ext_shared_channel":false,"event_context":"1-app_mention-T111-C111"},"type":"events_api","accepts_response_payload":false,"retry_attempt":0,"retry_reason":""}""", + """{"envelope_id":"57d6a792-4d35-4d0b-b6aa-3361493e1caf","payload":{"type":"shortcut","token":"verification-token","action_ts":"1610198080.300836","team":{"id":"T111","domain":"testxyz"},"user":{"id":"U111","username":"testxyz","team_id":"T111"},"is_enterprise_install":false,"enterprise":null,"callback_id":"do-something","trigger_id":"111.222.xxx"},"type":"interactive","accepts_response_payload":false}""", + """{"envelope_id":"ac2cfd40-6f8c-4d5e-a1ad-646e532baa19","payload":{"token":"verification-token","team_id":"T111","api_app_id":"A111","event":{"client_msg_id":"f0582a78-72db-4feb-b2f3-1e47d66365c8","type":"message","text":"<@U111> Hi here!","user":"U222","ts":"1610241741.000200","team":"T111","channel":"C111","event_ts":"1610241741.000200","channel_type":"channel"},"type":"event_callback","event_id":"Ev111","event_time":1610241741,"authorizations":[{"enterprise_id":null,"team_id":"T111","user_id":"U333","is_bot":true,"is_enterprise_install":false}],"is_ext_shared_channel":false,"event_context":"1-message-T111-C111"},"type":"events_api","accepts_response_payload":false,"retry_attempt":0,"retry_reason":""}""", + ] + +socket_mode_hello_message = """{"type":"hello","num_connections":2,"debug_info":{"host":"applink-111-xxx","build_number":10,"approximate_connection_time":18060},"connection_info":{"app_id":"A111"}}""" + +socket_mode_disconnect_message = """{"type":"disconnect","reason":"too_many_websockets","num_connections":2,"debug_info":{"host":"applink-111-xxx"},"connection_info":{"app_id":"A111"}}""" + + +def start_thread_socket_mode_server(self, port: int): + logger = logging.getLogger(__name__) + state: Dict[str, Any] = {} + + def reset_server_state(): + state.update( + hello_sent=False, + disconnect=False, + envelopes_to_consume=list(socket_mode_envelopes), + ) + + self.reset_server_state = reset_server_state + + async def health(request: web.Request): + wr = web.Response() + await wr.prepare(request) + wr.set_status(200) + return wr + + async def disconnect(request: web.Request): + state["disconnect"] = True + wr = web.Response() + await wr.prepare(request) + wr.set_status(200) + return wr + + async def link(request): + connected = True + ws = web.WebSocketResponse() + await ws.prepare(request) + + async for msg in ws: + if msg.type == WSMsgType.PING: + await ws.pong(f"sdk-ping-pong:{time.time()}") + continue + if msg.type != WSMsgType.TEXT: + continue + + message = msg.data + logger.debug(f"Server received a message: {message}") + + if not state["hello_sent"]: + state["hello_sent"] = True + await ws.send_str(socket_mode_hello_message) + + if state["disconnect"]: + state["hello_sent"] = False + state["disconnect"] = False + connected = False + await ws.send_str(socket_mode_disconnect_message) + logger.debug("Disconnect message sent") + + if state["envelopes_to_consume"] and connected: + e = state["envelopes_to_consume"].pop(0) + logger.debug(f"Send an envelope: {e}") + await ws.send_str(e) + + await ws.send_str(message) + + return ws + + app = web.Application() + app.add_routes( + [ + web.get("/link", link), + web.get("/health", health), + web.get("/disconnect", disconnect), + ] + ) + runner = web.AppRunner(app) + + def run_server(): + reset_server_state() + + self.loop = loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + loop.run_until_complete(runner.setup()) + site = web.TCPSite(runner, "127.0.0.1", port, reuse_port=True) + loop.run_until_complete(site.start()) + + # run until it's stopped from the main thread + loop.run_forever() + + loop.run_until_complete(runner.cleanup()) + loop.close() + + return run_server + + +def start_socket_mode_server(test, port: int): + test.sm_thread = threading.Thread(target=start_thread_socket_mode_server(test, port)) + test.sm_thread.daemon = True + test.sm_thread.start() + wait_for_socket_mode_server(port, 4) + + +def stop_socket_mode_server(test: TestCase): + # An event loop runs in a thread and executes all callbacks and Tasks in + # its thread. While a Task is running in the event loop, no other Tasks + # can run in the same thread. When a Task executes an await expression, the + # running Task gets suspended, and the event loop executes the next Task. + # To schedule a callback from another OS thread, the loop.call_soon_threadsafe() method should be used. + # https://docs.python.org/3/library/asyncio-dev.html#asyncio-multithreading + test.loop.call_soon_threadsafe(test.loop.stop) + test.sm_thread.join(timeout=5) + + +def wait_for_socket_mode_server(port: int, timeout: int): + start_time = time.time() + while (time.time() - start_time) < timeout: + try: + urlopen(f"http://127.0.0.1:{port}/health") + return + except URLError: + time.sleep(0.01) + + +def request_socket_mode_server_disconnect(port: int, timeout: int): + start_time = time.time() + while (time.time() - start_time) < timeout: + try: + urlopen(f"http://127.0.0.1:{port}/disconnect") + return + except URLError: + time.sleep(0.01) diff --git a/tests/slack_sdk/socket_mode/mock_web_api_handler.py b/tests/slack_sdk/socket_mode/mock_web_api_handler.py new file mode 100644 index 000000000..387778ecb --- /dev/null +++ b/tests/slack_sdk/socket_mode/mock_web_api_handler.py @@ -0,0 +1,111 @@ +import json +import logging +import re +from http import HTTPStatus +from http.server import SimpleHTTPRequestHandler +from urllib.parse import urlparse, parse_qs + + +class MockHandler(SimpleHTTPRequestHandler): + protocol_version = "HTTP/1.1" + default_request_version = "HTTP/1.1" + logger = logging.getLogger(__name__) + + pattern_for_language = re.compile("python/(\\S+)", re.IGNORECASE) + pattern_for_package_identifier = re.compile("slackclient/(\\S+)") + + def is_valid_user_agent(self): + user_agent = self.headers["User-Agent"] + return self.pattern_for_language.search(user_agent) and self.pattern_for_package_identifier.search(user_agent) + + def is_valid_token(self): + if self.path.startswith("oauth"): + return True + return "Authorization" in self.headers and ( + str(self.headers["Authorization"]).startswith("Bearer xoxb-") + or str(self.headers["Authorization"]).startswith("Bearer xapp-") + ) + + def set_common_headers(self): + self.send_header("content-type", "application/json;charset=utf-8") + self.send_header("connection", "close") + self.end_headers() + + invalid_auth = { + "ok": False, + "error": "invalid_auth", + } + + not_found = { + "ok": False, + "error": "test_data_not_found", + } + + def _handle(self): + try: + # put_nowait is common between Queue & asyncio.Queue, it does not need to be awaited + self.server.queue.put_nowait(self.path) + if self.is_valid_token() and self.is_valid_user_agent(): + parsed_path = urlparse(self.path) + + len_header = self.headers.get("Content-Length") or 0 + content_len = int(len_header) + post_body = self.rfile.read(content_len) + request_body = None + if post_body: + try: + post_body = post_body.decode("utf-8") + if post_body.startswith("{"): + request_body = json.loads(post_body) + else: + request_body = {k: v[0] for k, v in parse_qs(post_body).items()} + except UnicodeDecodeError: + pass + else: + if parsed_path and parsed_path.query: + request_body = {k: v[0] for k, v in parse_qs(parsed_path.query).items()} + + body = {"ok": False, "error": "internal_error"} + if self.path == "/auth.test": + body = { + "ok": True, + "url": "https://xyz.slack.com/", + "team": "Testing Workspace", + "user": "bot-user", + "team_id": "T111", + "user_id": "W11", + "bot_id": "B111", + "enterprise_id": "E111", + "is_enterprise_install": False, + } + if self.path == "/apps.connections.open": + body = { + "ok": True, + "url": "ws://0.0.0.0:3001/link", + } + if self.path == "/api.test" and request_body: + body = {"ok": True, "args": request_body} + else: + body = self.invalid_auth + + if not body: + body = self.not_found + + self.send_response(HTTPStatus.OK) + self.set_common_headers() + self.wfile.write(json.dumps(body).encode("utf-8")) + self.wfile.close() + + except Exception as e: + self.logger.error(str(e), exc_info=True) + raise + + def do_GET(self): + self._handle() + + def do_POST(self): + self._handle() + + def do_CONNECT(self): + self.wfile.write("HTTP/1.1 200 Connection established\r\n\r\n".encode("utf-8")) + self.wfile.close() diff --git a/tests/slack_sdk/socket_mode/test_builtin.py b/tests/slack_sdk/socket_mode/test_builtin.py new file mode 100644 index 000000000..a1780a7e0 --- /dev/null +++ b/tests/slack_sdk/socket_mode/test_builtin.py @@ -0,0 +1,151 @@ +import logging +import socket +import ssl +import time +import unittest +from unittest.mock import sentinel +from threading import Thread + +from slack_sdk import WebClient +from slack_sdk.socket_mode import SocketModeClient +from slack_sdk.socket_mode.builtin.connection import Connection, ConnectionState +from slack_sdk.socket_mode.builtin.frame_header import FrameHeader +from slack_sdk.socket_mode.builtin.internals import ( + _generate_sec_websocket_key, + _to_readable_opcode, + _build_data_frame_for_sending, + _parse_connect_response, + _use_or_create_ssl_context, +) +from slack_sdk.web.legacy_client import LegacyWebClient +from .mock_web_api_handler import MockHandler +from tests.mock_web_api_server import setup_mock_web_api_server, cleanup_mock_web_api_server + + +class TestBuiltin(unittest.TestCase): + logger = logging.getLogger(__name__) + + def setUp(self): + setup_mock_web_api_server(self, MockHandler) + self.web_client = WebClient( + token="xoxb-api_test", + base_url="http://localhost:8888", + ) + + def tearDown(self): + cleanup_mock_web_api_server(self) + + # ---------------------------------- + # SocketModeClient + + def test_init_close(self): + on_message_listeners = [lambda message: None] + client = SocketModeClient(app_token="xapp-A111-222-xyz", on_message_listeners=on_message_listeners) + try: + self.assertIsNotNone(client) + self.assertFalse(client.is_connected()) + self.assertIsNone(client.session_id()) # not yet connected + finally: + client.close() + + def test_issue_new_wss_url(self): + client = SocketModeClient( + app_token="xapp-A111-222-xyz", + web_client=self.web_client, + ) + url = client.issue_new_wss_url() + self.assertTrue(url.startswith("ws://")) + + legacy_client = LegacyWebClient(token="xoxb-api_test", base_url="http://localhost:8888") + response = legacy_client.apps_connections_open(app_token="xapp-A111-222-xyz") + self.assertIsNotNone(response["url"]) + + def test_connect_to_new_endpoint(self): + client = SocketModeClient( + app_token="xapp-A111-222-xyz", + web_client=self.web_client, + ) + client.connect_to_new_endpoint() + self.assertFalse(client.is_connected()) + + def test_enqueue_message(self): + client = SocketModeClient( + app_token="xapp-A111-222-xyz", + web_client=self.web_client, + on_message_listeners=[lambda message: None], + ) + client.enqueue_message("hello") + client.process_message() + + client.enqueue_message( + """{"type":"hello","num_connections":1,"debug_info":{"host":"applink-111-222","build_number":10,"approximate_connection_time":18060},"connection_info":{"app_id":"A111"}}""" + ) + client.process_message() + + def test_client_with_ssl(self): + self.web_client.ssl = sentinel.ssl_context + client = SocketModeClient( + app_token="xapp-A111-222-xyz", + web_client=self.web_client, + ) + self.assertEqual(client.web_client.ssl, sentinel.ssl_context) + + # ---------------------------------- + # Connection + + def test_connection_init(self): + conn = Connection(url="0.0.0.0", logger=self.logger) + self.assertFalse(conn.is_active()) + state = ConnectionState() + + def run(): + conn.run_until_completion(state) + + t = Thread(target=run) + t.start() + + time.sleep(0.5) + state.terminated = True + + self.assertIsNotNone(conn) + + # ---------------------------------- + # FrameHeader + + def test_frame_header(self): + fh = FrameHeader(0x1) + self.assertIsNotNone(fh) + + # ---------------------------------- + # internals + + def test_generate_sec_websocket_key(self): + key = _generate_sec_websocket_key() + self.assertEqual(len(key), 24) + + def test_to_readable_opcode(self): + res = _to_readable_opcode(FrameHeader.OPCODE_PONG) + self.assertEqual(res, "pong") + + def test_build_data_frame_for_sending(self): + res = _build_data_frame_for_sending("hello!", FrameHeader.OPCODE_TEXT) + self.assertIsNotNone(res) + + def test_parse_connect_response(self): + sock = socket.socket() + try: + sock.connect(("localhost", 8888)) + sock.send("""CONNECT localhost:8888 HTTP/1.0\r\n\r\n""".encode("utf-8")) + status, text = _parse_connect_response(sock) + self.assertEqual(status, 200) + self.assertEqual(text, "HTTP/1.1 200 Connection established") + finally: + sock.close() + + def test_creating_ssl_context(self): + ssl_context = _use_or_create_ssl_context(None) + self.assertTrue(isinstance(ssl_context, ssl.SSLContext)) + + def test_using_supplied_ssl_context(self): + ssl_context = _use_or_create_ssl_context(sentinel.ssl_context) + self.assertEqual(ssl_context, sentinel.ssl_context) diff --git a/tests/slack_sdk/socket_mode/test_builtin_message_parser.py b/tests/slack_sdk/socket_mode/test_builtin_message_parser.py new file mode 100644 index 000000000..096bd7d04 --- /dev/null +++ b/tests/slack_sdk/socket_mode/test_builtin_message_parser.py @@ -0,0 +1,48 @@ +import logging +import unittest +from typing import Optional, List, Tuple + +from slack_sdk.socket_mode.builtin.frame_header import FrameHeader +from slack_sdk.socket_mode.builtin.internals import _fetch_messages + + +class TestBuiltin(unittest.TestCase): + logger = logging.getLogger(__name__) + + def test_parse_test_server_response_1(self): + def receive(): + return b"\n\x8a7230b6da2-4280-46b3-9ab0-986d4093c5a1:1610196543.3950982\x8a6230b6da2-4280-46b3-9ab0-986d4093c5a1:1610196543.395274" + + messages: List[Tuple[Optional[FrameHeader], bytes]] = _fetch_messages( + messages=[], + receive=receive, + logger=self.logger, + ) + self.assertEqual(len(messages), 3) + self.assertEqual(messages[0], (None, b"\n")) + self.assertEqual(messages[1][0].opcode, FrameHeader.OPCODE_PONG) + self.assertEqual(messages[1][1], b"230b6da2-4280-46b3-9ab0-986d4093c5a1:1610196543.3950982") + self.assertEqual(messages[2][0].opcode, FrameHeader.OPCODE_PONG) + self.assertEqual(messages[2][1], b"230b6da2-4280-46b3-9ab0-986d4093c5a1:1610196543.395274") + + def test_parse_test_server_response_2(self): + socket_data = [ + b'\x81\x03foo\x81~\x03@{"envelope_id":"08cfc559-d933-402e-a5c1-79e135afaae4","payload":{"token":"xxx","team_id":"T111","api_app_id":"A111","event":{"client_msg_id":"c9b466b5-845c-49c6-a371-57ae44359bf1","type":"message","text":"<@W111>","user":"U111","ts":"1610197986.000300","team":"T111","blocks":[{"type":"rich_text","block_id":"1HBPc","elements":[{"type":"rich_text_section","elements":[{"type":"user","user_id":"U111"}]}]}],"channel":"C111","event_ts":"1610197986.000300","channel_type":"channel"},"type":"event_callback","event_id":"Ev111","event_time":1610197986,"authorizations":[{"enterprise_id":null,"team_id":"T111","user_id":"U111","is_bot":true,"is_enterprise_install":false}],"is_ext_shared_channel":false,"event_context":"1-message-T111-C111"},"type":"events_api","accepts_response_payload":false,"retry_attempt":1,"retry_reason":"timeout"}\x81\x03bar\x81~\x01\x83{"envelope_id":"57d6a792-4d35-4d0b-b6aa-3361493e1caf","payload":{"type":"shortcut","token":"xxx","action_ts":"1610198080.300836","team":{"id":"T111","domain":"seratch"},"user', + b'":{"id":"U111","username":"seratch","team_id":"T111"},"is_enterprise_install":false,"enterprise":null,"callback_id":"do-something","trigger_id":"111.222.xxx"},"type":"interactive","accepts_response_payload":false}\x81\x03baz', + ] + + def receive(): + if len(socket_data) > 0: + return socket_data.pop(0) + else: + return bytes() + + messages: List[Tuple[Optional[FrameHeader], bytes]] = _fetch_messages( + messages=[], + receive=receive, + logger=self.logger, + ) + self.assertEqual(len(messages), 5) + self.assertEqual(messages[0][1], b"foo") + self.assertEqual(messages[2][1], b"bar") + self.assertEqual(messages[4][1], b"baz") diff --git a/tests/slack_sdk/socket_mode/test_interactions_builtin.py b/tests/slack_sdk/socket_mode/test_interactions_builtin.py new file mode 100644 index 000000000..4ff576fcf --- /dev/null +++ b/tests/slack_sdk/socket_mode/test_interactions_builtin.py @@ -0,0 +1,149 @@ +import logging +import time +import unittest +from random import randint + +import pytest + +from slack_sdk.errors import SlackClientConfigurationError, SlackClientNotConnectedError +from slack_sdk.socket_mode.request import SocketModeRequest + +from slack_sdk.socket_mode.client import BaseSocketModeClient + +from slack_sdk import WebClient +from slack_sdk.socket_mode import SocketModeClient +from tests.slack_sdk.socket_mode.mock_socket_mode_server import ( + start_socket_mode_server, + socket_mode_envelopes, + socket_mode_hello_message, + stop_socket_mode_server, +) +from tests.slack_sdk.socket_mode.mock_web_api_handler import MockHandler +from tests.mock_web_api_server import setup_mock_web_api_server, cleanup_mock_web_api_server + +import sys + + +class TestInteractionsBuiltin(unittest.TestCase): + logger = logging.getLogger(__name__) + + def setUp(self): + setup_mock_web_api_server(self, MockHandler) + self.web_client = WebClient( + token="xoxb-api_test", + base_url="http://localhost:8888", + ) + start_socket_mode_server(self, 3011) + + def tearDown(self): + cleanup_mock_web_api_server(self) + stop_socket_mode_server(self) + + def test_buffer_size_validation(self): + try: + SocketModeClient(app_token="xapp-A111-222-xyz", receive_buffer_size=1) + self.fail("SlackClientConfigurationError is expected here") + except SlackClientConfigurationError: + pass + + def test_interactions(self): + default_recursion_limit = sys.getrecursionlimit() # will restore later + # This built-in WebSocket client internally has recursive method calls of _fetch_messages method. + # In this test, the method calls can result in the following error when giving a quite small buffer size. + # RecursionError: maximum recursion depth exceeded while calling a Python object + # (the default recursion depth in Python is 1500) + # Since the default buffer size is set to 1024, and it's enough to prevent the same situation happening, + # we believe that the same situation never happens in the production usage. + sys.setrecursionlimit(10000) + + try: + buffer_size_list = [1024, 9000, 35, 49] + list([randint(16, 128) for _ in range(10)]) + for buffer_size in buffer_size_list: + self.reset_server_state() + + received_messages = [] + received_socket_mode_requests = [] + + def message_handler(message): + self.logger.info(f"Raw Message: {message}") + time.sleep(randint(50, 200) / 1000) + received_messages.append(message) + + def socket_mode_request_handler(client: BaseSocketModeClient, request: SocketModeRequest): + self.logger.info(f"Socket Mode Request: {request}") + time.sleep(randint(50, 200) / 1000) + received_socket_mode_requests.append(request) + + self.logger.info(f"Started testing with buffer size: {buffer_size}") + client = SocketModeClient( + app_token="xapp-A111-222-xyz", + web_client=self.web_client, + on_message_listeners=[message_handler], + receive_buffer_size=buffer_size, + auto_reconnect_enabled=False, + trace_enabled=True, + ) + try: + client.socket_mode_request_listeners.append(socket_mode_request_handler) + client.wss_uri = "ws://0.0.0.0:3011/link" + client.connect() + self.assertTrue(client.is_connected()) + time.sleep(2) # wait for the message receiver + + repeat = 2 + for _ in range(repeat): + client.send_message("foo") + client.send_message("bar") + client.send_message("baz") + self.assertTrue(client.is_connected()) + + expected = socket_mode_envelopes + [socket_mode_hello_message] + ["foo", "bar", "baz"] * repeat + expected.sort() + + count = 0 + while count < 5 and len(received_messages) < len(expected): + time.sleep(0.1) + self.logger.debug(f"Received messages: {len(received_messages)}") + count += 0.1 + + received_messages.sort() + self.assertEqual(len(received_messages), len(expected)) + self.assertEqual(received_messages, expected) + + self.assertEqual(len(socket_mode_envelopes), len(received_socket_mode_requests)) + finally: + pass + # client.close() + self.logger.info(f"Passed with buffer size: {buffer_size}") + + finally: + # Restore the default value + sys.setrecursionlimit(default_recursion_limit) + client.close() + + self.logger.info(f"Passed with buffer size: {buffer_size_list}") + + def test_send_message_while_disconnection(self): + try: + self.reset_server_state() + client = SocketModeClient( + app_token="xapp-A111-222-xyz", + web_client=self.web_client, + auto_reconnect_enabled=False, + trace_enabled=True, + ) + client.wss_uri = "ws://0.0.0.0:3011/link" + client.connect() + time.sleep(1) # wait for the connection + client.send_message("foo") + + client.disconnect() + time.sleep(1) # wait for the connection + with pytest.raises(SlackClientNotConnectedError): + client.send_message("foo") + + client.connect() + time.sleep(1) # wait for the connection + client.send_message("foo") + finally: + client.close() diff --git a/tests/slack_sdk/socket_mode/test_interactions_websocket_client.py b/tests/slack_sdk/socket_mode/test_interactions_websocket_client.py new file mode 100644 index 000000000..c9869af46 --- /dev/null +++ b/tests/slack_sdk/socket_mode/test_interactions_websocket_client.py @@ -0,0 +1,117 @@ +import logging +import time +import unittest +from random import randint + +from websocket import WebSocketException + +from slack_sdk.socket_mode.client import BaseSocketModeClient + +from slack_sdk.socket_mode.request import SocketModeRequest + +from slack_sdk import WebClient +from slack_sdk.socket_mode.websocket_client import SocketModeClient +from tests.slack_sdk.socket_mode.mock_socket_mode_server import ( + start_socket_mode_server, + socket_mode_envelopes, + socket_mode_hello_message, + stop_socket_mode_server, +) +from tests.slack_sdk.socket_mode.mock_web_api_handler import MockHandler +from tests.mock_web_api_server import setup_mock_web_api_server, cleanup_mock_web_api_server + + +class TestInteractionsWebSocketClient(unittest.TestCase): + logger = logging.getLogger(__name__) + + def setUp(self): + setup_mock_web_api_server(self, MockHandler) + self.web_client = WebClient( + token="xoxb-api_test", + base_url="http://localhost:8888", + ) + start_socket_mode_server(self, 3012) + + def tearDown(self): + cleanup_mock_web_api_server(self) + stop_socket_mode_server(self) + + def test_interactions(self): + received_messages = [] + received_socket_mode_requests = [] + + def message_handler(ws_app, message): + self.logger.info(f"Raw Message: {message}") + time.sleep(randint(50, 200) / 1000) + received_messages.append(message) + + def socket_mode_request_handler(client: BaseSocketModeClient, request: SocketModeRequest): + self.logger.info(f"Socket Mode Request: {request}") + time.sleep(randint(50, 200) / 1000) + received_socket_mode_requests.append(request) + + client = SocketModeClient( + app_token="xapp-A111-222-xyz", + web_client=self.web_client, + on_message_listeners=[message_handler], + auto_reconnect_enabled=False, + trace_enabled=True, + ) + client.socket_mode_request_listeners.append(socket_mode_request_handler) + + try: + client.wss_uri = "ws://0.0.0.0:3012/link" + client.connect() + time.sleep(1) # wait for the message receiver + self.assertTrue(client.is_connected()) + + for _ in range(10): + client.send_message("foo") + client.send_message("bar") + client.send_message("baz") + self.assertTrue(client.is_connected()) + + expected = socket_mode_envelopes + [socket_mode_hello_message] + ["foo", "bar", "baz"] * 10 + expected.sort() + + count = 0 + while count < 10 and ( + len(received_messages) < len(expected) or len(received_socket_mode_requests) < len(socket_mode_envelopes) + ): + time.sleep(0.2) + count += 0.2 + + received_messages.sort() + self.assertEqual(received_messages, expected) + + self.assertEqual(len(socket_mode_envelopes), len(received_socket_mode_requests)) + finally: + client.close() + + def test_send_message_while_disconnection(self): + try: + client = SocketModeClient( + app_token="xapp-A111-222-xyz", + web_client=self.web_client, + auto_reconnect_enabled=False, + trace_enabled=True, + ) + client.wss_uri = "ws://0.0.0.0:3012/link" + client.connect() + time.sleep(1) # wait for the connection + client.send_message("foo") + + client.disconnect() + time.sleep(1) # wait for the connection + try: + client.send_message("foo") + # TODO: The client may not raise an exception here + # self.fail("WebSocketException is expected here") + except WebSocketException as _: + pass + + client.connect() + time.sleep(1) # wait for the connection + client.send_message("foo") + finally: + client.close() diff --git a/tests/slack_sdk/socket_mode/test_request.py b/tests/slack_sdk/socket_mode/test_request.py new file mode 100644 index 000000000..cf451571d --- /dev/null +++ b/tests/slack_sdk/socket_mode/test_request.py @@ -0,0 +1,20 @@ +import json +import unittest + +from slack_sdk.socket_mode.request import SocketModeRequest + + +class TestRequest(unittest.TestCase): + def setUp(self): + pass + + def tearDown(self): + pass + + def test(self): + body = json.loads( + """{"envelope_id":"1d3c79ab-0ffb-41f3-a080-d19e85f53649","payload":{"token":"xxx","team_id":"T111","team_domain":"xxx","channel_id":"C03E94MKU","channel_name":"random","user_id":"U111","user_name":"seratch","command":"/hello-socket-mode","text":"","api_app_id":"A111","response_url":"https://hooks.slack.com/commands/T111/111/xxx","trigger_id":"111.222.xxx"},"type":"slash_commands","accepts_response_payload":true}""" + ) + req = SocketModeRequest.from_dict(body) + self.assertIsNotNone(req) + self.assertEqual(req.envelope_id, "1d3c79ab-0ffb-41f3-a080-d19e85f53649") diff --git a/tests/slack_sdk/socket_mode/test_response.py b/tests/slack_sdk/socket_mode/test_response.py new file mode 100644 index 000000000..fa5af71bc --- /dev/null +++ b/tests/slack_sdk/socket_mode/test_response.py @@ -0,0 +1,24 @@ +import json +import unittest + +from slack_sdk.socket_mode.response import SocketModeResponse + + +class TestResponse(unittest.TestCase): + def setUp(self): + pass + + def tearDown(self): + pass + + def test_parser(self): + text = """{"envelope_id":"1d3c79ab-0ffb-41f3-a080-d19e85f53649","payload":{"text":"Thanks!"}}""" + body = json.loads(text) + response = SocketModeResponse(body["envelope_id"], body["payload"]) + self.assertIsNotNone(response) + self.assertEqual(response.envelope_id, "1d3c79ab-0ffb-41f3-a080-d19e85f53649") + self.assertEqual(response.payload.get("text"), "Thanks!") + + def test_to_dict(self): + response = SocketModeResponse(envelope_id="xxx", payload={"text": "hi"}) + self.assertDictEqual(response.to_dict(), {"envelope_id": "xxx", "payload": {"text": "hi"}}) diff --git a/tests/slack_sdk/socket_mode/test_websocket_client.py b/tests/slack_sdk/socket_mode/test_websocket_client.py new file mode 100644 index 000000000..4530f7ddc --- /dev/null +++ b/tests/slack_sdk/socket_mode/test_websocket_client.py @@ -0,0 +1,19 @@ +import unittest + +from slack_sdk.socket_mode.websocket_client import SocketModeClient + + +class TestWebSocketClientLibrary(unittest.TestCase): + def setUp(self): + pass + + def tearDown(self): + pass + + def test_init_close(self): + client = SocketModeClient(app_token="xapp-A111-222-xyz") + try: + self.assertIsNotNone(client) + self.assertFalse(client.is_connected()) + finally: + client.close() diff --git a/tests/slack_sdk/web/__init__.py b/tests/slack_sdk/web/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/slack_sdk/web/mock_web_api_handler.py b/tests/slack_sdk/web/mock_web_api_handler.py new file mode 100644 index 000000000..12a487e05 --- /dev/null +++ b/tests/slack_sdk/web/mock_web_api_handler.py @@ -0,0 +1,294 @@ +import asyncio +import json +import logging +from queue import Queue +import re +import threading +import time +from http import HTTPStatus +from http.server import HTTPServer, SimpleHTTPRequestHandler +from typing import Type, Union +from unittest import TestCase +from urllib.parse import urlparse, parse_qs + + +class MockHandler(SimpleHTTPRequestHandler): + protocol_version = "HTTP/1.1" + default_request_version = "HTTP/1.1" + logger = logging.getLogger(__name__) + + pattern_for_language = re.compile("python/(\\S+)", re.IGNORECASE) + pattern_for_package_identifier = re.compile("slackclient/(\\S+)") + + html_response_body = '\n\n404 Not Found\n\n

Not Found

\n

The requested URL /api/team.info was not found on this server.

\n\n' + + error_html_response_body = '\n\n\n\t\n\tServer Error | Slack\n\t\n\t\n\n\n\t\n\t
\n\t\t
\n\t\t\t

\n\t\t\t\t\n\t\t\t\tServer Error\n\t\t\t

\n\t\t\t
\n\t\t\t\t

It seems like there’s a problem connecting to our servers, and we’re investigating the issue.

\n\t\t\t\t

Please check our Status page for updates.

\n\t\t\t
\n\t\t
\n\t
\n\t\n\n' + + state = {"ratelimited_count": 0, "fatal_error_count": 0, "server_error_count": 0} + + def is_valid_user_agent(self): + user_agent = self.headers["User-Agent"] + return self.pattern_for_language.search(user_agent) and self.pattern_for_package_identifier.search(user_agent) + + def is_valid_token(self): + if self.path.startswith("oauth"): + return True + return "Authorization" in self.headers and str(self.headers["Authorization"]).startswith("Bearer xoxb-") + + def set_common_headers(self): + self.send_header("content-type", "application/json;charset=utf-8") + self.send_header("connection", "close") + self.end_headers() + + invalid_auth = { + "ok": False, + "error": "invalid_auth", + } + + not_found = { + "ok": False, + "error": "test_data_not_found", + } + + token_refresh = { + "ok": True, + "app_id": "A111", + "authed_user": { + "id": "W111", + "scope": "search:read", + "access_token": "xoxe.xoxp-1-xxx", + "token_type": "user", + "refresh_token": "xoxe-1-xxx", + "expires_in": 43200, + }, + "scope": "app_mentions:read,chat:write,commands", + "token_type": "bot", + "access_token": "xoxe.xoxb-1-yyy", + "bot_user_id": "UB111", + "refresh_token": "xoxe-1-yyy", + "expires_in": 43201, + "team": {"id": "T111", "name": "Testing Workspace"}, + "enterprise": {"id": "E111", "name": "Sandbox Org"}, + "is_enterprise_install": False, + } + + def _handle(self): + try: + # put_nowait is common between Queue & asyncio.Queue, it does not need to be awaited + self.server.queue.put_nowait(self.path) + + if self.path in {"/oauth.access", "/oauth.v2.access"}: + self.send_response(200) + self.set_common_headers() + if self.headers["authorization"] == "Basic MTExLjIyMjpzZWNyZXQ=": + self.wfile.write("""{"ok":true}""".encode("utf-8")) + return + elif self.headers["authorization"] == "Basic MTExLjIyMjp0b2tlbl9yb3RhdGlvbl9zZWNyZXQ=": + self.wfile.write(json.dumps(self.token_refresh).encode("utf-8")) + return + else: + self.wfile.write("""{"ok":false, "error":"invalid"}""".encode("utf-8")) + return + + header = self.headers["Authorization"] + if header is not None and ("xoxb-" in header or "xoxp-" in header): + pattern = "" + xoxb = str(header).split("xoxb-", 1) + if len(xoxb) > 1: + pattern = xoxb[1] + else: + xoxp = str(header).split("xoxp-", 1) + pattern = xoxp[1] + + if "remote_disconnected" in pattern: + # http.client.RemoteDisconnected + self.finish() + return + + if pattern == "ratelimited" or (pattern == "ratelimited_only_once" and self.state["ratelimited_count"] == 0): + self.state["ratelimited_count"] += 1 + self.send_response(429) + self.send_header("retry-after", 1) + self.send_header("content-type", "application/json;charset=utf-8") + self.send_header("connection", "close") + self.end_headers() + self.wfile.write("""{"ok":false,"error":"ratelimited"}""".encode("utf-8")) + self.wfile.close() + return + + if pattern == "fatal_error" or (pattern == "fatal_error_only_once" and self.state["fatal_error_count"] == 0): + self.state["fatal_error_count"] += 1 + self.send_response(200) + self.set_common_headers() + self.wfile.write("""{"ok":false,"error":"fatal_error"}""".encode("utf-8")) + self.wfile.close() + return + + if self.is_valid_token() and self.is_valid_user_agent(): + parsed_path = urlparse(self.path) + + len_header = self.headers.get("Content-Length") or 0 + content_len = int(len_header) + post_body = self.rfile.read(content_len) + request_body = None + if post_body: + try: + post_body = post_body.decode("utf-8") + if post_body.startswith("{"): + request_body = json.loads(post_body) + else: + request_body = {k: v[0] for k, v in parse_qs(post_body).items()} + except UnicodeDecodeError: + pass + else: + if parsed_path and parsed_path.query: + request_body = {k: v[0] for k, v in parse_qs(parsed_path.query).items()} + + header = self.headers["authorization"] + pattern = str(header).split("xoxb-", 1)[1] + if pattern.isnumeric(): + self.send_response(int(pattern)) + self.set_common_headers() + self.wfile.write("""{"ok":false}""".encode("utf-8")) + return + + if pattern == "timeout": + time.sleep(3) + self.send_response(200) + self.set_common_headers() + self.wfile.write("""{"ok":true}""".encode("utf-8")) + self.wfile.close() + return + + if pattern == "html_response": + self.send_response(404) + self.send_header("content-type", "text/html; charset=utf-8") + self.end_headers() + self.wfile.write(self.html_response_body.encode("utf-8")) + self.wfile.close() + return + + if pattern == "error_html_response": + self.send_response(503) + # no charset here is intentional for testing + self.send_header("content-type", "text/html") + self.send_header("connection", "close") + self.end_headers() + self.wfile.write(self.error_html_response_body.encode("utf-8")) + self.wfile.close() + return + if pattern == "server_error_only_once": + if self.state["server_error_count"] == 0: + self.state["server_error_count"] += 1 + self.send_response(500) + # no charset here is intentional for testing + self.send_header("content-type", "text/html") + self.send_header("connection", "close") + self.end_headers() + self.wfile.write(self.error_html_response_body.encode("utf-8")) + self.wfile.close() + return + else: + self.send_response(200) + self.set_common_headers() + self.wfile.write("""{"ok":true}""".encode("utf-8")) + self.wfile.close() + return + + if pattern.startswith("user-agent"): + elements = pattern.split(" ") + prefix, suffix = elements[1], elements[-1] + ua: str = self.headers["User-Agent"] + if ua.startswith(prefix) and ua.endswith(suffix): + self.send_response(200) + self.set_common_headers() + self.wfile.write("""{"ok":true}""".encode("utf-8")) + self.wfile.close() + return + else: + self.send_response(400) + self.set_common_headers() + self.wfile.write("""{"ok":false, "error":"invalid_user_agent"}""".encode("utf-8")) + self.wfile.close() + return + + if request_body and "cursor" in request_body: + page = request_body["cursor"] + pattern = f"{pattern}_{page}" + if pattern == "coverage": + if self.path.startswith("/calls."): + for k, v in request_body.items(): + if k == "users": + users = json.loads(v) + for u in users: + if "slack_id" not in u and "external_id" not in u: + raise Exception(f"User ({u}) is invalid value") + else: + ids = ["channels", "users", "channel_ids"] + if request_body: + for k, v in request_body.items(): + if k in ids: + if not re.compile(r"^[^,\[\]]+?,[^,\[\]]+$").match(v): + raise Exception(f"The parameter {k} is not a comma-separated string value: {v}") + body = {"ok": True, "method": parsed_path.path.replace("/", "")} + else: + with open(f"tests/slack_sdk_fixture/web_response_{pattern}.json") as file: + body = json.load(file) + + if self.path == "/api.test" and request_body: + body["args"] = request_body + + else: + body = self.invalid_auth + + if not body: + body = self.not_found + + self.send_response(HTTPStatus.OK) + self.set_common_headers() + self.wfile.write(json.dumps(body).encode("utf-8")) + self.wfile.close() + + except Exception as e: + self.logger.error(str(e), exc_info=True) + raise + + def do_GET(self): + self._handle() + + def do_POST(self): + self._handle() + + +class MockServerThread(threading.Thread): + def __init__( + self, queue: Union[Queue, asyncio.Queue], test: TestCase, handler: Type[SimpleHTTPRequestHandler] = MockHandler + ): + threading.Thread.__init__(self) + self.handler = handler + self.test = test + self.queue = queue + + def run(self): + self.server = HTTPServer(("localhost", 8888), self.handler) + self.server.queue = self.queue + self.test.server_url = "http://localhost:8888" + self.test.host, self.test.port = self.server.socket.getsockname() + self.test.server_started.set() # threading.Event() + + self.test = None + try: + self.server.serve_forever(0.05) + finally: + self.server.server_close() + + def stop(self): + with self.server.queue.mutex: + del self.server.queue + self.server.shutdown() + self.join() + + def stop_unsafe(self): + del self.server.queue + self.server.shutdown() + self.join() diff --git a/tests/slack_sdk/web/mock_web_api_http_retry_handler.py b/tests/slack_sdk/web/mock_web_api_http_retry_handler.py new file mode 100644 index 000000000..d22eaa38b --- /dev/null +++ b/tests/slack_sdk/web/mock_web_api_http_retry_handler.py @@ -0,0 +1,67 @@ +import logging +import time +from http import HTTPStatus +from http.server import SimpleHTTPRequestHandler + + +class MockHandler(SimpleHTTPRequestHandler): + protocol_version = "HTTP/1.1" + default_request_version = "HTTP/1.1" + logger = logging.getLogger(__name__) + state = {"request_count": 0} + + def set_common_headers(self): + self.send_header("content-type", "application/json;charset=utf-8") + self.send_header("connection", "close") + self.end_headers() + + success_response = {"ok": True} + + def _handle(self): + self.state["request_count"] += 1 + try: + # put_nowait is common between Queue & asyncio.Queue, it does not need to be awaited + self.server.queue.put_nowait(self.path) + header = self.headers["authorization"] + pattern = str(header).split("xoxb-", 1)[1] + + if self.state["request_count"] % 2 == 1: + if "remote_disconnected" in pattern: + # http.client.RemoteDisconnected + self.finish() + return + + if pattern.isnumeric(): + self.send_response(int(pattern)) + self.set_common_headers() + self.wfile.write("""{"ok":false}""".encode("utf-8")) + return + if pattern == "ratelimited": + self.send_response(429) + self.send_header("retry-after", 1) + self.set_common_headers() + self.wfile.write("""{"ok":false,"error":"ratelimited"}""".encode("utf-8")) + self.wfile.close() + return + + if pattern == "timeout": + time.sleep(2) + self.send_response(200) + self.wfile.write("""{"ok":true}""".encode("utf-8")) + self.wfile.close() + return + + self.send_response(HTTPStatus.OK) + self.set_common_headers() + self.wfile.write("""{"ok":true}""".encode("utf-8")) + self.wfile.close() + + except Exception as e: + self.logger.error(str(e), exc_info=True) + raise + + def do_GET(self): + self._handle() + + def do_POST(self): + self._handle() diff --git a/tests/slack_sdk/web/test_chat_stream.py b/tests/slack_sdk/web/test_chat_stream.py new file mode 100644 index 000000000..75c13c8c2 --- /dev/null +++ b/tests/slack_sdk/web/test_chat_stream.py @@ -0,0 +1,188 @@ +import json +import unittest +from urllib.parse import parse_qs, urlparse + +from slack_sdk import WebClient +from slack_sdk.errors import SlackRequestError +from slack_sdk.models.blocks.basic_components import FeedbackButtonObject +from slack_sdk.models.blocks.block_elements import FeedbackButtonsElement, IconButtonElement +from slack_sdk.models.blocks.blocks import ContextActionsBlock +from tests.mock_web_api_server import cleanup_mock_web_api_server, setup_mock_web_api_server +from tests.slack_sdk.web.mock_web_api_handler import MockHandler + + +class ChatStreamMockHandler(MockHandler): + """Extended mock handler that captures request bodies for chat stream methods""" + + def _handle(self): + try: + # put_nowait is common between Queue & asyncio.Queue, it does not need to be awaited + self.server.queue.put_nowait(self.path) + + # Standard auth and validation from parent + if self.is_valid_token() and self.is_valid_user_agent(): + token = self.headers["authorization"].split(" ")[1] + parsed_path = urlparse(self.path) + len_header = self.headers.get("Content-Length") or 0 + content_len = int(len_header) + post_body = self.rfile.read(content_len) + request_body = None + if post_body: + try: + post_body = post_body.decode("utf-8") + if post_body.startswith("{"): + request_body = json.loads(post_body) + else: + request_body = {k: v[0] for k, v in parse_qs(post_body).items()} + except UnicodeDecodeError: + pass + else: + if parsed_path and parsed_path.query: + request_body = {k: v[0] for k, v in parse_qs(parsed_path.query).items()} + + # Store request body for chat stream endpoints + if self.path in ["/chat.startStream", "/chat.appendStream", "/chat.stopStream"] and request_body: + if not hasattr(self.server, "chat_stream_requests"): + self.server.chat_stream_requests = {} + self.server.chat_stream_requests[self.path] = { + "token": token, + **request_body, + } + + # Load response file + pattern = str(token).split("xoxb-", 1)[1] + with open(f"tests/slack_sdk_fixture/web_response_{pattern}.json") as file: + body = json.load(file) + + else: + body = self.invalid_auth + + if not body: + body = self.not_found + + self.send_response(200) + self.set_common_headers() + self.wfile.write(json.dumps(body).encode("utf-8")) + self.wfile.close() + + except Exception as e: + self.logger.error(str(e), exc_info=True) + raise + + +class TestChatStream(unittest.TestCase): + def setUp(self): + setup_mock_web_api_server(self, ChatStreamMockHandler) + self.client = WebClient( + token="xoxb-chat_stream_test", + base_url="http://localhost:8888", + ) + + def tearDown(self): + cleanup_mock_web_api_server(self) + + def test_streams_a_short_message(self): + streamer = self.client.chat_stream( + channel="C0123456789", + thread_ts="123.000", + recipient_team_id="T0123456789", + recipient_user_id="U0123456789", + ) + streamer.append(markdown_text="nice!") + streamer.stop() + + self.assertEqual(self.received_requests.get("/chat.startStream", 0), 1) + self.assertEqual(self.received_requests.get("/chat.appendStream", 0), 0) + self.assertEqual(self.received_requests.get("/chat.stopStream", 0), 1) + + if hasattr(self.thread.server, "chat_stream_requests"): + start_request = self.thread.server.chat_stream_requests.get("/chat.startStream", {}) + self.assertEqual(start_request.get("channel"), "C0123456789") + self.assertEqual(start_request.get("thread_ts"), "123.000") + self.assertEqual(start_request.get("recipient_team_id"), "T0123456789") + self.assertEqual(start_request.get("recipient_user_id"), "U0123456789") + + stop_request = self.thread.server.chat_stream_requests.get("/chat.stopStream", {}) + self.assertEqual(stop_request.get("channel"), "C0123456789") + self.assertEqual(stop_request.get("ts"), "123.123") + self.assertEqual(stop_request.get("markdown_text"), "nice!") + + def test_streams_a_long_message(self): + streamer = self.client.chat_stream( + buffer_size=5, + channel="C0123456789", + recipient_team_id="T0123456789", + recipient_user_id="U0123456789", + thread_ts="123.000", + ) + streamer.append(markdown_text="**this messag") + streamer.append(markdown_text="e is", token="xoxb-chat_stream_test_token1") + streamer.append(markdown_text=" bold!") + streamer.append(markdown_text="*") + streamer.stop( + blocks=[ + ContextActionsBlock( + elements=[ + FeedbackButtonsElement( + positive_button=FeedbackButtonObject(text="good", value="+1"), + negative_button=FeedbackButtonObject(text="bad", value="-1"), + ), + IconButtonElement( + icon="trash", + text="delete", + ), + ], + ) + ], + markdown_text="*", + token="xoxb-chat_stream_test_token2", + ) + + self.assertEqual(self.received_requests.get("/chat.startStream", 0), 1) + self.assertEqual(self.received_requests.get("/chat.appendStream", 0), 1) + self.assertEqual(self.received_requests.get("/chat.stopStream", 0), 1) + + if hasattr(self.thread.server, "chat_stream_requests"): + start_request = self.thread.server.chat_stream_requests.get("/chat.startStream", {}) + self.assertEqual(start_request.get("channel"), "C0123456789") + self.assertEqual(start_request.get("thread_ts"), "123.000") + self.assertEqual(start_request.get("markdown_text"), "**this messag") + self.assertEqual(start_request.get("recipient_team_id"), "T0123456789") + self.assertEqual(start_request.get("recipient_user_id"), "U0123456789") + + append_request = self.thread.server.chat_stream_requests.get("/chat.appendStream", {}) + self.assertEqual(append_request.get("channel"), "C0123456789") + self.assertEqual(append_request.get("markdown_text"), "e is bold!") + self.assertEqual(append_request.get("token"), "xoxb-chat_stream_test_token1") + self.assertEqual(append_request.get("ts"), "123.123") + + stop_request = self.thread.server.chat_stream_requests.get("/chat.stopStream", {}) + self.assertEqual( + json.dumps(stop_request.get("blocks")), + '[{"elements": [{"negative_button": {"text": {"emoji": true, "text": "bad", "type": "plain_text"}, "value": "-1"}, "positive_button": {"text": {"emoji": true, "text": "good", "type": "plain_text"}, "value": "+1"}, "type": "feedback_buttons"}, {"icon": "trash", "text": {"emoji": true, "text": "delete", "type": "plain_text"}, "type": "icon_button"}], "type": "context_actions"}]', + ) + self.assertEqual(stop_request.get("channel"), "C0123456789") + self.assertEqual(stop_request.get("markdown_text"), "**") + self.assertEqual(stop_request.get("token"), "xoxb-chat_stream_test_token2") + self.assertEqual(stop_request.get("ts"), "123.123") + + def test_streams_errors_when_appending_to_an_unstarted_stream(self): + streamer = self.client.chat_stream( + channel="C0123456789", + thread_ts="123.000", + token="xoxb-chat_stream_test_missing_ts", + ) + with self.assertRaisesRegex(SlackRequestError, r"^Failed to stop stream: stream not started$"): + streamer.stop() + + def test_streams_errors_when_appending_to_a_completed_stream(self): + streamer = self.client.chat_stream( + channel="C0123456789", + thread_ts="123.000", + ) + streamer.append(markdown_text="nice!") + streamer.stop() + with self.assertRaisesRegex(SlackRequestError, r"^Cannot append to stream: stream state is completed$"): + streamer.append(markdown_text="more...") + with self.assertRaisesRegex(SlackRequestError, r"^Cannot stop stream: stream state is completed$"): + streamer.stop() diff --git a/tests/slack_sdk/web/test_internal_utils.py b/tests/slack_sdk/web/test_internal_utils.py new file mode 100644 index 000000000..ac7704b30 --- /dev/null +++ b/tests/slack_sdk/web/test_internal_utils.py @@ -0,0 +1,136 @@ +import json +import unittest +from io import BytesIO +from pathlib import Path +from typing import Dict, Sequence, Union + +import pytest + +from slack_sdk.models.attachments import Attachment +from slack_sdk.models.blocks import Block, DividerBlock +from slack_sdk.web.internal_utils import ( + _build_unexpected_body_error_message, + _parse_web_class_objects, + _to_v2_file_upload_item, + _next_cursor_is_present, + _get_url, +) + + +class TestInternalUtils(unittest.TestCase): + def setUp(self): + pass + + def tearDown(self): + pass + + error_html_response_body = '\n\n\n\t\n\tServer Error | Slack\n\t\n\t\n\n\n\t\n\t
\n\t\t
\n\t\t\t

\n\t\t\t\t\n\t\t\t\tServer Error\n\t\t\t

\n\t\t\t
\n\t\t\t\t

It seems like there’s a problem connecting to our servers, and we’re investigating the issue.

\n\t\t\t\t

Please check our Status page for updates.

\n\t\t\t
\n\t\t
\n\t
\n\t\n\n' + + def test_build_unexpected_body_error_message(self): + message = _build_unexpected_body_error_message(self.error_html_response_body) + assert message.startswith( + """Received a response in a non-JSON format: """ + ) + + def test_can_parse_sequence_of_blocks(self): + for blocks in [ + [Block(block_id="42"), Block(block_id="24")], # list + (Block(block_id="42"), Block(block_id="24")), # tuple + ]: + kwargs = {"blocks": blocks} + _parse_web_class_objects(kwargs) + assert kwargs["blocks"] + for block in kwargs["blocks"]: + assert isinstance(block, Dict) + + def test_can_parse_sequence_of_attachments(self): + for attachments in [ + [Attachment(text="foo"), Attachment(text="bar")], # list + ( + Attachment(text="foo"), + Attachment(text="bar"), + ), # tuple + ]: + kwargs = {"attachments": attachments} + _parse_web_class_objects(kwargs) + assert kwargs["attachments"] + for attachment in kwargs["attachments"]: + assert isinstance(attachment, Dict) + + def test_can_parse_str_blocks(self): + input = json.dumps([Block(block_id="42").to_dict(), Block(block_id="24").to_dict()]) + kwargs = {"blocks": input} + _parse_web_class_objects(kwargs) + assert isinstance(kwargs["blocks"], str) + assert input == kwargs["blocks"] + + def test_can_parse_str_attachments(self): + input = json.dumps([Attachment(text="foo").to_dict(), Attachment(text="bar").to_dict()]) + kwargs = {"attachments": input} + _parse_web_class_objects(kwargs) + assert isinstance(kwargs["attachments"], str) + assert input == kwargs["attachments"] + + def test_can_parse_user_auth_blocks(self): + kwargs = { + "channel": "C12345", + "ts": "1111.2222", + "unfurls": {}, + "user_auth_blocks": [DividerBlock(), DividerBlock()], + } + _parse_web_class_objects(kwargs) + assert isinstance(kwargs["user_auth_blocks"][0], dict) + + def test_files_upload_v2_issue_1356(self): + content_item = _to_v2_file_upload_item({"content": "test"}) + assert content_item.get("filename") == "Uploaded file" + + filepath_item = _to_v2_file_upload_item({"file": "tests/slack_sdk/web/test_internal_utils.py"}) + assert filepath_item.get("filename") == "test_internal_utils.py" + filepath_item = _to_v2_file_upload_item({"file": "tests/slack_sdk/web/test_internal_utils.py", "filename": "foo.py"}) + assert filepath_item.get("filename") == "foo.py" + + file_bytes = "This is a test!".encode("utf-8") + file_bytes_item = _to_v2_file_upload_item({"file": file_bytes}) + assert file_bytes_item.get("filename") == "Uploaded file" + file_bytes_item = _to_v2_file_upload_item({"file": file_bytes, "filename": "foo.txt"}) + assert file_bytes_item.get("filename") == "foo.txt" + + file_io = BytesIO(file_bytes) + file_io_item = _to_v2_file_upload_item({"file": file_io}) + assert file_io_item.get("filename") == "Uploaded file" + file_io_item = _to_v2_file_upload_item({"file": file_io, "filename": "foo.txt"}) + assert file_io_item.get("filename") == "foo.txt" + + def test_to_v2_file_upload_item_can_accept_file_as_path(self): + filepath = "tests/slack_sdk/web/test_internal_utils.py" + upload_item_str = _to_v2_file_upload_item({"file": filepath}) + upload_item_path = _to_v2_file_upload_item({"file": Path(filepath)}) + assert upload_item_path == upload_item_str + assert upload_item_str.get("filename") == "test_internal_utils.py" + + def test_next_cursor_is_present(self): + assert _next_cursor_is_present({"next_cursor": "next-page"}) is True + assert _next_cursor_is_present({"next_cursor": ""}) is False + assert _next_cursor_is_present({"next_cursor": None}) is False + assert _next_cursor_is_present({"response_metadata": {"next_cursor": "next-page"}}) is True + assert _next_cursor_is_present({"response_metadata": {"next_cursor": ""}}) is False + assert _next_cursor_is_present({"response_metadata": {"next_cursor": None}}) is False + assert _next_cursor_is_present({"something_else": {"next_cursor": "next-page"}}) is False + + def test_get_url_prevent_double_slash(self): + # Test case: Prevent double slash when both base_url and api_method include slashes + api_url = _get_url("https://slack.com/api/", "/chat.postMessage") + self.assertEqual( + api_url, + "https://slack.com/api/chat.postMessage", + "Should correctly handle and remove double slashes between base_url and api_method", + ) + + # Test case: Handle api_method without leading slash + api_url = _get_url("https://slack.com/api/", "chat.postMessage") + self.assertEqual( + api_url, + "https://slack.com/api/chat.postMessage", + "Should correctly handle api_method without a leading slash", + ) diff --git a/tests/slack_sdk/web/test_legacy_web_client_url_format.py b/tests/slack_sdk/web/test_legacy_web_client_url_format.py new file mode 100644 index 000000000..65c8c35b2 --- /dev/null +++ b/tests/slack_sdk/web/test_legacy_web_client_url_format.py @@ -0,0 +1,53 @@ +from unittest import TestCase + +from slack_sdk.web.legacy_client import LegacyWebClient +from tests.slack_sdk.web.mock_web_api_handler import MockHandler +from tests.mock_web_api_server import setup_mock_web_api_server, cleanup_mock_web_api_server, assert_received_request_count + + +class TestLegacyWebClientUrlFormat(TestCase): + def setUp(self): + setup_mock_web_api_server(self, MockHandler) + self.client = LegacyWebClient(token="xoxb-api_test", base_url="http://localhost:8888") + self.client_base_url_slash = LegacyWebClient(token="xoxb-api_test", base_url="http://localhost:8888/") + self.client_api = LegacyWebClient(token="xoxb-api_test", base_url="http://localhost:8888/api") + self.client_api_slash = LegacyWebClient(token="xoxb-api_test", base_url="http://localhost:8888/api/") + + def tearDown(self): + cleanup_mock_web_api_server(self) + + def test_base_url_without_slash_api_method_without_slash(self): + self.client.api_call("chat.postMessage") + assert_received_request_count(self, "/chat.postMessage", 1) + + def test_base_url_without_slash_api_method_with_slash(self): + self.client.api_call("/chat.postMessage") + assert_received_request_count(self, "/chat.postMessage", 1) + + def test_base_url_with_slash_api_method_without_slash(self): + self.client_base_url_slash.api_call("chat.postMessage") + assert_received_request_count(self, "/chat.postMessage", 1) + + def test_base_url_with_slash_api_method_with_slash(self): + self.client_base_url_slash.api_call("/chat.postMessage") + assert_received_request_count(self, "/chat.postMessage", 1) + + def test_base_url_without_slash_api_method_with_slash_and_trailing_slash(self): + self.client.api_call("/chat.postMessage/") + assert_received_request_count(self, "/chat.postMessage/", 1) + + def test_base_url_with_api(self): + self.client_api.api_call("chat.postMessage") + assert_received_request_count(self, "/api/chat.postMessage", 1) + + def test_base_url_with_api_method_without_slash_method_with_slash(self): + self.client_api.api_call("/chat.postMessage") + assert_received_request_count(self, "/api/chat.postMessage", 1) + + def test_base_url_with_api_slash(self): + self.client_api_slash.api_call("chat.postMessage") + assert_received_request_count(self, "/api/chat.postMessage", 1) + + def test_base_url_with_api_slash_and_method_with_slash(self): + self.client_api_slash.api_call("/chat.postMessage") + assert_received_request_count(self, "/api/chat.postMessage", 1) diff --git a/tests/slack_sdk/web/test_slack_response.py b/tests/slack_sdk/web/test_slack_response.py new file mode 100644 index 000000000..bb34d7c2d --- /dev/null +++ b/tests/slack_sdk/web/test_slack_response.py @@ -0,0 +1,59 @@ +import unittest + +from slack_sdk.web import WebClient +from slack_sdk.web.slack_response import SlackResponse + + +class TestSlackResponse(unittest.TestCase): + def setUp(self): + pass + + def tearDown(self): + pass + + # https://github.com/slackapi/python-slackclient/issues/559 + def test_issue_559(self): + response = SlackResponse( + client=WebClient(token="xoxb-dummy"), + http_verb="POST", + api_url="http://localhost:3000/api.test", + req_args={}, + data={"ok": True, "args": {"hello": "world"}}, + headers={}, + status_code=200, + ) + + self.assertTrue("ok" in response.data) + self.assertTrue("args" in response.data) + self.assertFalse("error" in response.data) + + # https://github.com/slackapi/python-slack-sdk/issues/1100 + def test_issue_1100(self): + response = SlackResponse( + client=WebClient(token="xoxb-dummy"), + http_verb="POST", + api_url="http://localhost:3000/api.test", + req_args={}, + data=None, + headers={}, + status_code=200, + ) + with self.assertRaises(ValueError): + response["foo"] + + foo = response.get("foo") + self.assertIsNone(foo) + + # https://github.com/slackapi/python-slack-sdk/issues/1102 + def test_issue_1102(self): + response = SlackResponse( + client=WebClient(token="xoxb-dummy"), + http_verb="POST", + api_url="http://localhost:3000/api.test", + req_args={}, + data={"ok": True, "args": {"hello": "world"}}, + headers={}, + status_code=200, + ) + self.assertTrue("ok" in response) + self.assertTrue("foo" not in response) diff --git a/tests/slack_sdk/web/test_web_client.py b/tests/slack_sdk/web/test_web_client.py new file mode 100644 index 000000000..cea47a38c --- /dev/null +++ b/tests/slack_sdk/web/test_web_client.py @@ -0,0 +1,237 @@ +import re +import socket +import unittest +import time + +import slack_sdk.errors as err +from slack_sdk import WebClient +from slack_sdk.models.blocks import DividerBlock +from slack_sdk.models.metadata import Metadata +from tests.slack_sdk.web.mock_web_api_handler import MockHandler +from tests.mock_web_api_server import setup_mock_web_api_server, cleanup_mock_web_api_server + + +class TestWebClient(unittest.TestCase): + def setUp(self): + setup_mock_web_api_server(self, MockHandler) + self.client = WebClient( + token="xoxb-api_test", + base_url="http://localhost:8888", + ) + + def tearDown(self): + cleanup_mock_web_api_server(self) + + pattern_for_language = re.compile("python/(\\S+)", re.IGNORECASE) + pattern_for_package_identifier = re.compile("slackclient/(\\S+)") + + def test_subsequent_requests_with_a_session_succeeds(self): + resp = self.client.api_test() + assert resp["ok"] + resp = self.client.api_test() + assert resp["ok"] + + def test_api_calls_include_user_agent(self): + self.client.token = "xoxb-api_test" + resp = self.client.api_test() + self.assertEqual(200, resp.status_code) + + def test_builtin_api_methods_send_json(self): + self.client.token = "xoxb-api_test" + resp = self.client.api_test(msg="bye") + self.assertEqual(200, resp.status_code) + self.assertEqual("bye", resp["args"]["msg"]) + + def test_requests_can_be_paginated(self): + self.client.token = "xoxb-users_list_pagination" + users = [] + for page in self.client.users_list(limit=2): + users = users + page["members"] + self.assertTrue(len(users) == 4) + + def test_response_can_be_paginated_multiple_times(self): + self.client.token = "xoxb-conversations_list_pagination" + # This test suite verifies the changes in #521 work as expected + response = self.client.conversations_list(limit=1) + ids = [] + for page in response: + ids.append(page["channels"][0]["id"]) + self.assertEqual(ids, ["C1", "C2", "C3"]) + + # The second iteration starting with page 2 + # (page1 is already cached in `response`) + self.client.token = "xoxb-conversations_list_pagination2" + ids = [] + for page in response: + ids.append(page["channels"][0]["id"]) + self.assertEqual(ids, ["C1", "C2", "C3"]) + + def test_request_pagination_stops_when_next_cursor_is_missing(self): + self.client.token = "xoxb-users_list_pagination_1" + users = [] + for page in self.client.users_list(limit=2): + users = users + page["members"] + self.assertTrue(len(users) == 2) + + def test_json_can_only_be_sent_with_post_requests(self): + with self.assertRaises(err.SlackRequestError): + self.client.api_call("fake.method", http_verb="GET", json={}) + + def test_slack_api_error_is_raised_on_unsuccessful_responses(self): + self.client.token = "xoxb-api_test_false" + with self.assertRaises(err.SlackApiError): + self.client.api_test() + self.client.token = "xoxb-500" + with self.assertRaises(err.SlackApiError): + self.client.api_test() + + def test_slack_api_rate_limiting_exception_returns_retry_after(self): + self.client.token = "xoxb-ratelimited" + try: + self.client.api_test() + except err.SlackApiError as slack_api_error: + self.assertFalse(slack_api_error.response["ok"]) + self.assertEqual(429, slack_api_error.response.status_code) + self.assertEqual(1, int(slack_api_error.response.headers["retry-after"])) + self.assertEqual(1, int(slack_api_error.response.headers["Retry-After"])) + + def test_the_api_call_files_argument_creates_the_expected_data(self): + self.client.token = "xoxb-users_setPhoto" + resp = self.client.users_setPhoto(image="tests/slack_sdk_fixture/slack_logo.png") + self.assertEqual(200, resp.status_code) + + def test_issue_560_bool_in_params_sync(self): + self.client.token = "xoxb-conversations_list" + self.client.conversations_list(exclude_archived=1) # ok + self.client.conversations_list(exclude_archived="true") # ok + self.client.conversations_list(exclude_archived=True) # ok + + def test_issue_690_oauth_v2_access(self): + self.client.token = "" + resp = self.client.oauth_v2_access(client_id="111.222", client_secret="secret", code="codeeeeeeeeee") + self.assertIsNone(resp["error"]) + with self.assertRaises(err.SlackApiError): + self.client.oauth_v2_access(client_id="999.999", client_secret="secret", code="codeeeeeeeeee") + + def test_issue_690_oauth_access(self): + self.client.token = "" + resp = self.client.oauth_access(client_id="111.222", client_secret="secret", code="codeeeeeeeeee") + self.assertIsNone(resp["error"]) + with self.assertRaises(err.SlackApiError): + self.client.oauth_access(client_id="999.999", client_secret="secret", code="codeeeeeeeeee") + + def test_issue_705_no_param_request_pagination(self): + self.client.token = "xoxb-users_list_pagination" + users = [] + for page in self.client.users_list(): + users = users + page["members"] + self.assertTrue(len(users) == 4) + + def test_token_param(self): + client = WebClient(base_url="http://localhost:8888") + with self.assertRaises(err.SlackApiError): + client.users_list() + resp = client.users_list(token="xoxb-users_list_pagination") + self.assertIsNone(resp["error"]) + with self.assertRaises(err.SlackApiError): + client.users_list() + + def test_timeout_issue_712(self): + client = WebClient(base_url="http://localhost:8888", timeout=1) + with self.assertRaises(socket.timeout): + client.users_list(token="xoxb-timeout") + + def test_html_response_body_issue_718(self): + client = WebClient(base_url="http://localhost:8888") + try: + client.users_list(token="xoxb-html_response") + self.fail("SlackApiError expected here") + except err.SlackApiError as e: + self.assertTrue( + str(e).startswith("The request to the Slack API failed. (url: http://"), + e, + ) + + def test_user_agent_customization_issue_769(self): + client = WebClient( + base_url="http://localhost:8888", + token="xoxb-user-agent this_is test", + user_agent_prefix="this_is", + user_agent_suffix="test", + ) + resp = client.api_test() + self.assertTrue(resp["ok"]) + + def test_default_team_id(self): + client = WebClient(base_url="http://localhost:8888", team_id="T_DEFAULT") + resp = client.users_list(token="xoxb-users_list_pagination") + self.assertIsNone(resp["error"]) + + def test_message_metadata(self): + client = self.client + new_message = client.chat_postMessage( + channel="#random", + text="message with metadata", + metadata=Metadata( + event_type="procurement-task", + event_payload={ + "id": "11111", + "amount": 5000, + "tags": ["foo", "bar", "baz"], + }, + ), + ) + self.assertIsNone(new_message.get("error")) + + history = client.conversations_history( + channel=new_message.get("channel"), + limit=1, + include_all_metadata=True, + ) + self.assertIsNone(history.get("error")) + + modification = client.chat_update( + channel=new_message.get("channel"), + ts=new_message.get("ts"), + text="message with metadata (modified)", + metadata=Metadata( + event_type="procurement-task", + event_payload={ + "id": "11111", + "amount": 6000, + }, + ), + ) + self.assertIsNone(modification.get("error")) + + scheduled = client.chat_scheduleMessage( + channel=new_message.get("channel"), + post_at=int(time.time()) + 30, + text="message with metadata (scheduled)", + metadata=Metadata( + event_type="procurement-task", + event_payload={ + "id": "11111", + "amount": 10, + }, + ), + ) + self.assertIsNone(scheduled.get("error")) + + def test_user_auth_blocks(self): + client = self.client + new_message = client.chat_unfurl( + channel="C12345", + ts="1111.2222", + unfurls={}, + user_auth_blocks=[DividerBlock(), DividerBlock()], + ) + self.assertIsNone(new_message.get("error")) + + def test_base_url_appends_trailing_slash_issue_15141(self): + client = self.client + self.assertEqual(client.base_url, "http://localhost:8888/") + + def test_base_url_preserves_trailing_slash_issue_15141(self): + client = WebClient(base_url="http://localhost:8888/") + self.assertEqual(client.base_url, "http://localhost:8888/") diff --git a/tests/slack_sdk/web/test_web_client_http_retry.py b/tests/slack_sdk/web/test_web_client_http_retry.py new file mode 100644 index 000000000..85d1c0875 --- /dev/null +++ b/tests/slack_sdk/web/test_web_client_http_retry.py @@ -0,0 +1,80 @@ +import unittest + +from slack_sdk.errors import SlackApiError +from slack_sdk.http_retry import RateLimitErrorRetryHandler +from slack_sdk.web import WebClient +from tests.slack_sdk.web.mock_web_api_handler import MockHandler +from tests.mock_web_api_server import setup_mock_web_api_server, cleanup_mock_web_api_server +from ..fatal_error_retry_handler import FatalErrorRetryHandler +from ..my_retry_handler import MyRetryHandler + + +class TestWebClient_HttpRetry(unittest.TestCase): + def setUp(self): + setup_mock_web_api_server(self, MockHandler) + + def tearDown(self): + cleanup_mock_web_api_server(self) + + def test_remote_disconnected(self): + retry_handler = MyRetryHandler(max_retry_count=2) + client = WebClient( + base_url="http://localhost:8888", + token="xoxb-remote_disconnected", + team_id="T111", + retry_handlers=[retry_handler], + ) + try: + client.auth_test() + self.fail("An exception is expected") + except Exception as _: + pass + + self.assertEqual(2, retry_handler.call_count) + + def test_ratelimited_no_retry(self): + client = WebClient( + base_url="http://localhost:8888", + token="xoxb-ratelimited", + team_id="T111", + ) + try: + client.auth_test() + self.fail("An exception is expected") + except SlackApiError as e: + # Just running retries; no assertions for call count so far + self.assertEqual(429, e.response.status_code) + + def test_ratelimited(self): + client = WebClient( + base_url="http://localhost:8888", + token="xoxb-ratelimited_only_once", + team_id="T111", + ) + client.retry_handlers.append(RateLimitErrorRetryHandler()) + # The auto-retry should work here + client.auth_test() + + def test_fatal_error_no_retry(self): + client = WebClient( + base_url="http://localhost:8888", + token="xoxb-fatal_error", + team_id="T111", + ) + try: + client.auth_test() + self.fail("An exception is expected") + except SlackApiError as e: + # Just running retries; no assertions for call count so far + self.assertEqual(200, e.response.status_code) + self.assertEqual("fatal_error", e.response["error"]) + + def test_fatal_error(self): + client = WebClient( + base_url="http://localhost:8888", + token="xoxb-fatal_error_only_once", + team_id="T111", + ) + client.retry_handlers.append(FatalErrorRetryHandler()) + # The auto-retry should work here + client.auth_test() diff --git a/tests/slack_sdk/web/test_web_client_http_retry_connection.py b/tests/slack_sdk/web/test_web_client_http_retry_connection.py new file mode 100644 index 000000000..b77a30b7e --- /dev/null +++ b/tests/slack_sdk/web/test_web_client_http_retry_connection.py @@ -0,0 +1,21 @@ +import unittest + +from slack_sdk.web import WebClient +from tests.slack_sdk.web.mock_web_api_http_retry_handler import MockHandler +from tests.mock_web_api_server import setup_mock_web_api_server, cleanup_mock_web_api_server + + +class TestWebClient_HttpRetry(unittest.TestCase): + def setUp(self): + setup_mock_web_api_server(self, MockHandler, port=8889) + + def tearDown(self): + cleanup_mock_web_api_server(self) + + def test_remote_disconnected(self): + client = WebClient( + base_url="http://localhost:8889", + token="xoxb-remote_disconnected", + team_id="T111", + ) + client.auth_test() diff --git a/tests/slack_sdk/web/test_web_client_http_retry_server_error.py b/tests/slack_sdk/web/test_web_client_http_retry_server_error.py new file mode 100644 index 000000000..b0d607683 --- /dev/null +++ b/tests/slack_sdk/web/test_web_client_http_retry_server_error.py @@ -0,0 +1,71 @@ +import unittest + +import slack_sdk.errors as err +from slack_sdk.http_retry import RetryHandler, RetryIntervalCalculator +from slack_sdk.http_retry.builtin_handlers import ServerErrorRetryHandler +from slack_sdk.http_retry.handler import default_interval_calculator +from slack_sdk.web import WebClient +from tests.slack_sdk.web.mock_web_api_handler import MockHandler +from tests.mock_web_api_server import setup_mock_web_api_server, cleanup_mock_web_api_server + + +class MyServerErrorRetryHandler(RetryHandler): + """RetryHandler that does retries for server-side errors.""" + + def __init__( + self, + max_retry_count: int = 1, + interval_calculator: RetryIntervalCalculator = default_interval_calculator, + ): + super().__init__(max_retry_count, interval_calculator) + self.call_count = 0 + + def _can_retry( + self, + *, + state, + request, + response, + error, + ) -> bool: + self.call_count += 1 + return response is not None and response.status_code >= 500 + + +class TestWebClient_HttpRetry_ServerError(unittest.TestCase): + def setUp(self): + setup_mock_web_api_server(self, MockHandler) + + def tearDown(self): + cleanup_mock_web_api_server(self) + + def test_html_response_body_issue_829(self): + retry_handlers = [MyServerErrorRetryHandler(max_retry_count=2)] + client = WebClient( + base_url="http://localhost:8888", + retry_handlers=retry_handlers, + ) + try: + client.users_list(token="xoxb-error_html_response") + self.fail("SlackApiError expected here") + except err.SlackApiError as e: + self.assertTrue( + str(e).startswith("The request to the Slack API failed. (url: http://"), + e, + ) + self.assertIsInstance(e.response.status_code, int) + self.assertFalse(e.response["ok"]) + self.assertTrue( + e.response["error"].startswith("Received a response in a non-JSON format: \n\n\n\t\n\tServer Error | Slack\n\t\n\t\n\n\n\t\n\t
\n\t\t
\n\t\t\t

\n\t\t\t\t\n\t\t\t\tServer Error\n\t\t\t

\n\t\t\t
\n\t\t\t\t

It seems like there’s a problem connecting to our servers, and we’re investigating the issue.

\n\t\t\t\t

Please check our Status page for updates.

\n\t\t\t
\n\t\t
\n\t
\n\t\n\n' + + def is_valid_user_agent(self): + user_agent = self.headers["User-Agent"] + return self.pattern_for_language.search(user_agent) and self.pattern_for_package_identifier.search(user_agent) + + def set_common_headers(self): + self.send_header("content-type", "text/plain;charset=utf-8") + self.send_header("connection", "close") + self.end_headers() + + def do_GET(self): + # put_nowait is common between Queue & asyncio.Queue, it does not need to be awaited + self.server.queue.put_nowait(self.path) + if self.path == "/received_requests.json": + self.send_response(200) + self.set_common_headers() + self.wfile.write(json.dumps(self.received_requests).encode("utf-8")) + return + + def do_POST(self): + try: + # put_nowait is common between Queue & asyncio.Queue, it does not need to be awaited + self.server.queue.put_nowait(self.path) + if self.path == "/remote_disconnected": + # http.client.RemoteDisconnected + self.finish() + return + + if self.path == "/ratelimited": + self.send_response(429) + self.send_header("retry-after", 1) + self.set_common_headers() + self.wfile.write("".encode("utf-8")) + return + + if self.path == "/timeout": + time.sleep(2) + + # user-agent-this_is-test + if self.path.startswith("/user-agent-"): + elements = self.path.split("-") + prefix, suffix = elements[2], elements[-1] + ua: str = self.headers["User-Agent"] + if ua.startswith(prefix) and ua.endswith(suffix): + self.send_response(HTTPStatus.OK) + self.set_common_headers() + self.wfile.write("ok".encode("utf-8")) + self.wfile.close() + return + else: + self.send_response(HTTPStatus.BAD_REQUEST) + self.set_common_headers() + self.wfile.write("invalid user agent".encode("utf-8")) + self.wfile.close() + return + + if self.path == "/error": + self.send_response(HTTPStatus.INTERNAL_SERVER_ERROR) + # no charset here is intentional for testing + self.send_header("content-type", "text/html") + self.send_header("connection", "close") + self.end_headers() + self.wfile.write(self.error_html_response_body.encode("utf-8")) + self.wfile.close() + return + + body = "ok" + + self.send_response(HTTPStatus.OK) + self.set_common_headers() + self.wfile.write(body.encode("utf-8")) + self.wfile.close() + + except Exception as e: + self.logger.error(str(e), exc_info=True) + raise diff --git a/tests/slack_sdk/webhook/test_webhook.py b/tests/slack_sdk/webhook/test_webhook.py new file mode 100644 index 000000000..19ac9a805 --- /dev/null +++ b/tests/slack_sdk/webhook/test_webhook.py @@ -0,0 +1,215 @@ +import socket +import unittest +import urllib +from logging import Logger + +from slack_sdk.models.attachments import Attachment, AttachmentField +from slack_sdk.models.blocks import SectionBlock, ImageBlock +from slack_sdk.webhook import WebhookClient, WebhookResponse +from tests.slack_sdk.webhook.mock_web_api_server import MockHandler +from tests.mock_web_api_server import setup_mock_web_api_server, cleanup_mock_web_api_server + + +class TestWebhook(unittest.TestCase): + def setUp(self): + setup_mock_web_api_server(self, MockHandler) + + def tearDown(self): + cleanup_mock_web_api_server(self) + + def test_send(self): + client = WebhookClient("http://localhost:8888") + + resp: WebhookResponse = client.send(text="hello!") + self.assertEqual(200, resp.status_code) + self.assertEqual("ok", resp.body) + + resp = client.send(text="hello!", response_type="in_channel") + self.assertEqual("ok", resp.body) + + def test_send_blocks(self): + client = WebhookClient("http://localhost:8888") + + resp = client.send( + text="hello!", + response_type="ephemeral", + blocks=[ + SectionBlock(text="Some text"), + ImageBlock(image_url="image.jpg", alt_text="an image"), + ], + ) + self.assertEqual("ok", resp.body) + + resp = client.send( + text="hello!", + response_type="ephemeral", + blocks=[ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "This is a mrkdwn section block :ghost: *this is bold*, and ~this is crossed out~, and ", + }, + }, + {"type": "divider"}, + { + "type": "section", + "text": {"type": "mrkdwn", "text": "Pick a date for the deadline."}, + "accessory": { + "type": "datepicker", + "initial_date": "1990-04-28", + "placeholder": { + "type": "plain_text", + "text": "Select a date", + }, + }, + }, + ], + ) + self.assertEqual("ok", resp.body) + + resp = client.send( + text="hello!", + response_type="ephemeral", + blocks=[ + SectionBlock(text="Some text"), + ImageBlock(image_url="image.jpg", alt_text="an image"), + ], + ) + self.assertEqual("ok", resp.body) + + def test_send_attachments(self): + client = WebhookClient("http://localhost:8888") + + resp = client.send( + text="hello!", + response_type="ephemeral", + attachments=[ + { + "color": "#f2c744", + "blocks": [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "This is a mrkdwn section block :ghost: *this is bold*, and ~this is crossed out~, and ", + }, + }, + {"type": "divider"}, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "Pick a date for the deadline.", + }, + "accessory": { + "type": "datepicker", + "initial_date": "1990-04-28", + "placeholder": { + "type": "plain_text", + "text": "Select a date", + }, + }, + }, + ], + } + ], + ) + self.assertEqual("ok", resp.body) + + resp = client.send( + text="hello!", + response_type="ephemeral", + attachments=[ + Attachment( + text="attachment text", + title="Attachment", + fallback="fallback_text", + pretext="some_pretext", + title_link="link in title", + fields=[AttachmentField(title=f"field_{i}_title", value=f"field_{i}_value") for i in range(5)], + color="#FFFF00", + author_name="John Doe", + author_link="http://johndoeisthebest.com", + author_icon="http://johndoeisthebest.com/avatar.jpg", + thumb_url="thumbnail URL", + footer="and a footer", + footer_icon="link to footer icon", + ts=123456789, + markdown_in=["fields"], + ) + ], + ) + self.assertEqual("ok", resp.body) + + def test_send_dict(self): + client = WebhookClient("http://localhost:8888") + resp: WebhookResponse = client.send_dict({"text": "hello!"}) + self.assertEqual(200, resp.status_code) + self.assertEqual("ok", resp.body) + + def test_timeout_issue_712(self): + client = WebhookClient(url="http://localhost:8888/timeout", timeout=1) + with self.assertRaises(socket.timeout): + client.send_dict({"text": "hello!"}) + + def test_error_response(self): + client = WebhookClient(url="http://localhost:8888/error") + resp: WebhookResponse = client.send_dict({"text": "hello!"}) + self.assertEqual(500, resp.status_code) + self.assertTrue(resp.body.startswith("")) + + def test_proxy_issue_714(self): + client = WebhookClient(url="http://localhost:8888", proxy="http://invalid-host:9999") + with self.assertRaises(urllib.error.URLError): + client.send_dict({"text": "hello!"}) + + def test_user_agent_customization_issue_769(self): + client = WebhookClient( + url="http://localhost:8888/user-agent-this_is-test", + user_agent_prefix="this_is", + user_agent_suffix="test", + ) + resp = client.send_dict({"text": "hi!"}) + self.assertEqual(resp.body, "ok") + + def test_issue_919_response_url_flag_options(self): + client = WebhookClient("http://localhost:8888") + resp = client.send( + text="hello!", + response_type="ephemeral", + replace_original=True, + blocks=[ + SectionBlock(text="Some text"), + ImageBlock(image_url="image.jpg", alt_text="an image"), + ], + ) + self.assertEqual("ok", resp.body) + + resp = client.send( + text="hello!", + response_type="ephemeral", + delete_original=True, + blocks=[ + SectionBlock(text="Some text"), + ImageBlock(image_url="image.jpg", alt_text="an image"), + ], + ) + self.assertEqual("ok", resp.body) + + def test_if_it_uses_custom_logger_issue_921(self): + logger = CustomLogger("test-logger") + client = WebhookClient(url="http://localhost:8888", logger=logger) + client.send_dict({"text": "hi!"}) + self.assertTrue(logger.called) + + +class CustomLogger(Logger): + called: bool + + def __init__(self, name, level="DEBUG"): + Logger.__init__(self, name, level) + self.called = False + + def debug(self, msg, *args, **kwargs): + self.called = True diff --git a/tests/slack_sdk/webhook/test_webhook_http_retry.py b/tests/slack_sdk/webhook/test_webhook_http_retry.py new file mode 100644 index 000000000..c35441500 --- /dev/null +++ b/tests/slack_sdk/webhook/test_webhook_http_retry.py @@ -0,0 +1,36 @@ +import unittest + +from slack_sdk.http_retry import RateLimitErrorRetryHandler +from slack_sdk.webhook import WebhookClient +from tests.slack_sdk.webhook.mock_web_api_server import MockHandler +from tests.mock_web_api_server import setup_mock_web_api_server, cleanup_mock_web_api_server +from ..my_retry_handler import MyRetryHandler + + +class TestWebhook_HttpRetries(unittest.TestCase): + def setUp(self): + setup_mock_web_api_server(self, MockHandler) + + def tearDown(self): + cleanup_mock_web_api_server(self) + + def test_send(self): + retry_handler = MyRetryHandler(max_retry_count=2) + client = WebhookClient( + "http://localhost:8888/remote_disconnected", + retry_handlers=[retry_handler], + ) + try: + client.send(text="hello!") + self.fail("An exception is expected") + except Exception as _: + pass + + self.assertEqual(2, retry_handler.call_count) + + def test_ratelimited(self): + client = WebhookClient("http://localhost:8888/ratelimited") + client.retry_handlers.append(RateLimitErrorRetryHandler()) + response = client.send(text="hello!") + # Just running retries; no assertions for call count so far + self.assertEqual(429, response.status_code) diff --git a/tests/slack_sdk_async/__init__.py b/tests/slack_sdk_async/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/slack_sdk_async/audit_logs/__init__.py b/tests/slack_sdk_async/audit_logs/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/slack_sdk_async/audit_logs/test_async_client.py b/tests/slack_sdk_async/audit_logs/test_async_client.py new file mode 100644 index 000000000..92f9fa149 --- /dev/null +++ b/tests/slack_sdk_async/audit_logs/test_async_client.py @@ -0,0 +1,44 @@ +import unittest + +from slack_sdk.audit_logs.async_client import AsyncAuditLogsClient +from slack_sdk.audit_logs import AuditLogsResponse +from tests.helpers import async_test +from tests.slack_sdk.audit_logs.mock_web_api_handler import MockHandler +from tests.mock_web_api_server import setup_mock_web_api_server_async, cleanup_mock_web_api_server_async + + +class TestAsyncAuditLogsClient(unittest.TestCase): + def setUp(self): + self.client = AsyncAuditLogsClient(token="xoxp-", base_url="http://localhost:8888/") + setup_mock_web_api_server_async(self, MockHandler) + + def tearDown(self): + cleanup_mock_web_api_server_async(self) + + @async_test + async def test_logs(self): + resp: AuditLogsResponse = await self.client.logs(limit=1, action="user_login") + self.assertEqual(200, resp.status_code) + self.assertIsNotNone(resp.body.get("entries")) + + self.assertEqual(resp.typed_body.entries[0].id, "xxx-yyy-zzz-111") + + @async_test + async def test_logs_pagination(self): + resp: AuditLogsResponse = await self.client.logs(limit=1, action="user_login", cursor="XXXXXXXXXXX") + self.assertEqual(200, resp.status_code) + self.assertIsNotNone(resp.body.get("entries")) + + self.assertEqual(resp.typed_body.entries[0].id, "xxx-yyy-zzz-111") + + @async_test + async def test_actions(self): + resp: AuditLogsResponse = await self.client.actions() + self.assertEqual(200, resp.status_code) + self.assertIsNotNone(resp.body.get("actions")) + + @async_test + async def test_schemas(self): + resp: AuditLogsResponse = await self.client.schemas() + self.assertEqual(200, resp.status_code) + self.assertIsNotNone(resp.body.get("schemas")) diff --git a/tests/slack_sdk_async/audit_logs/test_async_client_http_retry.py b/tests/slack_sdk_async/audit_logs/test_async_client_http_retry.py new file mode 100644 index 000000000..b25200072 --- /dev/null +++ b/tests/slack_sdk_async/audit_logs/test_async_client_http_retry.py @@ -0,0 +1,44 @@ +import unittest + +from slack_sdk.audit_logs.async_client import AsyncAuditLogsClient +from slack_sdk.http_retry.builtin_async_handlers import AsyncRateLimitErrorRetryHandler +from tests.helpers import async_test +from tests.slack_sdk.audit_logs.mock_web_api_handler import MockHandler +from tests.mock_web_api_server import setup_mock_web_api_server_async, cleanup_mock_web_api_server_async +from ..my_retry_handler import MyRetryHandler + + +class TestAsyncAuditLogsClient_HttpRetries(unittest.TestCase): + def setUp(self): + setup_mock_web_api_server_async(self, MockHandler) + + def tearDown(self): + cleanup_mock_web_api_server_async(self) + + @async_test + async def test_http_retries(self): + retry_handler = MyRetryHandler(max_retry_count=2) + client = AsyncAuditLogsClient( + token="xoxp-remote_disconnected", + base_url="http://localhost:8888/", + retry_handlers=[retry_handler], + ) + try: + await client.actions() + self.fail("An exception is expected") + except Exception as _: + pass + + self.assertEqual(2, retry_handler.call_count) + + @async_test + async def test_ratelimited(self): + client = AsyncAuditLogsClient( + token="xoxp-ratelimited", + base_url="http://localhost:8888/", + retry_handlers=[AsyncRateLimitErrorRetryHandler()], + ) + + response = await client.actions() + # Just running retries; no assertions for call count so far + self.assertEqual(429, response.status_code) diff --git a/tests/slack_sdk_async/fatal_error_retry_handler.py b/tests/slack_sdk_async/fatal_error_retry_handler.py new file mode 100644 index 000000000..b7b5ac038 --- /dev/null +++ b/tests/slack_sdk_async/fatal_error_retry_handler.py @@ -0,0 +1,30 @@ +from typing import Optional +from aiohttp import ServerDisconnectedError, ClientOSError + +from slack_sdk.http_retry.async_handler import AsyncRetryHandler +from slack_sdk.http_retry.interval_calculator import RetryIntervalCalculator +from slack_sdk.http_retry.state import RetryState +from slack_sdk.http_retry.request import HttpRequest +from slack_sdk.http_retry.response import HttpResponse +from slack_sdk.http_retry.handler import default_interval_calculator + + +class FatalErrorRetryHandler(AsyncRetryHandler): + def __init__( + self, + max_retry_count: int = 1, + interval_calculator: RetryIntervalCalculator = default_interval_calculator, + ): + super().__init__(max_retry_count, interval_calculator) + self.call_count = 0 + + async def _can_retry_async( + self, + *, + state: RetryState, + request: HttpRequest, + response: Optional[HttpResponse], + error: Optional[Exception], + ) -> bool: + self.call_count += 1 + return response is not None and response.status_code == 200 and response.body.get("error") == "fatal_error" diff --git a/tests/slack_sdk_async/helpers.py b/tests/slack_sdk_async/helpers.py new file mode 100644 index 000000000..71673203e --- /dev/null +++ b/tests/slack_sdk_async/helpers.py @@ -0,0 +1,12 @@ +import asyncio + + +def async_test(coro): + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + + def wrapper(*args, **kwargs): + future = coro(*args, **kwargs) + return asyncio.get_event_loop().run_until_complete(future) + + return wrapper diff --git a/tests/slack_sdk_async/http_retry/__init__.py b/tests/slack_sdk_async/http_retry/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/slack_sdk_async/http_retry/test_builtins.py b/tests/slack_sdk_async/http_retry/test_builtins.py new file mode 100644 index 000000000..45c086e61 --- /dev/null +++ b/tests/slack_sdk_async/http_retry/test_builtins.py @@ -0,0 +1,13 @@ +import unittest + +from slack_sdk.http_retry.builtin_async_handlers import async_default_handlers + + +class TestBuiltins(unittest.TestCase): + def test_default_ones(self): + list = async_default_handlers() + self.assertEqual(1, len(list)) + list.clear() + self.assertEqual(0, len(list)) + list = async_default_handlers() + self.assertEqual(1, len(list)) diff --git a/tests/slack_sdk_async/my_retry_handler.py b/tests/slack_sdk_async/my_retry_handler.py new file mode 100644 index 000000000..6ee3766cc --- /dev/null +++ b/tests/slack_sdk_async/my_retry_handler.py @@ -0,0 +1,35 @@ +from typing import Optional +from aiohttp import ServerDisconnectedError, ClientOSError + +from slack_sdk.http_retry.async_handler import AsyncRetryHandler +from slack_sdk.http_retry.interval_calculator import RetryIntervalCalculator +from slack_sdk.http_retry.state import RetryState +from slack_sdk.http_retry.request import HttpRequest +from slack_sdk.http_retry.response import HttpResponse +from slack_sdk.http_retry.handler import default_interval_calculator + + +class MyRetryHandler(AsyncRetryHandler): + def __init__( + self, + max_retry_count: int = 1, + interval_calculator: RetryIntervalCalculator = default_interval_calculator, + ): + super().__init__(max_retry_count, interval_calculator) + self.call_count = 0 + + async def _can_retry_async( + self, + *, + state: RetryState, + request: HttpRequest, + response: Optional[HttpResponse], + error: Optional[Exception], + ) -> bool: + self.call_count += 1 + if error is None: + return False + for error_type in [ServerDisconnectedError, ClientOSError]: + if isinstance(error, error_type): + return True + return False diff --git a/tests/slack_sdk_async/oauth/__init__.py b/tests/slack_sdk_async/oauth/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/slack_sdk_async/oauth/installation_store/__init__.py b/tests/slack_sdk_async/oauth/installation_store/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/slack_sdk_async/oauth/installation_store/test_simple_cache.py b/tests/slack_sdk_async/oauth/installation_store/test_simple_cache.py new file mode 100644 index 000000000..c1f43f4e4 --- /dev/null +++ b/tests/slack_sdk_async/oauth/installation_store/test_simple_cache.py @@ -0,0 +1,88 @@ +import os +import unittest + +from slack_sdk.oauth.installation_store import Installation +from slack_sdk.oauth.installation_store.async_cacheable_installation_store import ( + AsyncCacheableInstallationStore, +) +from slack_sdk.oauth.installation_store.sqlite3 import SQLite3InstallationStore +from tests.helpers import async_test + + +class TestCacheable(unittest.TestCase): + @async_test + async def test_save_and_find(self): + sqlite3_store = SQLite3InstallationStore(database="logs/cacheable.db", client_id="111.222") + sqlite3_store.init() + store = AsyncCacheableInstallationStore(sqlite3_store) + + installation = Installation( + app_id="A111", + enterprise_id="E111", + team_id="T111", + user_id="U111", + bot_id="B111", + bot_token="xoxb-111", + bot_scopes=["chat:write"], + bot_user_id="U222", + ) + await store.async_save(installation) + + bot = await store.async_find_bot(enterprise_id="E111", team_id="T111") + self.assertIsNotNone(bot) + + os.remove("logs/cacheable.db") + + bot = await sqlite3_store.async_find_bot(enterprise_id="E111", team_id="T111") + self.assertIsNone(bot) + bot = await store.async_find_bot(enterprise_id="E111", team_id="T111") + self.assertIsNotNone(bot) + + @async_test + async def test_save_and_find_token_rotation(self): + sqlite3_store = SQLite3InstallationStore(database="logs/cacheable.db", client_id="111.222") + sqlite3_store.init() + store = AsyncCacheableInstallationStore(sqlite3_store) + + installation = Installation( + app_id="A111", + enterprise_id="E111", + team_id="T111", + user_id="U111", + bot_id="B111", + bot_token="xoxb-initial", + bot_scopes=["chat:write"], + bot_user_id="U222", + bot_refresh_token="xoxe-1-initial", + bot_token_expires_in=43200, + ) + await store.async_save(installation) + + bot = await store.async_find_bot(enterprise_id="E111", team_id="T111") + self.assertIsNotNone(bot) + self.assertEqual(bot.bot_refresh_token, "xoxe-1-initial") + + installation = Installation( + app_id="A111", + enterprise_id="E111", + team_id="T111", + user_id="U111", + bot_id="B111", + bot_token="xoxb-refreshed", + bot_scopes=["chat:write"], + bot_user_id="U222", + bot_refresh_token="xoxe-1-refreshed", + bot_token_expires_in=43200, + ) + await store.async_save(installation) + + bot = await store.async_find_bot(enterprise_id="E111", team_id="T111") + self.assertIsNotNone(bot) + self.assertEqual(bot.bot_refresh_token, "xoxe-1-refreshed") + + os.remove("logs/cacheable.db") + + bot = await sqlite3_store.async_find_bot(enterprise_id="E111", team_id="T111") + self.assertIsNone(bot) + bot = await store.async_find_bot(enterprise_id="E111", team_id="T111") + self.assertIsNotNone(bot) diff --git a/tests/slack_sdk_async/oauth/token_rotation/__init__.py b/tests/slack_sdk_async/oauth/token_rotation/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/slack_sdk_async/oauth/token_rotation/test_token_rotator.py b/tests/slack_sdk_async/oauth/token_rotation/test_token_rotator.py new file mode 100644 index 000000000..4c16bb3b5 --- /dev/null +++ b/tests/slack_sdk_async/oauth/token_rotation/test_token_rotator.py @@ -0,0 +1,117 @@ +import unittest + +from slack_sdk.errors import SlackTokenRotationError +from slack_sdk.oauth.installation_store import Installation +from slack_sdk.oauth.token_rotation.async_rotator import AsyncTokenRotator +from slack_sdk.web.async_client import AsyncWebClient +from tests.helpers import async_test +from tests.slack_sdk.web.mock_web_api_handler import MockHandler +from tests.mock_web_api_server import setup_mock_web_api_server_async, cleanup_mock_web_api_server_async + + +class TestTokenRotator(unittest.TestCase): + def setUp(self): + setup_mock_web_api_server_async(self, MockHandler) + self.token_rotator = AsyncTokenRotator( + client=AsyncWebClient(base_url="http://localhost:8888", token=None), + client_id="111.222", + client_secret="token_rotation_secret", + ) + + def tearDown(self): + cleanup_mock_web_api_server_async(self) + + @async_test + async def test_refresh(self): + installation = Installation( + app_id="A111", + enterprise_id="E111", + team_id="T111", + user_id="U111", + bot_id="B111", + bot_token="xoxe.xoxp-1-initial", + bot_scopes=["chat:write"], + bot_user_id="U222", + bot_refresh_token="xoxe-1-initial", + bot_token_expires_in=43200, + ) + refreshed = await self.token_rotator.perform_token_rotation( + installation=installation, minutes_before_expiration=60 * 24 * 365 + ) + self.assertIsNotNone(refreshed) + + should_not_be_refreshed = await self.token_rotator.perform_token_rotation( + installation=installation, minutes_before_expiration=1 + ) + self.assertIsNone(should_not_be_refreshed) + + @async_test + async def test_refresh_with_custom_values(self): + installation = Installation( + app_id="A111", + enterprise_id="E111", + team_id="T111", + user_id="U111", + bot_id="B111", + bot_token="xoxe.xoxp-1-initial", + bot_scopes=["chat:write"], + bot_user_id="U222", + bot_refresh_token="xoxe-1-initial", + bot_token_expires_in=43200, + custom_values={"foo": "bar"}, + ) + refreshed = await self.token_rotator.perform_token_rotation( + installation=installation, minutes_before_expiration=60 * 24 * 365 + ) + self.assertIsNotNone(refreshed) + self.assertIsNotNone(refreshed.custom_values) + + should_not_be_refreshed = await self.token_rotator.perform_token_rotation( + installation=installation, minutes_before_expiration=1 + ) + self.assertIsNone(should_not_be_refreshed) + + @async_test + async def test_token_rotation_disabled(self): + installation = Installation( + app_id="A111", + enterprise_id="E111", + team_id="T111", + user_id="U111", + bot_id="B111", + bot_token="xoxe.xoxp-1-initial", + bot_scopes=["chat:write"], + bot_user_id="U222", + ) + should_not_be_refreshed = await self.token_rotator.perform_token_rotation( + installation=installation, minutes_before_expiration=60 * 24 * 365 + ) + self.assertIsNone(should_not_be_refreshed) + + should_not_be_refreshed = await self.token_rotator.perform_token_rotation( + installation=installation, minutes_before_expiration=1 + ) + self.assertIsNone(should_not_be_refreshed) + + @async_test + async def test_refresh_error(self): + token_rotator = AsyncTokenRotator( + client=AsyncWebClient(base_url="http://localhost:8888", token=None), + client_id="111.222", + client_secret="invalid_value", + ) + + installation = Installation( + app_id="A111", + enterprise_id="E111", + team_id="T111", + user_id="U111", + bot_id="B111", + bot_token="xoxe.xoxp-1-initial", + bot_scopes=["chat:write"], + bot_user_id="U222", + bot_refresh_token="xoxe-1-initial", + bot_token_expires_in=43200, + ) + with self.assertRaises(SlackTokenRotationError): + await token_rotator.perform_token_rotation(installation=installation, minutes_before_expiration=60 * 24 * 365) diff --git a/tests/slack_sdk_async/scim/__init__.py b/tests/slack_sdk_async/scim/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/slack_sdk_async/scim/test_async_client.py b/tests/slack_sdk_async/scim/test_async_client.py new file mode 100644 index 000000000..c4faa6a73 --- /dev/null +++ b/tests/slack_sdk_async/scim/test_async_client.py @@ -0,0 +1,76 @@ +import time +import unittest + +from slack_sdk.scim import User, Group +from slack_sdk.scim.v1.async_client import AsyncSCIMClient +from slack_sdk.scim.v1.group import GroupMember +from slack_sdk.scim.v1.user import UserName, UserEmail +from tests.helpers import async_test +from tests.slack_sdk.scim.mock_web_api_handler import MockHandler +from tests.mock_web_api_server import setup_mock_web_api_server_async, cleanup_mock_web_api_server_async + + +class TestSCIMClient(unittest.TestCase): + def setUp(self): + setup_mock_web_api_server_async(self, MockHandler) + + def tearDown(self): + cleanup_mock_web_api_server_async(self) + + @async_test + async def test_users(self): + client = AsyncSCIMClient(base_url="http://localhost:8888/", token="xoxp-valid") + await client.search_users(start_index=0, count=1) + await client.read_user("U111") + + now = str(time.time())[:10] + user = User( + user_name=f"user_{now}", + name=UserName(given_name="Kaz", family_name="Sera"), + emails=[UserEmail(value=f"seratch+{now}@example.com")], + schemas=["urn:scim:schemas:core:1.0"], + ) + await client.create_user(user) + # The mock server does not work for PATH requests + try: + await client.patch_user("U111", partial_user=User(user_name="foo")) + except: + pass + user.id = "U111" + user.user_name = "updated" + try: + await client.update_user(user) + except: + pass + try: + await client.delete_user("U111") + except: + pass + + @async_test + async def test_groups(self): + client = AsyncSCIMClient(base_url="http://localhost:8888/", token="xoxp-valid") + await client.search_groups(start_index=0, count=1) + await client.read_group("S111") + + now = str(time.time())[:10] + group = Group( + display_name=f"TestGroup_{now}", + members=[GroupMember(value="U111")], + ) + await client.create_group(group) + # The mock server does not work for PATH requests + try: + await client.patch_group("S111", partial_group=Group(display_name=f"TestGroup_{now}_2")) + except: + pass + group.id = "S111" + group.display_name = "updated" + try: + await client.update_group(group) + except: + pass + try: + await client.delete_group("S111") + except: + pass diff --git a/tests/slack_sdk_async/scim/test_async_client_http_retry.py b/tests/slack_sdk_async/scim/test_async_client_http_retry.py new file mode 100644 index 000000000..92fe595dd --- /dev/null +++ b/tests/slack_sdk_async/scim/test_async_client_http_retry.py @@ -0,0 +1,45 @@ +import unittest + +from slack_sdk.http_retry.builtin_async_handlers import AsyncRateLimitErrorRetryHandler +from slack_sdk.scim.v1.async_client import AsyncSCIMClient +from tests.helpers import async_test +from tests.slack_sdk.scim.mock_web_api_handler import MockHandler +from tests.mock_web_api_server import setup_mock_web_api_server_async, cleanup_mock_web_api_server_async +from ..my_retry_handler import MyRetryHandler + + +class TestSCIMClient_HttpRetries(unittest.TestCase): + def setUp(self): + setup_mock_web_api_server_async(self, MockHandler) + + def tearDown(self): + cleanup_mock_web_api_server_async(self) + + @async_test + async def test_retries(self): + retry_handler = MyRetryHandler(max_retry_count=2) + client = AsyncSCIMClient( + base_url="http://localhost:8888/", + token="xoxp-remote_disconnected", + retry_handlers=[retry_handler], + ) + + try: + await client.search_users(start_index=0, count=1) + self.fail("An exception is expected") + except Exception as _: + pass + + self.assertEqual(2, retry_handler.call_count) + + @async_test + async def test_ratelimited(self): + client = AsyncSCIMClient( + base_url="http://localhost:8888/", + token="xoxp-ratelimited", + retry_handlers=[AsyncRateLimitErrorRetryHandler()], + ) + + response = await client.search_users(start_index=0, count=1) + # Just running retries; no assertions for call count so far + self.assertEqual(429, response.status_code) diff --git a/tests/slack_sdk_async/socket_mode/__init__.py b/tests/slack_sdk_async/socket_mode/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/slack_sdk_async/socket_mode/test_aiohttp.py b/tests/slack_sdk_async/socket_mode/test_aiohttp.py new file mode 100644 index 000000000..eda1fecdd --- /dev/null +++ b/tests/slack_sdk_async/socket_mode/test_aiohttp.py @@ -0,0 +1,99 @@ +import asyncio +import unittest + +from slack_sdk.socket_mode.aiohttp import SocketModeClient +from slack_sdk.web.async_client import AsyncWebClient +from tests.slack_sdk.socket_mode.mock_web_api_handler import MockHandler +from tests.slack_sdk_async.helpers import async_test +from tests.mock_web_api_server import setup_mock_web_api_server_async, cleanup_mock_web_api_server_async + + +class TestAiohttp(unittest.TestCase): + def setUp(self): + setup_mock_web_api_server_async(self, MockHandler) + self.web_client = AsyncWebClient( + token="xoxb-api_test", + base_url="http://localhost:8888", + ) + + def tearDown(self): + cleanup_mock_web_api_server_async(self) + + @async_test + async def test_init_close(self): + client = SocketModeClient( + app_token="xapp-A111-222-xyz", + web_client=self.web_client, + auto_reconnect_enabled=False, + ) + try: + self.assertIsNotNone(client) + finally: + await client.close() + + @async_test + async def test_init_with_loop(self): + client = SocketModeClient( + app_token="xapp-A111-222-xyz", + web_client=self.web_client, + auto_reconnect_enabled=False, + loop=asyncio.get_event_loop(), + ) + try: + self.assertIsNotNone(client) + finally: + await client.close() + + @async_test + async def test_issue_new_wss_url(self): + client = SocketModeClient( + app_token="xapp-A111-222-xyz", + web_client=self.web_client, + auto_reconnect_enabled=False, + ) + try: + url = await client.issue_new_wss_url() + self.assertTrue(url.startswith("ws://")) + finally: + await client.close() + + # TODO: valid test to connect + # @async_test + # async def test_connect_to_new_endpoint(self): + # client = SocketModeClient( + # app_token="xapp-A111-222-xyz", + # web_client=self.web_client, + # auto_reconnect_enabled=False, + # ) + # try: + # await client.connect_to_new_endpoint() + # except Exception as e: + # pass + # finally: + # await client.close() + + @async_test + async def test_enqueue_message(self): + client = SocketModeClient( + app_token="xapp-A111-222-xyz", + web_client=self.web_client, + auto_reconnect_enabled=False, + trace_enabled=True, + on_message_listeners=[lambda msg: None], + ) + client.message_listeners.append(listener) + try: + await client.enqueue_message("hello") + await client.process_message() + + await client.enqueue_message( + """{"type":"hello","num_connections":1,"debug_info":{"host":"applink-111-222","build_number":10,"approximate_connection_time":18060},"connection_info":{"app_id":"A111"}}""" + ) + await client.process_message() + finally: + await client.disconnect() + await client.close() + + +async def listener(self, message, raw_message): + pass diff --git a/tests/slack_sdk_async/socket_mode/test_interactions_aiohttp.py b/tests/slack_sdk_async/socket_mode/test_interactions_aiohttp.py new file mode 100644 index 000000000..91e28d7ca --- /dev/null +++ b/tests/slack_sdk_async/socket_mode/test_interactions_aiohttp.py @@ -0,0 +1,203 @@ +import asyncio +import logging +import time +import unittest +from random import randint + +import pytest +from aiohttp import WSMessage + +from slack_sdk.socket_mode.request import SocketModeRequest + +from slack_sdk.socket_mode.async_client import AsyncBaseSocketModeClient + +from slack_sdk.socket_mode.aiohttp import SocketModeClient +from slack_sdk.web.async_client import AsyncWebClient +from tests.slack_sdk.socket_mode.mock_socket_mode_server import ( + request_socket_mode_server_disconnect, + start_socket_mode_server, + socket_mode_envelopes, + socket_mode_hello_message, + stop_socket_mode_server, +) +from tests.slack_sdk.socket_mode.mock_web_api_handler import MockHandler +from tests.mock_web_api_server import setup_mock_web_api_server_async, cleanup_mock_web_api_server_async +from tests.slack_sdk_async.helpers import async_test + + +class TestInteractionsAiohttp(unittest.TestCase): + logger = logging.getLogger(__name__) + + def setUp(self): + setup_mock_web_api_server_async(self, MockHandler) + self.web_client = AsyncWebClient( + token="xoxb-api_test", + base_url="http://localhost:8888", + ) + start_socket_mode_server(self, 3001) + + def tearDown(self): + cleanup_mock_web_api_server_async(self) + stop_socket_mode_server(self) + + @async_test + async def test_interactions(self): + received_messages = [] + received_socket_mode_requests = [] + + async def message_handler(message: WSMessage): + self.logger.info(f"Raw Message: {message}") + await asyncio.sleep(randint(50, 200) / 1000) + received_messages.append(message.data) + + async def socket_mode_listener( + self: AsyncBaseSocketModeClient, + request: SocketModeRequest, + ): + self.logger.info(f"Socket Mode Request: {request.payload}") + await asyncio.sleep(randint(50, 200) / 1000) + received_socket_mode_requests.append(request.payload) + + client = SocketModeClient( + app_token="xapp-A111-222-xyz", + web_client=self.web_client, + on_message_listeners=[message_handler], + auto_reconnect_enabled=False, + trace_enabled=True, + ) + client.socket_mode_request_listeners.append(socket_mode_listener) + + try: + client.wss_uri = "ws://0.0.0.0:3001/link" + await client.connect() + await asyncio.sleep(1) # wait for the message receiver + + for _ in range(10): + await client.send_message("foo") + await client.send_message("bar") + await client.send_message("baz") + + expected = socket_mode_envelopes + [socket_mode_hello_message] + ["foo", "bar", "baz"] * 10 + expected.sort() + expected.sort() + + count = 0 + while count < 10 and ( + len(received_messages) < len(expected) or len(received_socket_mode_requests) < len(socket_mode_envelopes) + ): + await asyncio.sleep(0.2) + count += 0.2 + + received_messages.sort() + self.assertEqual(received_messages, expected) + + self.assertEqual(len(socket_mode_envelopes), len(received_socket_mode_requests)) + finally: + await client.close() + + @async_test + async def test_interactions_with_disconnection(self): + self.disconnected = False + received_messages = [] + received_socket_mode_requests = [] + + async def message_handler(message: WSMessage): + session_id = client.build_session_id(client.current_session) + if "wait_for_disconnect" in message.data: + return + self.logger.info(f"Raw Message: {message}") + await asyncio.sleep(randint(50, 200) / 1000) + self.disconnected = "disconnect" in message.data + received_messages.append(message.data + "_" + session_id) + + async def socket_mode_listener( + self: AsyncBaseSocketModeClient, + request: SocketModeRequest, + ): + self.logger.info(f"Socket Mode Request: {request.payload}") + await asyncio.sleep(randint(50, 200) / 1000) + received_socket_mode_requests.append(request.payload) + + client = SocketModeClient( + app_token="xapp-A111-222-xyz", + web_client=self.web_client, + on_message_listeners=[message_handler], + auto_reconnect_enabled=True, + trace_enabled=True, + ) + client.socket_mode_request_listeners.append(socket_mode_listener) + + try: + client.wss_uri = "ws://0.0.0.0:3001/link" + await client.connect() + await asyncio.sleep(1) # wait for the message receiver + + request_socket_mode_server_disconnect(3001, 1) + + # Because we want to check the expected messages of new session, + # we need to ensure we send messaged after disconnected. + count = 0 + while not self.disconnected and count < 10: + try: + await client.send_message("wait_for_disconnect") + except Exception as e: + self.logger.exception(e) + finally: + await asyncio.sleep(1) + count += 1 + await asyncio.sleep(10) + expected_session_id = client.build_session_id(client.current_session) + + for _ in range(10): + await client.send_message("foo") + await client.send_message("bar") + await client.send_message("baz") + + expected = socket_mode_envelopes + [socket_mode_hello_message] + ["foo", "bar", "baz"] * 10 + expected.sort() + + count = 0 + while count < 10 and ( + len([msg for msg in received_messages if expected_session_id in msg]) < len(expected) + or len(received_socket_mode_requests) < len(socket_mode_envelopes) + ): + await asyncio.sleep(0.2) + count += 0.2 + + received_messages.sort() + + # Only check messages of current alive session. Ignore the disconnected session. + received_messages = [msg for msg in received_messages if expected_session_id in msg] + expected = [msg + "_" + expected_session_id for msg in expected] + + self.assertEqual(received_messages, expected) + + self.assertEqual(len(socket_mode_envelopes), len(received_socket_mode_requests)) + finally: + await client.close() + + @async_test + async def test_send_message_while_disconnection(self): + client = SocketModeClient( + app_token="xapp-A111-222-xyz", + web_client=self.web_client, + auto_reconnect_enabled=False, + trace_enabled=True, + ) + + try: + client.wss_uri = "ws://0.0.0.0:3001/link" + await client.connect() + await asyncio.sleep(1) # wait for the message receiver + await client.send_message("foo") + + await client.disconnect() + await asyncio.sleep(1) # wait for the message receiver + with pytest.raises(ConnectionError): + await client.send_message("foo") + + await client.connect() + await asyncio.sleep(1) # wait for the message receiver + await client.send_message("foo") + finally: + await client.close() diff --git a/tests/slack_sdk_async/socket_mode/test_interactions_websockets.py b/tests/slack_sdk_async/socket_mode/test_interactions_websockets.py new file mode 100644 index 000000000..5b408dbcf --- /dev/null +++ b/tests/slack_sdk_async/socket_mode/test_interactions_websockets.py @@ -0,0 +1,121 @@ +import asyncio +import logging +import time +import unittest +from random import randint +from typing import Optional + +import pytest +from websockets.exceptions import WebSocketException + +from slack_sdk.socket_mode.request import SocketModeRequest + +from slack_sdk.socket_mode.async_client import AsyncBaseSocketModeClient +from slack_sdk.socket_mode.websockets import SocketModeClient +from slack_sdk.web.async_client import AsyncWebClient +from tests.slack_sdk.socket_mode.mock_socket_mode_server import ( + start_socket_mode_server, + socket_mode_envelopes, + socket_mode_hello_message, +) +from tests.slack_sdk.socket_mode.mock_web_api_handler import MockHandler +from tests.mock_web_api_server import setup_mock_web_api_server_async, cleanup_mock_web_api_server_async +from tests.slack_sdk_async.helpers import async_test + + +class TestInteractionsWebsockets(unittest.TestCase): + logger = logging.getLogger(__name__) + + def setUp(self): + setup_mock_web_api_server_async(self, MockHandler) + self.web_client = AsyncWebClient( + token="xoxb-api_test", + base_url="http://localhost:8888", + ) + start_socket_mode_server(self, 3001) + + def tearDown(self): + cleanup_mock_web_api_server_async(self) + + @async_test + async def test_interactions(self): + received_messages = [] + received_socket_mode_requests = [] + + async def message_handler( + receiver: AsyncBaseSocketModeClient, + message: dict, + raw_message: Optional[str], + ): + self.logger.info(f"Raw Message: {raw_message}") + await asyncio.sleep(randint(50, 200) / 1000) + received_messages.append(raw_message) + + async def socket_mode_listener( + receiver: AsyncBaseSocketModeClient, + request: SocketModeRequest, + ): + self.logger.info(f"Socket Mode Request: {request}") + await asyncio.sleep(randint(50, 200) / 1000) + received_socket_mode_requests.append(request) + + client = SocketModeClient( + app_token="xapp-A111-222-xyz", + web_client=self.web_client, + auto_reconnect_enabled=False, + ) + client.message_listeners.append(message_handler) + client.socket_mode_request_listeners.append(socket_mode_listener) + + try: + client.wss_uri = "ws://0.0.0.0:3001/link" + await client.connect() + await asyncio.sleep(1) # wait for the message receiver + + for _ in range(10): + await client.send_message("foo") + await client.send_message("bar") + await client.send_message("baz") + + expected = socket_mode_envelopes + [socket_mode_hello_message] + ["foo", "bar", "baz"] * 10 + expected.sort() + + count = 0 + while count < 10 and ( + len(received_messages) < len(expected) or len(received_socket_mode_requests) < len(socket_mode_envelopes) + ): + await asyncio.sleep(0.2) + count += 0.2 + + received_messages.sort() + self.assertEqual(received_messages, expected) + + self.assertEqual(len(socket_mode_envelopes), len(received_socket_mode_requests)) + finally: + await client.close() + + @async_test + async def test_send_message_while_disconnection(self): + client = SocketModeClient( + app_token="xapp-A111-222-xyz", + web_client=self.web_client, + auto_reconnect_enabled=False, + trace_enabled=True, + ) + + try: + client.wss_uri = "ws://0.0.0.0:3001/link" + await client.connect() + await asyncio.sleep(1) # wait for the message receiver + await client.send_message("foo") + + await client.disconnect() + await asyncio.sleep(1) # wait for the message receiver + with pytest.raises(WebSocketException): + await client.send_message("foo") + + await client.connect() + await asyncio.sleep(1) # wait for the message receiver + await client.send_message("foo") + finally: + await client.close() diff --git a/tests/slack_sdk_async/socket_mode/test_websockets.py b/tests/slack_sdk_async/socket_mode/test_websockets.py new file mode 100644 index 000000000..322bd8c73 --- /dev/null +++ b/tests/slack_sdk_async/socket_mode/test_websockets.py @@ -0,0 +1,75 @@ +import unittest + +from slack_sdk.socket_mode.websockets import SocketModeClient +from slack_sdk.web.async_client import AsyncWebClient +from tests.slack_sdk.socket_mode.mock_web_api_handler import MockHandler +from tests.mock_web_api_server import setup_mock_web_api_server_async, cleanup_mock_web_api_server_async +from tests.slack_sdk_async.helpers import async_test + + +class TestAiohttp(unittest.TestCase): + def setUp(self): + setup_mock_web_api_server_async(self, MockHandler) + self.web_client = AsyncWebClient( + token="xoxb-api_test", + base_url="http://localhost:8888", + ) + + def tearDown(self): + cleanup_mock_web_api_server_async(self) + + @async_test + async def test_init_close(self): + client = SocketModeClient(app_token="xapp-A111-222-xyz") + try: + self.assertIsNotNone(client) + finally: + await client.close() + + @async_test + async def test_issue_new_wss_url(self): + client = SocketModeClient( + app_token="xapp-A111-222-xyz", + web_client=self.web_client, + ) + try: + url = await client.issue_new_wss_url() + self.assertTrue(url.startswith("ws://")) + finally: + await client.close() + + @async_test + async def test_connect_to_new_endpoint(self): + client = SocketModeClient( + app_token="xapp-A111-222-xyz", + web_client=self.web_client, + ) + try: + await client.connect_to_new_endpoint() + except Exception as e: + # TODO: valida test to connect + pass + finally: + await client.close() + + @async_test + async def test_enqueue_message(self): + client = SocketModeClient( + app_token="xapp-A111-222-xyz", + web_client=self.web_client, + ) + client.message_listeners.append(listener) + try: + await client.enqueue_message("hello") + await client.process_message() + + await client.enqueue_message( + """{"type":"hello","num_connections":1,"debug_info":{"host":"applink-111-222","build_number":10,"approximate_connection_time":18060},"connection_info":{"app_id":"A111"}}""" + ) + await client.process_message() + finally: + await client.close() + + +async def listener(message, raw_message): + pass diff --git a/tests/slack_sdk_async/web/__init__.py b/tests/slack_sdk_async/web/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/slack_sdk_async/web/test_async_chat_stream.py b/tests/slack_sdk_async/web/test_async_chat_stream.py new file mode 100644 index 000000000..212fee1e2 --- /dev/null +++ b/tests/slack_sdk_async/web/test_async_chat_stream.py @@ -0,0 +1,193 @@ +import json +import unittest +from urllib.parse import parse_qs, urlparse + +from slack_sdk.errors import SlackRequestError +from slack_sdk.models.blocks.basic_components import FeedbackButtonObject +from slack_sdk.models.blocks.block_elements import FeedbackButtonsElement, IconButtonElement +from slack_sdk.models.blocks.blocks import ContextActionsBlock +from slack_sdk.web.async_client import AsyncWebClient +from tests.mock_web_api_server import cleanup_mock_web_api_server, setup_mock_web_api_server +from tests.slack_sdk.web.mock_web_api_handler import MockHandler +from tests.slack_sdk_async.helpers import async_test + + +class ChatStreamMockHandler(MockHandler): + """Extended mock handler that captures request bodies for chat stream methods""" + + def _handle(self): + try: + # put_nowait is common between Queue & asyncio.Queue, it does not need to be awaited + self.server.queue.put_nowait(self.path) + + # Standard auth and validation from parent + if self.is_valid_token() and self.is_valid_user_agent(): + token = self.headers["authorization"].split(" ")[1] + parsed_path = urlparse(self.path) + len_header = self.headers.get("Content-Length") or 0 + content_len = int(len_header) + post_body = self.rfile.read(content_len) + request_body = None + if post_body: + try: + post_body = post_body.decode("utf-8") + if post_body.startswith("{"): + request_body = json.loads(post_body) + else: + request_body = {k: v[0] for k, v in parse_qs(post_body).items()} + except UnicodeDecodeError: + pass + else: + if parsed_path and parsed_path.query: + request_body = {k: v[0] for k, v in parse_qs(parsed_path.query).items()} + + # Store request body for chat stream endpoints + if self.path in ["/chat.startStream", "/chat.appendStream", "/chat.stopStream"] and request_body: + if not hasattr(self.server, "chat_stream_requests"): + self.server.chat_stream_requests = {} + self.server.chat_stream_requests[self.path] = { + "token": token, + **request_body, + } + + # Load response file + pattern = str(token).split("xoxb-", 1)[1] + with open(f"tests/slack_sdk_fixture/web_response_{pattern}.json") as file: + body = json.load(file) + + else: + body = self.invalid_auth + + if not body: + body = self.not_found + + self.send_response(200) + self.set_common_headers() + self.wfile.write(json.dumps(body).encode("utf-8")) + self.wfile.close() + + except Exception as e: + self.logger.error(str(e), exc_info=True) + raise + + +class TestAsyncChatStream(unittest.TestCase): + def setUp(self): + setup_mock_web_api_server(self, ChatStreamMockHandler) + self.client = AsyncWebClient( + token="xoxb-chat_stream_test", + base_url="http://localhost:8888", + ) + + def tearDown(self): + cleanup_mock_web_api_server(self) + + @async_test + async def test_streams_a_short_message(self): + streamer = await self.client.chat_stream( + channel="C0123456789", + thread_ts="123.000", + recipient_team_id="T0123456789", + recipient_user_id="U0123456789", + ) + await streamer.append(markdown_text="nice!") + await streamer.stop() + + self.assertEqual(self.received_requests.get("/chat.startStream", 0), 1) + self.assertEqual(self.received_requests.get("/chat.appendStream", 0), 0) + self.assertEqual(self.received_requests.get("/chat.stopStream", 0), 1) + + if hasattr(self.thread.server, "chat_stream_requests"): + start_request = self.thread.server.chat_stream_requests.get("/chat.startStream", {}) + self.assertEqual(start_request.get("channel"), "C0123456789") + self.assertEqual(start_request.get("thread_ts"), "123.000") + self.assertEqual(start_request.get("recipient_team_id"), "T0123456789") + self.assertEqual(start_request.get("recipient_user_id"), "U0123456789") + + stop_request = self.thread.server.chat_stream_requests.get("/chat.stopStream", {}) + self.assertEqual(stop_request.get("channel"), "C0123456789") + self.assertEqual(stop_request.get("ts"), "123.123") + self.assertEqual(stop_request.get("markdown_text"), "nice!") + + @async_test + async def test_streams_a_long_message(self): + streamer = await self.client.chat_stream( + buffer_size=5, + channel="C0123456789", + recipient_team_id="T0123456789", + recipient_user_id="U0123456789", + thread_ts="123.000", + ) + await streamer.append(markdown_text="**this messag") + await streamer.append(markdown_text="e is", token="xoxb-chat_stream_test_token1") + await streamer.append(markdown_text=" bold!") + await streamer.append(markdown_text="*") + await streamer.stop( + blocks=[ + ContextActionsBlock( + elements=[ + FeedbackButtonsElement( + positive_button=FeedbackButtonObject(text="good", value="+1"), + negative_button=FeedbackButtonObject(text="bad", value="-1"), + ), + IconButtonElement( + icon="trash", + text="delete", + ), + ], + ) + ], + markdown_text="*", + token="xoxb-chat_stream_test_token2", + ) + + self.assertEqual(self.received_requests.get("/chat.startStream", 0), 1) + self.assertEqual(self.received_requests.get("/chat.appendStream", 0), 1) + self.assertEqual(self.received_requests.get("/chat.stopStream", 0), 1) + + if hasattr(self.thread.server, "chat_stream_requests"): + start_request = self.thread.server.chat_stream_requests.get("/chat.startStream", {}) + self.assertEqual(start_request.get("channel"), "C0123456789") + self.assertEqual(start_request.get("thread_ts"), "123.000") + self.assertEqual(start_request.get("markdown_text"), "**this messag") + self.assertEqual(start_request.get("recipient_team_id"), "T0123456789") + self.assertEqual(start_request.get("recipient_user_id"), "U0123456789") + + append_request = self.thread.server.chat_stream_requests.get("/chat.appendStream", {}) + self.assertEqual(append_request.get("channel"), "C0123456789") + self.assertEqual(append_request.get("markdown_text"), "e is bold!") + self.assertEqual(append_request.get("token"), "xoxb-chat_stream_test_token1") + self.assertEqual(append_request.get("ts"), "123.123") + + stop_request = self.thread.server.chat_stream_requests.get("/chat.stopStream", {}) + self.assertEqual( + json.dumps(stop_request.get("blocks")), + '[{"elements": [{"negative_button": {"text": {"emoji": true, "text": "bad", "type": "plain_text"}, "value": "-1"}, "positive_button": {"text": {"emoji": true, "text": "good", "type": "plain_text"}, "value": "+1"}, "type": "feedback_buttons"}, {"icon": "trash", "text": {"emoji": true, "text": "delete", "type": "plain_text"}, "type": "icon_button"}], "type": "context_actions"}]', + ) + self.assertEqual(stop_request.get("channel"), "C0123456789") + self.assertEqual(stop_request.get("markdown_text"), "**") + self.assertEqual(stop_request.get("token"), "xoxb-chat_stream_test_token2") + self.assertEqual(stop_request.get("ts"), "123.123") + + @async_test + async def test_streams_errors_when_appending_to_an_unstarted_stream(self): + streamer = await self.client.chat_stream( + channel="C0123456789", + thread_ts="123.000", + token="xoxb-chat_stream_test_missing_ts", + ) + with self.assertRaisesRegex(SlackRequestError, r"^Failed to stop stream: stream not started$"): + await streamer.stop() + + @async_test + async def test_streams_errors_when_appending_to_a_completed_stream(self): + streamer = await self.client.chat_stream( + channel="C0123456789", + thread_ts="123.000", + ) + await streamer.append(markdown_text="nice!") + await streamer.stop() + with self.assertRaisesRegex(SlackRequestError, r"^Cannot append to stream: stream state is completed$"): + await streamer.append(markdown_text="more...") + with self.assertRaisesRegex(SlackRequestError, r"^Cannot stop stream: stream state is completed$"): + await streamer.stop() diff --git a/tests/slack_sdk_async/web/test_async_slack_response.py b/tests/slack_sdk_async/web/test_async_slack_response.py new file mode 100644 index 000000000..f2820375b --- /dev/null +++ b/tests/slack_sdk_async/web/test_async_slack_response.py @@ -0,0 +1,43 @@ +import unittest + +from slack_sdk.web.async_slack_response import AsyncSlackResponse +from slack_sdk.web.async_client import AsyncWebClient + + +class TestAsyncSlackResponse(unittest.TestCase): + def setUp(self): + pass + + def tearDown(self): + pass + + # https://github.com/slackapi/python-slack-sdk/issues/1100 + def test_issue_1100(self): + response = AsyncSlackResponse( + client=AsyncWebClient(token="xoxb-dummy"), + http_verb="POST", + api_url="http://localhost:3000/api.test", + req_args={}, + data=None, + headers={}, + status_code=200, + ) + with self.assertRaises(ValueError): + response["foo"] + + foo = response.get("foo") + self.assertIsNone(foo) + + # https://github.com/slackapi/python-slack-sdk/issues/1102 + def test_issue_1102(self): + response = AsyncSlackResponse( + client=AsyncWebClient(token="xoxb-dummy"), + http_verb="POST", + api_url="http://localhost:3000/api.test", + req_args={}, + data={"ok": True, "args": {"hello": "world"}}, + headers={}, + status_code=200, + ) + self.assertTrue("ok" in response) + self.assertTrue("foo" not in response) diff --git a/tests/slack_sdk_async/web/test_async_web_client.py b/tests/slack_sdk_async/web/test_async_web_client.py new file mode 100644 index 000000000..e48ad0941 --- /dev/null +++ b/tests/slack_sdk_async/web/test_async_web_client.py @@ -0,0 +1,169 @@ +import re +import unittest + +import slack_sdk.errors as err +from slack_sdk.models.blocks import DividerBlock +from slack_sdk.web.async_client import AsyncWebClient +from tests.slack_sdk_async.helpers import async_test +from tests.slack_sdk.web.mock_web_api_handler import MockHandler +from tests.mock_web_api_server import setup_mock_web_api_server_async, cleanup_mock_web_api_server_async + + +class TestAsyncWebClient(unittest.TestCase): + def setUp(self): + setup_mock_web_api_server_async(self, MockHandler) + self.client = AsyncWebClient( + token="xoxp-1234", + base_url="http://localhost:8888", + ) + + def tearDown(self): + cleanup_mock_web_api_server_async(self) + + pattern_for_language = re.compile("python/(\\S+)", re.IGNORECASE) + pattern_for_package_identifier = re.compile("slackclient/(\\S+)") + + @async_test + async def test_api_calls_return_a_future(self): + self.client.token = "xoxb-api_test" + resp = await self.client.api_test() + self.assertEqual(200, resp.status_code) + self.assertTrue(resp["ok"]) + + @async_test + async def test_requests_can_be_paginated(self): + self.client.token = "xoxb-users_list_pagination" + users = [] + async for page in await self.client.users_list(limit=2): + users = users + page["members"] + self.assertTrue(len(users) == 4) + + @async_test + async def test_request_pagination_stops_when_next_cursor_is_missing(self): + self.client.token = "xoxb-users_list_pagination_1" + users = [] + async for page in await self.client.users_list(limit=2): + users = users + page["members"] + self.assertTrue(len(users) == 2) + + @async_test + async def test_json_can_only_be_sent_with_post_requests(self): + with self.assertRaises(err.SlackRequestError): + await self.client.api_call("fake.method", http_verb="GET", json={}) + + @async_test + async def test_slack_api_error_is_raised_on_unsuccessful_responses(self): + self.client.token = "xoxb-api_test_false" + with self.assertRaises(err.SlackApiError): + await self.client.api_test() + self.client.token = "xoxb-500" + with self.assertRaises(err.SlackApiError): + await self.client.api_test() + + @async_test + async def test_slack_api_rate_limiting_exception_returns_retry_after(self): + self.client.token = "xoxb-ratelimited" + try: + await self.client.api_test() + except err.SlackApiError as slack_api_error: + self.assertFalse(slack_api_error.response["ok"]) + self.assertEqual(429, slack_api_error.response.status_code) + self.assertEqual(1, int(slack_api_error.response.headers["retry-after"])) + self.assertEqual(1, int(slack_api_error.response.headers["Retry-After"])) + + @async_test + async def test_the_api_call_files_argument_creates_the_expected_data(self): + self.client.token = "xoxb-users_setPhoto" + resp = await self.client.users_setPhoto(image="tests/slack_sdk_fixture/slack_logo.png") + self.assertEqual(200, resp.status_code) + + @async_test + async def test_issue_560_bool_in_params_sync(self): + self.client.token = "xoxb-conversations_list" + await self.client.conversations_list(exclude_archived=1) # ok + await self.client.conversations_list(exclude_archived="true") # ok + await self.client.conversations_list(exclude_archived=True) # ok + + @async_test + async def test_issue_690_oauth_v2_access_async(self): + self.client.token = "" + resp = await self.client.oauth_v2_access( + client_id="111.222", + client_secret="secret", + code="codeeeeeeeeee", + ) + self.assertIsNone(resp["error"]) + with self.assertRaises(err.SlackApiError): + await self.client.oauth_v2_access( + client_id="999.999", + client_secret="secret", + code="codeeeeeeeeee", + ) + + @async_test + async def test_issue_690_oauth_access_async(self): + self.client.token = "" + resp = await self.client.oauth_access(client_id="111.222", client_secret="secret", code="codeeeeeeeeee") + self.assertIsNone(resp["error"]) + with self.assertRaises(err.SlackApiError): + await self.client.oauth_access(client_id="999.999", client_secret="secret", code="codeeeeeeeeee") + + @async_test + async def test_token_param_async(self): + with self.assertRaises(err.SlackApiError): + await self.client.users_list() + resp = await self.client.users_list(token="xoxb-users_list_pagination") + self.assertIsNone(resp["error"]) + with self.assertRaises(err.SlackApiError): + await self.client.users_list() + + @async_test + async def test_timeout_issue_712_async(self): + client = AsyncWebClient( + token="xoxp-1234", + base_url="http://localhost:8888", + timeout=1, + ) + with self.assertRaises(Exception): + await client.users_list(token="xoxb-timeout") + + @async_test + async def test_html_response_body_issue_718_async(self): + try: + await self.client.users_list(token="xoxb-html_response") + self.fail("SlackApiError expected here") + except err.SlackApiError as e: + self.assertEqual( + "The request to the Slack API failed. (url: http://localhost:8888/users.list, status: 404)\n" + "The server responded with: {}", + str(e), + ) + + @async_test + async def test_user_agent_customization_issue_769_async(self): + client = AsyncWebClient( + base_url="http://localhost:8888", + token="xoxb-user-agent this_is test", + user_agent_prefix="this_is", + user_agent_suffix="test", + ) + resp = await client.api_test() + self.assertTrue(resp["ok"]) + + @async_test + async def test_default_team_id(self): + client = AsyncWebClient(base_url="http://localhost:8888", team_id="T_DEFAULT") + resp = await client.users_list(token="xoxb-users_list_pagination") + self.assertIsNone(resp["error"]) + + @async_test + async def test_user_auth_blocks(self): + self.client.token = "xoxb-api_test" + client = self.client + new_message = await client.chat_unfurl( + channel="C12345", + ts="1111.2222", + unfurls={}, + user_auth_blocks=[DividerBlock(), DividerBlock()], + ) + self.assertIsNone(new_message.get("error")) diff --git a/tests/slack_sdk_async/web/test_async_web_client_http_retry.py b/tests/slack_sdk_async/web/test_async_web_client_http_retry.py new file mode 100644 index 000000000..dac633aeb --- /dev/null +++ b/tests/slack_sdk_async/web/test_async_web_client_http_retry.py @@ -0,0 +1,111 @@ +import unittest + +import slack_sdk.errors as err +from slack_sdk.http_retry.builtin_async_handlers import AsyncRateLimitErrorRetryHandler, AsyncServerErrorRetryHandler +from slack_sdk.web.async_client import AsyncWebClient +from tests.slack_sdk_async.helpers import async_test +from tests.slack_sdk.web.mock_web_api_handler import MockHandler +from tests.mock_web_api_server import setup_mock_web_api_server_async, cleanup_mock_web_api_server_async +from ..fatal_error_retry_handler import FatalErrorRetryHandler +from ..my_retry_handler import MyRetryHandler + + +class TestAsyncWebClient_HttpRetries(unittest.TestCase): + def setUp(self): + setup_mock_web_api_server_async(self, MockHandler) + + def tearDown(self): + cleanup_mock_web_api_server_async(self) + + @async_test + async def test_remote_disconnected(self): + retry_handler = MyRetryHandler(max_retry_count=2) + client = AsyncWebClient( + base_url="http://localhost:8888", + token="xoxb-remote_disconnected", + team_id="T111", + retry_handlers=[retry_handler], + ) + try: + await client.auth_test() + self.fail("An exception is expected") + except Exception as _: + pass + + self.assertEqual(2, retry_handler.call_count) + + @async_test + async def test_ratelimited_no_retry(self): + client = AsyncWebClient( + base_url="http://localhost:8888", + token="xoxb-ratelimited", + team_id="T111", + ) + try: + await client.auth_test() + self.fail("An exception is expected") + except err.SlackApiError as e: + # Just running retries; no assertions for call count so far + self.assertEqual(429, e.response.status_code) + + @async_test + async def test_ratelimited(self): + client = AsyncWebClient( + base_url="http://localhost:8888", + token="xoxb-ratelimited_only_once", + team_id="T111", + ) + client.retry_handlers.append(AsyncRateLimitErrorRetryHandler()) + # The auto-retry should work here + await client.auth_test() + + @async_test + async def test_fatal_error_no_retry(self): + client = AsyncWebClient( + base_url="http://localhost:8888", + token="xoxb-fatal_error", + team_id="T111", + ) + try: + await client.auth_test() + self.fail("An exception is expected") + except err.SlackApiError as e: + self.assertEqual("fatal_error", e.response["error"]) + + @async_test + async def test_fatal_error(self): + client = AsyncWebClient( + base_url="http://localhost:8888", + token="xoxb-fatal_error_only_once", + team_id="T111", + ) + client.retry_handlers.append(FatalErrorRetryHandler()) + # The auto-retry should work here + await client.auth_test() + + @async_test + async def test_retries(self): + retry_handler = MyRetryHandler(max_retry_count=2) + client = AsyncWebClient( + token="xoxp-remote_disconnected", + base_url="http://localhost:8888", + retry_handlers=[retry_handler], + ) + try: + await client.auth_test() + self.fail("An exception is expected") + except Exception as _: + pass + + self.assertEqual(2, retry_handler.call_count) + + @async_test + async def test_server_error(self): + client = AsyncWebClient( + base_url="http://localhost:8888", + token="xoxb-server_error_only_once", + team_id="T111", + ) + client.retry_handlers.append(AsyncServerErrorRetryHandler()) + # The auto-retry should work here + await client.chat_postMessage(channel="C123", text="Hi there!") diff --git a/tests/slack_sdk_async/web/test_async_web_client_logger.py b/tests/slack_sdk_async/web/test_async_web_client_logger.py new file mode 100644 index 000000000..1a267cb72 --- /dev/null +++ b/tests/slack_sdk_async/web/test_async_web_client_logger.py @@ -0,0 +1,49 @@ +import logging +import unittest + +from slack_sdk.web.async_client import AsyncWebClient +from slack_sdk.web import async_base_client +from tests.helpers import create_copy +from tests.slack_sdk.web.mock_web_api_handler import MockHandler +from tests.mock_web_api_server import setup_mock_web_api_server_async, cleanup_mock_web_api_server_async + + +class TestAsyncWebClientLogger(unittest.TestCase): + test_logger: logging.Logger + + def setUp(self): + self.test_logger = logging.getLogger("test-logger") + setup_mock_web_api_server_async(self, MockHandler) + + def tearDown(self): + cleanup_mock_web_api_server_async(self) + + def test_logger_property_returns_default_logger(self): + client = AsyncWebClient(base_url="http://localhost:8888", token="xoxb-api_test") + self.assertEqual(client.logger.name, async_base_client.__name__) + + def test_logger_property_returns_custom_logger(self): + client = AsyncWebClient( + base_url="http://localhost:8888", + token="xoxb-api_test", + logger=self.test_logger, + ) + self.assertEqual(client.logger, self.test_logger) + + def test_logger_property_has_no_setter(self): + client = AsyncWebClient( + base_url="http://localhost:8888", + token="xoxb-api_test", + ) + with self.assertRaises(AttributeError): + client.logger = self.test_logger + + def test_ensure_async_web_client_with_logger_is_copyable(self): + client = AsyncWebClient( + base_url="http://localhost:8888", + token="xoxb-api_test", + logger=self.test_logger, + ) + client_copy = create_copy(client) + self.assertEqual(client.logger, self.test_logger) + self.assertEqual(client_copy.logger, self.test_logger) diff --git a/tests/slack_sdk_async/web/test_web_client_coverage.py b/tests/slack_sdk_async/web/test_web_client_coverage.py new file mode 100644 index 000000000..0a3c1687b --- /dev/null +++ b/tests/slack_sdk_async/web/test_web_client_coverage.py @@ -0,0 +1,1165 @@ +import os +import unittest + +import slack_sdk.errors as e +from slack_sdk.models.blocks import DividerBlock +from slack_sdk.models.views import View +from slack_sdk.web import WebClient +from slack_sdk.web.async_client import AsyncWebClient +from slack_sdk.web.legacy_client import LegacyWebClient +from tests.mock_web_api_server import cleanup_mock_web_api_server_async, setup_mock_web_api_server_async +from tests.slack_sdk.web.mock_web_api_handler import MockHandler +from tests.slack_sdk_async.helpers import async_test + + +class TestWebClientCoverage(unittest.TestCase): + # 295 endpoints as of September 17, 2025 + # Can be fetched by running `var methodNames = [].slice.call(document.getElementsByClassName('apiReferenceFilterableList__listItemLink')).map(e => e.href.replace("https://api.slack.com/methods/", ""));console.log(methodNames.toString());console.log(methodNames.length);` on https://api.slack.com/methods + all_api_methods = "admin.analytics.getFile,admin.apps.activities.list,admin.apps.approve,admin.apps.clearResolution,admin.apps.restrict,admin.apps.uninstall,admin.apps.approved.list,admin.apps.config.lookup,admin.apps.config.set,admin.apps.requests.cancel,admin.apps.requests.list,admin.apps.restricted.list,admin.audit.anomaly.allow.getItem,admin.audit.anomaly.allow.updateItem,admin.auth.policy.assignEntities,admin.auth.policy.getEntities,admin.auth.policy.removeEntities,admin.barriers.create,admin.barriers.delete,admin.barriers.list,admin.barriers.update,admin.conversations.archive,admin.conversations.bulkArchive,admin.conversations.bulkDelete,admin.conversations.bulkMove,admin.conversations.convertToPrivate,admin.conversations.convertToPublic,admin.conversations.create,admin.conversations.createForObjects,admin.conversations.delete,admin.conversations.disconnectShared,admin.conversations.getConversationPrefs,admin.conversations.getCustomRetention,admin.conversations.getTeams,admin.conversations.invite,admin.conversations.linkObjects,admin.conversations.lookup,admin.conversations.removeCustomRetention,admin.conversations.rename,admin.conversations.search,admin.conversations.setConversationPrefs,admin.conversations.setCustomRetention,admin.conversations.setTeams,admin.conversations.unarchive,admin.conversations.unlinkObjects,admin.conversations.ekm.listOriginalConnectedChannelInfo,admin.conversations.restrictAccess.addGroup,admin.conversations.restrictAccess.listGroups,admin.conversations.restrictAccess.removeGroup,admin.emoji.add,admin.emoji.addAlias,admin.emoji.list,admin.emoji.remove,admin.emoji.rename,admin.functions.list,admin.functions.permissions.lookup,admin.functions.permissions.set,admin.inviteRequests.approve,admin.inviteRequests.deny,admin.inviteRequests.list,admin.inviteRequests.approved.list,admin.inviteRequests.denied.list,admin.roles.addAssignments,admin.roles.listAssignments,admin.roles.removeAssignments,admin.teams.admins.list,admin.teams.create,admin.teams.list,admin.teams.owners.list,admin.teams.settings.info,admin.teams.settings.setDefaultChannels,admin.teams.settings.setDescription,admin.teams.settings.setDiscoverability,admin.teams.settings.setIcon,admin.teams.settings.setName,admin.usergroups.addChannels,admin.usergroups.addTeams,admin.usergroups.listChannels,admin.usergroups.removeChannels,admin.users.assign,admin.users.invite,admin.users.list,admin.users.remove,admin.users.setAdmin,admin.users.setExpiration,admin.users.setOwner,admin.users.setRegular,admin.users.session.clearSettings,admin.users.session.getSettings,admin.users.session.invalidate,admin.users.session.list,admin.users.session.reset,admin.users.session.resetBulk,admin.users.session.setSettings,admin.users.unsupportedVersions.export,admin.workflows.collaborators.add,admin.workflows.collaborators.remove,admin.workflows.permissions.lookup,admin.workflows.search,admin.workflows.unpublish,admin.workflows.triggers.types.permissions.lookup,admin.workflows.triggers.types.permissions.set,api.test,apps.activities.list,apps.auth.external.delete,apps.auth.external.get,apps.connections.open,apps.uninstall,apps.datastore.bulkDelete,apps.datastore.bulkGet,apps.datastore.bulkPut,apps.datastore.count,apps.datastore.delete,apps.datastore.get,apps.datastore.put,apps.datastore.query,apps.datastore.update,apps.event.authorizations.list,apps.manifest.create,apps.manifest.delete,apps.manifest.export,apps.manifest.update,apps.manifest.validate,assistant.search.context,assistant.threads.setStatus,assistant.threads.setSuggestedPrompts,assistant.threads.setTitle,auth.revoke,auth.test,auth.teams.list,bookmarks.add,bookmarks.edit,bookmarks.list,bookmarks.remove,bots.info,calls.add,calls.end,calls.info,calls.update,calls.participants.add,calls.participants.remove,canvases.access.delete,canvases.access.set,canvases.create,canvases.delete,canvases.edit,canvases.sections.lookup,channels.mark,chat.appendStream,chat.delete,chat.deleteScheduledMessage,chat.getPermalink,chat.meMessage,chat.postEphemeral,chat.postMessage,chat.scheduleMessage,chat.scheduledMessages.list,chat.startStream,chat.stopStream,chat.unfurl,chat.update,conversations.acceptSharedInvite,conversations.approveSharedInvite,conversations.archive,conversations.close,conversations.create,conversations.declineSharedInvite,conversations.history,conversations.info,conversations.invite,conversations.inviteShared,conversations.join,conversations.kick,conversations.leave,conversations.list,conversations.listConnectInvites,conversations.mark,conversations.members,conversations.open,conversations.rename,conversations.replies,conversations.setPurpose,conversations.setTopic,conversations.unarchive,conversations.canvases.create,conversations.externalInvitePermissions.set,conversations.requestSharedInvite.approve,conversations.requestSharedInvite.deny,conversations.requestSharedInvite.list,dialog.open,dnd.endDnd,dnd.endSnooze,dnd.info,dnd.setSnooze,dnd.teamInfo,emoji.list,files.completeUploadExternal,files.delete,files.getUploadURLExternal,files.info,files.list,files.revokePublicURL,files.sharedPublicURL,files.upload,files.comments.delete,files.remote.add,files.remote.info,files.remote.list,files.remote.remove,files.remote.share,files.remote.update,functions.completeError,functions.completeSuccess,functions.distributions.permissions.add,functions.distributions.permissions.list,functions.distributions.permissions.remove,functions.distributions.permissions.set,functions.workflows.steps.list,functions.workflows.steps.responses.export,groups.mark,migration.exchange,oauth.access,oauth.v2.access,oauth.v2.exchange,openid.connect.token,openid.connect.userInfo,pins.add,pins.list,pins.remove,reactions.add,reactions.get,reactions.list,reactions.remove,reminders.add,reminders.complete,reminders.delete,reminders.info,reminders.list,rtm.connect,rtm.start,search.all,search.files,search.messages,slackLists.access.delete,slackLists.access.set,slackLists.create,slackLists.download.get,slackLists.download.start,slackLists.items.create,slackLists.items.delete,slackLists.items.delete,slackLists.items.deleteMultiple,slackLists.items.info,slackLists.items.list,slackLists.items.update,slackLists.update,stars.add,stars.list,stars.remove,team.accessLogs,team.billableInfo,team.info,team.integrationLogs,team.billing.info,team.externalTeams.disconnect,team.externalTeams.list,team.preferences.list,team.profile.get,tooling.tokens.rotate,usergroups.create,usergroups.disable,usergroups.enable,usergroups.list,usergroups.update,usergroups.users.list,usergroups.users.update,users.conversations,users.deletePhoto,users.getPresence,users.identity,users.info,users.list,users.lookupByEmail,users.setActive,users.setPhoto,users.setPresence,users.discoverableContacts.lookup,users.profile.get,users.profile.set,views.open,views.publish,views.push,views.update,workflows.featured.add,workflows.featured.list,workflows.featured.remove,workflows.featured.set,workflows.stepCompleted,workflows.stepFailed,workflows.updateStep,workflows.triggers.permissions.add,workflows.triggers.permissions.list,workflows.triggers.permissions.remove,workflows.triggers.permissions.set,im.list,im.mark,mpim.list,mpim.mark".split( + "," + ) + + api_methods_to_call = [] + os.environ.setdefault("SLACKCLIENT_SKIP_DEPRECATION", "1") + + def setUp(self): + setup_mock_web_api_server_async(self, MockHandler) + self.client = WebClient(token="xoxb-coverage", base_url="http://localhost:8888") + self.legacy_client = LegacyWebClient(token="xoxb-coverage", base_url="http://localhost:8888") + self.async_client = AsyncWebClient(token="xoxb-coverage", base_url="http://localhost:8888") + + self.api_methods_to_call = [] + for api_method in self.all_api_methods: + if api_method.startswith("apps.permissions.") or api_method in [ + "apps.connections.open", # app-level token + "oauth.access", + "oauth.v2.access", + "oauth.v2.exchange", + "oauth.token", + "openid.connect.token", + "openid.connect.userInfo", + "users.setActive", + # automation platform token required ones + "apps.activities.list", + "apps.auth.external.delete", + "apps.auth.external.get", + "apps.datastore.delete", + "apps.datastore.get", + "apps.datastore.put", + "apps.datastore.query", + "apps.datastore.update", + "apps.datastore.bulkDelete", + "apps.datastore.bulkGet", + "apps.datastore.bulkPut", + "apps.datastore.count", + "functions.workflows.steps.list", + "functions.workflows.steps.responses.export", + "functions.distributions.permissions.add", + "functions.distributions.permissions.list", + "functions.distributions.permissions.remove", + "functions.distributions.permissions.set", + "workflows.triggers.permissions.add", + "workflows.triggers.permissions.list", + "workflows.triggers.permissions.remove", + "workflows.triggers.permissions.set", + "admin.workflows.triggers.types.permissions.lookup", + "admin.workflows.triggers.types.permissions.set", + # TODO: admin.audit.anomaly.allow.* / The endpoints requires a "session" token + "admin.audit.anomaly.allow.getItem", + "admin.audit.anomaly.allow.updateItem", + "assistant.search.context", # TODO: add this method in follow up PR + "conversations.requestSharedInvite.list", # TODO: add this method in follow up PR + ]: + continue + self.api_methods_to_call.append(api_method) + + def tearDown(self): + cleanup_mock_web_api_server_async(self) + + async def run_method(self, method_name, method, async_method): + # Run the api calls with required arguments + if callable(method): + if method_name == "admin_analytics_getFile": + self.api_methods_to_call.remove(method(date="2020-09-01", type="member")["method"]) + await async_method(date="2020-09-01", type="member") + elif method_name == "admin_apps_approve": + self.api_methods_to_call.remove(method(app_id="AID123", request_id="RID123")["method"]) + await async_method(app_id="AID123", request_id="RID123") + elif method_name == "admin_apps_restrict": + self.api_methods_to_call.remove(method(app_id="AID123", request_id="RID123")["method"]) + await async_method(app_id="AID123", request_id="RID123") + elif method_name == "admin_apps_uninstall": + self.api_methods_to_call.remove( + 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") + elif method_name == "admin_apps_clearResolution": + self.api_methods_to_call.remove(method(app_id="AID123")["method"]) + await async_method(app_id="AID123") + elif method_name == "admin_apps_activities_list": + self.api_methods_to_call.remove(method()["method"]) + await async_method() + elif method_name == "admin_apps_config_lookup": + self.api_methods_to_call.remove(method(app_ids=["A111"])["method"]) + await async_method(app_ids=["A111"]) + elif method_name == "admin_apps_config_set": + self.api_methods_to_call.remove(method(app_id="A111")["method"]) + await async_method(app_id="A111") + elif method_name == "admin_auth_policy_getEntities": + self.api_methods_to_call.remove(method(policy_name="policyname")["method"]) + await async_method(policy_name="policyname") + elif method_name == "admin_auth_policy_assignEntities": + self.api_methods_to_call.remove( + method( + entity_ids=["1", "2"], + entity_type="type", + policy_name="policyname", + )["method"] + ) + await async_method(entity_ids=["1", "2"], entity_type="type", policy_name="policyname") + elif method_name == "admin_auth_policy_removeEntities": + self.api_methods_to_call.remove( + method( + entity_ids=["1", "2"], + entity_type="type", + policy_name="policyname", + )["method"] + ) + await async_method(entity_ids=["1", "2"], entity_type="type", policy_name="policyname") + elif method_name == "admin_conversations_createForObjects": + self.api_methods_to_call.remove( + method( + object_id="0019000000DmehKAAR", + salesforce_org_id="00DGC00000024hsuWY", + )["method"] + ) + await async_method(object_id="0019000000DmehKAAR", salesforce_org_id="00DGC00000024hsuWY") + elif method_name == "admin_conversations_linkObjects": + self.api_methods_to_call.remove( + method( + channel="C1234567890", + record_id="0019000000DmehKAAR", + salesforce_org_id="00DGC00000024hsuWY", + )["method"] + ) + await async_method( + channel="C1234567890", record_id="0019000000DmehKAAR", salesforce_org_id="00DGC00000024hsuWY" + ) + elif method_name == "admin_conversations_unlinkObjects": + self.api_methods_to_call.remove( + method( + channel="C1234567890", + new_name="new-channel-name", + )["method"] + ) + await async_method(channel="C1234567890", new_name="new-channel-name") + elif method_name == "admin_barriers_create": + self.api_methods_to_call.remove( + method( + barriered_from_usergroup_ids=["AAA"], + primary_usergroup_id="AAA", + restricted_subjects=["AAA"], + )["method"] + ) + await async_method( + barriered_from_usergroup_ids=["AAA"], + primary_usergroup_id="AAA", + restricted_subjects=["AAA"], + ) + elif method_name == "admin_barriers_update": + self.api_methods_to_call.remove( + method( + barrier_id="AAA", + barriered_from_usergroup_ids=["AAA"], + primary_usergroup_id="AAA", + restricted_subjects=["AAA"], + )["method"] + ) + await async_method( + barrier_id="AAA", + barriered_from_usergroup_ids=["AAA"], + primary_usergroup_id="AAA", + restricted_subjects=["AAA"], + ) + elif method_name == "admin_barriers_delete": + self.api_methods_to_call.remove(method(barrier_id="AAA")["method"]) + await async_method(barrier_id="AAA") + elif method_name == "admin_emoji_add": + self.api_methods_to_call.remove(method(name="eyes", url="https://www.example.com/")["method"]) + await async_method(name="eyes", url="https://www.example.com/") + elif method_name == "admin_emoji_addAlias": + self.api_methods_to_call.remove(method(name="watching", alias_for="eyes")["method"]) + await async_method(name="watching", alias_for="eyes") + elif method_name == "admin_emoji_remove": + self.api_methods_to_call.remove(method(name="eyes")["method"]) + await async_method(name="eyes") + elif method_name == "admin_emoji_rename": + self.api_methods_to_call.remove(method(name="eyes", new_name="eyez")["method"]) + await async_method(name="eyes", new_name="eyez") + elif method_name == "admin_functions_list": + self.api_methods_to_call.remove(method(app_ids=["A111"])["method"]) + await async_method(app_ids=["A111"]) + elif method_name == "admin_functions_permissions_lookup": + self.api_methods_to_call.remove(method(function_ids=["A111"])["method"]) + await async_method(function_ids=["A111"]) + elif method_name == "admin_functions_permissions_set": + self.api_methods_to_call.remove(method(function_id="F", visibility="everyone")["method"]) + await async_method(function_id="F", visibility="everyone") + elif method_name == "admin_inviteRequests_approve": + self.api_methods_to_call.remove(method(invite_request_id="ID123")["method"]) + await async_method(invite_request_id="ID123") + elif method_name == "admin_inviteRequests_deny": + self.api_methods_to_call.remove(method(invite_request_id="ID123")["method"]) + await async_method(invite_request_id="ID123") + elif method_name == "admin_roles_addAssignments": + self.api_methods_to_call.remove(method(entity_ids=["X"], user_ids=["U"], role_id="R")["method"]) + await async_method(entity_ids=["X"], user_ids=["U"], role_id="R") + elif method_name == "admin_roles_listAssignments": + self.api_methods_to_call.remove(method()["method"]) + await async_method() + elif method_name == "admin_roles_removeAssignments": + self.api_methods_to_call.remove(method(entity_ids=["X"], user_ids=["U"], role_id="R")["method"]) + await async_method(entity_ids=["X"], user_ids=["U"], role_id="R") + elif method_name == "admin_teams_admins_list": + self.api_methods_to_call.remove(method(team_id="T123")["method"]) + await async_method(team_id="T123") + elif method_name == "admin_teams_create": + self.api_methods_to_call.remove(method(team_domain="awesome-team", team_name="Awesome Team")["method"]) + await async_method(team_domain="awesome-team", team_name="Awesome Team") + elif method_name == "admin_teams_owners_list": + self.api_methods_to_call.remove(method(team_id="T123")["method"]) + await async_method(team_id="T123") + elif method_name == "admin_teams_settings_info": + self.api_methods_to_call.remove(method(team_id="T123")["method"]) + await async_method(team_id="T123") + elif method_name == "admin_teams_settings_setDefaultChannels": + self.api_methods_to_call.remove(method(team_id="T123", channel_ids=["C123", "C234"])["method"]) + # checking tuple compatibility as sample + method(team_id="T123", channel_ids=("C123", "C234")) + method(team_id="T123", channel_ids="C123,C234") + await async_method(team_id="T123", channel_ids="C123,C234") + elif method_name == "admin_teams_settings_setDescription": + self.api_methods_to_call.remove( + method(team_id="T123", description="Workspace for an awesome team")["method"] + ) + await async_method(team_id="T123", description="Workspace for an awesome team") + elif method_name == "admin_teams_settings_setDiscoverability": + self.api_methods_to_call.remove(method(team_id="T123", discoverability="invite_only")["method"]) + await async_method(team_id="T123", discoverability="invite_only") + elif method_name == "admin_teams_settings_setIcon": + self.api_methods_to_call.remove( + method( + team_id="T123", + image_url="https://www.example.com/images/dummy.png", + )["method"] + ) + await async_method( + team_id="T123", + image_url="https://www.example.com/images/dummy.png", + ) + elif method_name == "admin_teams_settings_setName": + self.api_methods_to_call.remove(method(team_id="T123", name="Awesome Engineering Team")["method"]) + await async_method(team_id="T123", name="Awesome Engineering Team") + elif method_name == "admin_usergroups_addChannels": + self.api_methods_to_call.remove( + method( + team_id="T123", + usergroup_id="S123", + channel_ids=["C1A2B3C4D", "C26Z25Y24"], + )["method"] + ) + method( + team_id="T123", + usergroup_id="S123", + channel_ids="C1A2B3C4D,C26Z25Y24", + ) + await async_method( + team_id="T123", + usergroup_id="S123", + channel_ids=["C1A2B3C4D", "C26Z25Y24"], + ) + elif method_name == "admin_usergroups_addTeams": + self.api_methods_to_call.remove( + method( + team_id="T123", + usergroup_id="S123", + team_ids=["T111", "T222"], + )["method"] + ) + method( + team_id="T123", + usergroup_id="S123", + team_ids="T111,T222", + ) + await async_method( + team_id="T123", + usergroup_id="S123", + team_ids="T111,T222", + ) + elif method_name == "admin_usergroups_listChannels": + self.api_methods_to_call.remove(method(usergroup_id="S123")["method"]) + method(usergroup_id="S123", include_num_members=True, team_id="T123") + method(usergroup_id="S123", include_num_members="1", team_id="T123") + method(usergroup_id="S123", include_num_members=1, team_id="T123") + method(usergroup_id="S123", include_num_members=False, team_id="T123") + method(usergroup_id="S123", include_num_members="0", team_id="T123") + method(usergroup_id="S123", include_num_members=0, team_id="T123") + await async_method(usergroup_id="S123", include_num_members=0, team_id="T123") + elif method_name == "admin_usergroups_removeChannels": + self.api_methods_to_call.remove( + method( + team_id="T123", + usergroup_id="S123", + channel_ids=["C1A2B3C4D", "C26Z25Y24"], + )["method"] + ) + method( + team_id="T123", + usergroup_id="S123", + channel_ids="C1A2B3C4D,C26Z25Y24", + ) + await async_method( + team_id="T123", + usergroup_id="S123", + channel_ids="C1A2B3C4D,C26Z25Y24", + ) + elif method_name == "admin_users_assign": + self.api_methods_to_call.remove(method(team_id="T123", user_id="W123")["method"]) + await async_method(team_id="T123", user_id="W123") + elif method_name == "admin_users_invite": + self.api_methods_to_call.remove( + method( + team_id="T123", + email="test@example.com", + channel_ids=["C1A2B3C4D", "C26Z25Y24"], + )["method"] + ) + method( + team_id="T123", + email="test@example.com", + channel_ids="C1A2B3C4D,C26Z25Y24", + ) + await async_method( + team_id="T123", + email="test@example.com", + channel_ids="C1A2B3C4D,C26Z25Y24", + ) + elif method_name == "admin_users_list": + self.api_methods_to_call.remove(method(team_id="T123")["method"]) + await async_method(team_id="T123") + elif method_name == "admin_users_remove": + self.api_methods_to_call.remove(method(team_id="T123", user_id="W123")["method"]) + await async_method(team_id="T123", user_id="W123") + elif method_name == "admin_users_setAdmin": + self.api_methods_to_call.remove(method(team_id="T123", user_id="W123")["method"]) + await async_method(team_id="T123", user_id="W123") + elif method_name == "admin_users_setExpiration": + self.api_methods_to_call.remove(method(team_id="T123", user_id="W123", expiration_ts=123)["method"]) + await async_method(team_id="T123", user_id="W123", expiration_ts=123) + elif method_name == "admin_users_setOwner": + self.api_methods_to_call.remove(method(team_id="T123", user_id="W123")["method"]) + await async_method(team_id="T123", user_id="W123") + elif method_name == "admin_users_setRegular": + self.api_methods_to_call.remove(method(team_id="T123", user_id="W123")["method"]) + await async_method(team_id="T123", user_id="W123") + elif method_name == "admin_users_session_invalidate": + self.api_methods_to_call.remove(method(session_id="XXX", team_id="T111")["method"]) + await async_method(session_id="XXX", team_id="T111") + elif method_name == "admin_users_session_reset": + self.api_methods_to_call.remove(method(user_id="W123")["method"]) + await async_method(user_id="W123") + elif method_name == "admin_users_session_resetBulk": + self.api_methods_to_call.remove(method(user_ids=["W123"])["method"]) + method(user_ids="W123,W234") + await async_method(user_ids=["W123"]) + await async_method(user_ids="W123,W234") + elif method_name == "admin_users_session_getSettings": + self.api_methods_to_call.remove(method(user_ids=["W111"])["method"]) + await async_method(user_ids=["W111"]) + elif method_name == "admin_users_session_setSettings": + self.api_methods_to_call.remove(method(user_ids=["W111"])["method"]) + await async_method(user_ids=["W111"]) + elif method_name == "admin_users_session_clearSettings": + self.api_methods_to_call.remove(method(user_ids=["W111"])["method"]) + await async_method(user_ids=["W111"]) + elif method_name == "admin_workflows_search": + self.api_methods_to_call.remove(method()["method"]) + await async_method() + elif method_name == "admin_workflows_permissions_lookup": + self.api_methods_to_call.remove(method(workflow_ids=["W"])["method"]) + await async_method(workflow_ids=["W"]) + elif method_name == "admin_workflows_collaborators_add": + self.api_methods_to_call.remove(method(workflow_ids=["W"], collaborator_ids=["W111"])["method"]) + await async_method(workflow_ids=["W"], collaborator_ids=["W111"]) + elif method_name == "admin_workflows_collaborators_remove": + self.api_methods_to_call.remove(method(workflow_ids=["W"], collaborator_ids=["W111"])["method"]) + await async_method(workflow_ids=["W"], collaborator_ids=["W111"]) + elif method_name == "admin_workflows_unpublish": + self.api_methods_to_call.remove(method(workflow_ids=["W"])["method"]) + await async_method(workflow_ids=["W"]) + elif method_name == "apps_event_authorizations_list": + self.api_methods_to_call.remove(method(event_context="xxx")["method"]) + await async_method(event_context="xxx") + elif method_name == "apps_uninstall": + self.api_methods_to_call.remove(method(client_id="111.222", client_secret="xxx")["method"]) + await async_method(client_id="111.222", client_secret="xxx") + elif method_name == "assistant_threads_setStatus": + self.api_methods_to_call.remove( + method(channel_id="D111", thread_ts="111.222", status="is typing...")["method"] + ) + method( + channel_id="D111", + thread_ts="111.222", + status="is typing...", + loading_states=["Thinking...", "Writing..."], + ) + await async_method(channel_id="D111", thread_ts="111.222", status="is typing...") + await async_method( + channel_id="D111", + thread_ts="111.222", + status="is typing...", + loading_states=["Thinking...", "Writing..."], + ) + elif method_name == "assistant_threads_setTitle": + self.api_methods_to_call.remove(method(channel_id="D111", thread_ts="111.222", title="New chat")["method"]) + await async_method(channel_id="D111", thread_ts="111.222", title="New chat") + elif method_name == "assistant_threads_setSuggestedPrompts": + self.api_methods_to_call.remove( + method(channel_id="D111", thread_ts="111.222", prompts=[{"title": "X", "message": "Y"}])["method"] + ) + await async_method(channel_id="D111", thread_ts="111.222", prompts=[{"title": "X", "message": "Y"}]) + elif method_name == "bookmarks_add": + self.api_methods_to_call.remove(method(channel_id="C1234", title="bedtime story", type="article")["method"]) + await async_method(channel_id="C1234", title="bedtime story", type="article") + elif method_name == "bookmarks_edit": + self.api_methods_to_call.remove(method(bookmark_id="B1234", channel_id="C1234")["method"]) + await async_method(bookmark_id="B1234", channel_id="C1234") + elif method_name == "bookmarks_list": + self.api_methods_to_call.remove(method(channel_id="C1234")["method"]) + await async_method(channel_id="C1234") + elif method_name == "bookmarks_remove": + self.api_methods_to_call.remove(method(bookmark_id="B1234", channel_id="C1234")["method"]) + await async_method(bookmark_id="B1234", channel_id="C1234") + elif method_name == "calls_add": + self.api_methods_to_call.remove( + method( + external_unique_id="unique-id", + join_url="https://www.example.com", + )["method"] + ) + await async_method( + external_unique_id="unique-id", + join_url="https://www.example.com", + ) + elif method_name == "calls_end": + self.api_methods_to_call.remove(method(id="R111")["method"]) + await async_method(id="R111") + elif method_name == "calls_info": + self.api_methods_to_call.remove(method(id="R111")["method"]) + await async_method(id="R111") + elif method_name == "calls_participants_add": + self.api_methods_to_call.remove( + method( + id="R111", + users=[ + {"slack_id": "U1H77"}, + { + "external_id": "54321678", + "display_name": "External User", + "avatar_url": "https://example.com/users/avatar1234.jpg", + }, + ], + )["method"] + ) + await async_method( + id="R111", + users=[ + {"slack_id": "U1H77"}, + { + "external_id": "54321678", + "display_name": "External User", + "avatar_url": "https://example.com/users/avatar1234.jpg", + }, + ], + ) + elif method_name == "calls_participants_remove": + self.api_methods_to_call.remove( + method( + id="R111", + users=[ + {"slack_id": "U1H77"}, + { + "external_id": "54321678", + "display_name": "External User", + "avatar_url": "https://example.com/users/avatar1234.jpg", + }, + ], + )["method"] + ) + await async_method( + id="R111", + users=[ + {"slack_id": "U1H77"}, + { + "external_id": "54321678", + "display_name": "External User", + "avatar_url": "https://example.com/users/avatar1234.jpg", + }, + ], + ) + elif method_name == "calls_update": + self.api_methods_to_call.remove(method(id="R111")["method"]) + await async_method(id="R111") + elif method_name == "canvases_create": + self.api_methods_to_call.remove(method(document_content={})["method"]) + await async_method(document_content={}) + elif method_name == "conversations_canvases_create": + self.api_methods_to_call.remove(method(channel_id="C123", document_content={})["method"]) + await async_method(channel_id="C123", document_content={}) + elif method_name == "conversations_externalInvitePermissions_set": + self.api_methods_to_call.remove(method(action="upgrade", channel="C123", target_team="T123")["method"]) + await async_method(action="upgrade", channel="C123", target_team="T123") + elif method_name == "canvases_access_set": + self.api_methods_to_call.remove(method(canvas_id="F111", access_level="write")["method"]) + await async_method(canvas_id="F111", access_level="write") + elif method_name == "canvases_access_delete": + self.api_methods_to_call.remove(method(canvas_id="F111")["method"]) + await async_method(canvas_id="F111") + elif method_name == "canvases_delete": + self.api_methods_to_call.remove(method(canvas_id="F111")["method"]) + await async_method(canvas_id="F111") + elif method_name == "canvases_edit": + self.api_methods_to_call.remove(method(canvas_id="F111", changes=[])["method"]) + await async_method(canvas_id="F111", changes=[]) + elif method_name == "canvases_sections_lookup": + self.api_methods_to_call.remove(method(canvas_id="F123", criteria={})["method"]) + await async_method(canvas_id="F123", criteria={}) + elif method_name == "chat_appendStream": + self.api_methods_to_call.remove(method(channel="C123", ts="123.123", markdown_text="**bold**")["method"]) + await async_method(channel="C123", ts="123.123", markdown_text="**bold**") + elif method_name == "chat_delete": + self.api_methods_to_call.remove(method(channel="C123", ts="123.123")["method"]) + await async_method(channel="C123", ts="123.123") + elif method_name == "chat_deleteScheduledMessage": + self.api_methods_to_call.remove(method(channel="C123", scheduled_message_id="123")["method"]) + await async_method(channel="C123", scheduled_message_id="123") + elif method_name == "chat_getPermalink": + self.api_methods_to_call.remove(method(channel="C123", message_ts="123.123")["method"]) + await async_method(channel="C123", message_ts="123.123") + elif method_name == "chat_meMessage": + self.api_methods_to_call.remove(method(channel="C123", text=":wave: Hi there!")["method"]) + await async_method(channel="C123", text=":wave: Hi there!") + elif method_name == "chat_postEphemeral": + self.api_methods_to_call.remove(method(channel="C123", user="U123")["method"]) + await async_method(channel="C123", user="U123") + elif method_name == "chat_postMessage": + self.api_methods_to_call.remove(method(channel="C123")["method"]) + await async_method(channel="C123") + elif method_name == "chat_scheduleMessage": + self.api_methods_to_call.remove(method(channel="C123", post_at=123, text="Hi")["method"]) + await async_method(channel="C123", post_at=123, text="Hi") + elif method_name == "chat_scheduledMessages_list": + self.api_methods_to_call.remove(method()["method"]) + await async_method() + elif method_name == "chat_startStream": + self.api_methods_to_call.remove(method(channel="C123", thread_ts="123.123")["method"]) + await async_method(channel="C123", thread_ts="123.123") + method(channel="C123", thread_ts="123.123", recipient_team_id="T123", recipient_user_id="U123") + await async_method(channel="C123", thread_ts="123.123", recipient_team_id="T123", recipient_user_id="U123") + elif method_name == "chat_stopStream": + self.api_methods_to_call.remove( + method(channel="C123", ts="123.123", blocks=[{"type": "markdown", "text": "**twelve**"}])["method"] + ) + await async_method(channel="C123", ts="123.123", blocks=[{"type": "markdown", "text": "**twelve**"}]) + elif method_name == "chat_unfurl": + self.api_methods_to_call.remove( + method( + channel="C123", + ts="123.123", + unfurls={"https://example.com/": {"text": "Every day is the test."}}, + )["method"] + ) + await async_method( + channel="C123", + ts="123.123", + unfurls={"https://example.com/": {"text": "Every day is the test."}}, + ) + method( + source="composer", + unfurl_id="Uxxxxxxx-909b5454-75f8-4ac4-b325-1b40e230bbd8", + unfurls={"https://example.com/": {"text": "Every day is the test."}}, + ) + await async_method( + source="composer", + unfurl_id="Uxxxxxxx-909b5454-75f8-4ac4-b325-1b40e230bbd8", + unfurls={"https://example.com/": {"text": "Every day is the test."}}, + ) + elif method_name == "chat_update": + self.api_methods_to_call.remove(method(channel="C123", ts="123.123")["method"]) + await async_method(channel="C123", ts="123.123") + elif method_name == "conversations_acceptSharedInvite": + self.api_methods_to_call.remove(method(channel_name="test-channel-name", channel_id="C123")["method"]) + method(channel_name="test-channel-name", invite_id="123") + # either invite_id or channel_id supplied or exception + self.assertRaises(e.SlackRequestError, method, channel_name="test-channel-name") + await async_method(channel_name="test-channel-name", channel_id="C123") + await async_method(channel_name="test-channel-name", invite_id="123") + with self.assertRaises(e.SlackRequestError): + await async_method(channel_name="test-channel-name") + elif method_name == "conversations_approveSharedInvite": + self.api_methods_to_call.remove(method(invite_id="123")["method"]) + await async_method(invite_id="123") + elif method_name == "conversations_archive": + self.api_methods_to_call.remove(method(channel="C123")["method"]) + await async_method(channel="C123") + elif method_name == "conversations_close": + self.api_methods_to_call.remove(method(channel="C123")["method"]) + await async_method(channel="C123") + elif method_name == "conversations_open": + self.api_methods_to_call.remove(method(channel="C123")["method"]) + await async_method(channel="C123") + elif method_name == "conversations_create": + self.api_methods_to_call.remove(method(name="announcements")["method"]) + await async_method(name="announcements") + elif method_name == "conversations_declineSharedInvite": + self.api_methods_to_call.remove(method(invite_id="123")["method"]) + await async_method(invite_id="123") + elif method_name == "conversations_history": + self.api_methods_to_call.remove(method(channel="C123")["method"]) + await async_method(channel="C123") + elif method_name == "conversations_info": + self.api_methods_to_call.remove(method(channel="C123")["method"]) + await async_method(channel="C123") + elif method_name == "conversations_invite": + self.api_methods_to_call.remove(method(channel="C123", users=["U2345678901", "U3456789012"])["method"]) + method(channel="C123", users="U2345678901,U3456789012") + await async_method(channel="C123", users=["U2345678901", "U3456789012"]) + elif method_name == "conversations_inviteShared": + self.api_methods_to_call.remove(method(channel="C123", emails="test@example.com")["method"]) + method(channel="C123", emails=["test2@example.com", "test3@example.com"]) + method(channel="C123", user_ids="U2345678901") + method(channel="C123", user_ids=["U2345678901", "U3456789012"]) + self.assertRaises(e.SlackRequestError, method, channel="C123") + await async_method(channel="C123", emails="test@example.com") + with self.assertRaises(e.SlackRequestError): + await async_method(channel="C123") + elif method_name == "conversations_join": + self.api_methods_to_call.remove(method(channel="C123")["method"]) + await async_method(channel="C123") + elif method_name == "conversations_kick": + self.api_methods_to_call.remove(method(channel="C123", user="U123")["method"]) + await async_method(channel="C123", user="U123") + elif method_name == "conversations_listConnectInvites": + self.api_methods_to_call.remove(method()["method"]) + await async_method() + elif method_name == "conversations_leave": + self.api_methods_to_call.remove(method(channel="C123")["method"]) + await async_method(channel="C123") + elif method_name == "conversations_mark": + self.api_methods_to_call.remove(method(channel="C123", ts="123.123")["method"]) + await async_method(channel="C123", ts="123.123") + elif method_name == "conversations_members": + self.api_methods_to_call.remove(method(channel="C123")["method"]) + await async_method(channel="C123") + elif method_name == "conversations_rename": + self.api_methods_to_call.remove(method(channel="C123", name="new-name")["method"]) + await async_method(channel="C123", name="new-name") + elif method_name == "conversations_replies": + self.api_methods_to_call.remove(method(channel="C123", ts="123.123")["method"]) + await async_method(channel="C123", ts="123.123") + elif method_name == "conversations_requestSharedInvite_approve": + self.api_methods_to_call.remove(method(invite_id="I123")["method"]) + await async_method(invite_id="I123") + elif method_name == "conversations_requestSharedInvite_deny": + self.api_methods_to_call.remove(method(invite_id="I123")["method"]) + await async_method(invite_id="I123") + elif method_name == "conversations_setPurpose": + self.api_methods_to_call.remove(method(channel="C123", purpose="The purpose")["method"]) + await async_method(channel="C123", purpose="The purpose") + elif method_name == "conversations_setTopic": + self.api_methods_to_call.remove(method(channel="C123", topic="The topic")["method"]) + await async_method(channel="C123", topic="The topic") + elif method_name == "conversations_unarchive": + self.api_methods_to_call.remove(method(channel="C123")["method"]) + await async_method(channel="C123") + elif method_name == "dialog_open": + self.api_methods_to_call.remove(method(dialog={}, trigger_id="123")["method"]) + await async_method(dialog={}, trigger_id="123") + elif method_name == "dnd_setSnooze": + self.api_methods_to_call.remove(method(num_minutes=120)["method"]) + await async_method(num_minutes=120) + elif method_name == "dnd_teamInfo": + self.api_methods_to_call.remove(method(users=["123", "U234"])["method"]) + method(users="U123,U234") + await async_method(users=["123", "U234"]) + elif method_name == "files_comments_delete": + self.api_methods_to_call.remove(method(file="F123", id="FC123")["method"]) + await async_method(file="F123", id="FC123") + elif method_name == "files_delete": + self.api_methods_to_call.remove(method(file="F123")["method"]) + await async_method(file="F123") + elif method_name == "files_info": + self.api_methods_to_call.remove(method(file="F123")["method"]) + await async_method(file="F123") + elif method_name == "files_revokePublicURL": + self.api_methods_to_call.remove(method(file="F123")["method"]) + await async_method(file="F123") + elif method_name == "files_sharedPublicURL": + self.api_methods_to_call.remove(method(file="F123")["method"]) + await async_method(file="F123") + elif method_name == "files_upload": + self.api_methods_to_call.remove(method(content="This is the content")["method"]) + await async_method(content="This is the content") + elif method_name == "files_remote_add": + self.api_methods_to_call.remove( + method( + external_id="123", + external_url="https://www.example.com/remote-files/123", + title="File title", + )["method"] + ) + await async_method( + external_id="123", + external_url="https://www.example.com/remote-files/123", + title="File title", + ) + elif method_name == "files_remote_share": + self.api_methods_to_call.remove(method(external_id="xxx", channels="C123,G123")["method"]) + method(external_id="xxx", channels=["C123", "G123"]) + method(external_id="xxx", channels="C123,G123") + await async_method(external_id="xxx", channels="C123,G123") + elif method_name == "files_getUploadURLExternal": + self.api_methods_to_call.remove(method(filename="foo.png", length=123)["method"]) + await async_method(filename="foo.png", length=123) + elif method_name == "files_completeUploadExternal": + self.api_methods_to_call.remove(method(files=[{"id": "F111"}])["method"]) + await async_method(files=[{"id": "F111"}]) + elif method_name == "functions_completeSuccess": + self.api_methods_to_call.remove(method(function_execution_id="Fn111", outputs={"num": 123})["method"]) + await async_method(function_execution_id="Fn111", outputs={"num": 123}) + elif method_name == "functions_completeError": + self.api_methods_to_call.remove(method(function_execution_id="Fn111", error="something wrong")["method"]) + await async_method(function_execution_id="Fn111", error="something wrong") + elif method_name == "migration_exchange": + self.api_methods_to_call.remove(method(users="U123,U234")["method"]) + method(users="U123,U234") + await async_method(users="U123,U234") + elif method_name == "mpim_open": + self.api_methods_to_call.remove(method(users="U123,U234")["method"]) + method(users="U123,U234") + await async_method(users="U123,U234") + elif method_name == "pins_add": + self.api_methods_to_call.remove(method(channel="C123")["method"]) + await async_method(channel="C123") + elif method_name == "pins_list": + self.api_methods_to_call.remove(method(channel="C123")["method"]) + await async_method(channel="C123") + elif method_name == "pins_remove": + self.api_methods_to_call.remove(method(channel="C123")["method"]) + await async_method(channel="C123") + elif method_name == "reactions_add": + self.api_methods_to_call.remove(method(name="eyes", channel="C111", timestamp="111.222")["method"]) + await async_method(name="eyes", channel="C111", timestamp="111.222") + elif method_name == "reactions_remove": + self.api_methods_to_call.remove(method(name="eyes")["method"]) + await async_method(name="eyes") + elif method_name == "reminders_add": + self.api_methods_to_call.remove(method(text="The task", time=123)["method"]) + await async_method(text="The task", time=123) + elif method_name == "reminders_complete": + self.api_methods_to_call.remove(method(reminder="R123")["method"]) + await async_method(reminder="R123") + elif method_name == "reminders_delete": + self.api_methods_to_call.remove(method(reminder="R123")["method"]) + await async_method(reminder="R123") + elif method_name == "reminders_info": + self.api_methods_to_call.remove(method(reminder="R123")["method"]) + await async_method(reminder="R123") + elif method_name == "search_all": + self.api_methods_to_call.remove(method(query="Slack")["method"]) + await async_method(query="Slack") + elif method_name == "search_files": + self.api_methods_to_call.remove(method(query="Slack")["method"]) + await async_method(query="Slack") + elif method_name == "search_messages": + self.api_methods_to_call.remove(method(query="Slack")["method"]) + await async_method(query="Slack") + elif method_name == "slackLists_access_delete": + self.api_methods_to_call.remove(method(list_id="123")["method"]) + await async_method(list_id="123") + elif method_name == "slackLists_access_set": + self.api_methods_to_call.remove(method(list_id="123", access_level="private")["method"]) + await async_method(list_id="123", access_level="private") + elif method_name == "slackLists_create": + self.api_methods_to_call.remove(method(name="Backlog")["method"]) + await async_method(name="Backlog") + elif method_name == "slackLists_download_get": + self.api_methods_to_call.remove(method(list_id="123", job_id="123")["method"]) + await async_method(list_id="123", job_id="123") + elif method_name == "slackLists_download_start": + self.api_methods_to_call.remove(method(list_id="123")["method"]) + await async_method(list_id="123") + elif method_name == "slackLists_items_create": + self.api_methods_to_call.remove(method(list_id="123")["method"]) + await async_method(list_id="123") + elif method_name == "slackLists_items_delete": + self.api_methods_to_call.remove(method(list_id="123", id="123")["method"]) + await async_method(list_id="123", id="123") + elif method_name == "slackLists_items_deleteMultiple": + self.api_methods_to_call.remove(method(list_id="123", ids=["123", "456"])["method"]) + await async_method(list_id="123", ids=["123", "456"]) + elif method_name == "slackLists_items_info": + self.api_methods_to_call.remove(method(list_id="123", id="123")["method"]) + await async_method(list_id="123", id="123") + elif method_name == "slackLists_items_list": + self.api_methods_to_call.remove(method(list_id="123")["method"]) + await async_method(list_id="123") + elif method_name == "slackLists_items_update": + self.api_methods_to_call.remove( + method(list_id="123", cells=[{"column_id": "col123"}, {"row_id": "row123"}])["method"] + ) + await async_method(list_id="123", cells=[{"column_id": "col123"}, {"row_id": "row123"}]) + elif method_name == "slackLists_update": + self.api_methods_to_call.remove(method(id="123")["method"]) + await async_method(id="123") + elif method_name == "team_externalTeams_disconnect": + self.api_methods_to_call.remove(method(target_team="T111")["method"]) + await async_method(target_team="T111") + 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") + elif method_name == "usergroups_disable": + self.api_methods_to_call.remove(method(usergroup="UG123")["method"]) + await async_method(usergroup="UG123") + elif method_name == "usergroups_enable": + self.api_methods_to_call.remove(method(usergroup="UG123")["method"]) + await async_method(usergroup="UG123") + elif method_name == "usergroups_update": + self.api_methods_to_call.remove(method(usergroup="UG123")["method"]) + await async_method(usergroup="UG123") + elif method_name == "usergroups_users_list": + self.api_methods_to_call.remove(method(usergroup="UG123")["method"]) + await async_method(usergroup="UG123") + elif method_name == "usergroups_users_update": + self.api_methods_to_call.remove(method(usergroup="UG123", users=["U123", "U234"])["method"]) + method(usergroup="UG123", users="U123,U234") + await async_method(usergroup="UG123", users="U123,U234") + elif method_name == "users_getPresence": + self.api_methods_to_call.remove(method(user="U123")["method"]) + await async_method(user="U123") + elif method_name == "users_info": + self.api_methods_to_call.remove(method(user="U123")["method"]) + await async_method(user="U123") + elif method_name == "users_lookupByEmail": + self.api_methods_to_call.remove(method(email="test@example.com")["method"]) + await async_method(email="test@example.com") + elif method_name == "users_setPhoto": + self.api_methods_to_call.remove(method(image="README.md")["method"]) + await async_method(image="README.md") + elif method_name == "users_setPresence": + self.api_methods_to_call.remove(method(presence="away")["method"]) + await async_method(presence="away") + elif method_name == "views_open": + self.api_methods_to_call.remove(method(trigger_id="123123", view={})["method"]) + method( + trigger_id="123123", + view=View(type="modal", blocks=[DividerBlock()]), + ) + await async_method( + trigger_id="123123", + view=View(type="modal", blocks=[DividerBlock()]), + ) + elif method_name == "views_publish": + self.api_methods_to_call.remove(method(user_id="U123", view={})["method"]) + await async_method(user_id="U123", view={}) + elif method_name == "views_push": + self.api_methods_to_call.remove(method(trigger_id="123123", view={})["method"]) + await async_method(trigger_id="123123", view={}) + elif method_name == "views_update": + self.api_methods_to_call.remove(method(view_id="V123", view={})["method"]) + await async_method(view_id="V123", view={}) + elif method_name == "workflows_featured_add": + self.api_methods_to_call.remove(method(channel_id="C123", trigger_ids=["Ft123", "Ft234"])["method"]) + method(channel_id="C123", trigger_ids="Ft123,Ft234") + await async_method(channel_id="C123", trigger_ids=["Ft123", "Ft234"]) + await async_method(channel_id="C123", trigger_ids="Ft123,Ft234") + elif method_name == "workflows_featured_list": + self.api_methods_to_call.remove(method(channel_ids=["C123", "C234"])["method"]) + method(channel_ids="C123,C234") + await async_method(channel_ids=["C123", "C234"]) + await async_method(channel_ids="C123,C234") + elif method_name == "workflows_featured_remove": + self.api_methods_to_call.remove(method(channel_id="C123", trigger_ids=["Ft123", "Ft234"])["method"]) + method(channel_id="C123", trigger_ids="Ft123,Ft234") + await async_method(channel_id="C123", trigger_ids=["Ft123", "Ft234"]) + await async_method(channel_id="C123", trigger_ids="Ft123,Ft234") + elif method_name == "workflows_featured_set": + self.api_methods_to_call.remove(method(channel_id="C123", trigger_ids=["Ft123", "Ft234"])["method"]) + method(channel_id="C123", trigger_ids="Ft123,Ft234") + await async_method(channel_id="C123", trigger_ids=["Ft123", "Ft234"]) + await async_method(channel_id="C123", trigger_ids="Ft123,Ft234") + elif method_name == "workflows_stepCompleted": + self.api_methods_to_call.remove(method(workflow_step_execute_id="S123", outputs={})["method"]) + await async_method(workflow_step_execute_id="S123", outputs={}) + elif method_name == "workflows_stepFailed": + self.api_methods_to_call.remove(method(workflow_step_execute_id="S456", error={})["method"]) + await async_method(workflow_step_execute_id="S456", error={}) + elif method_name == "workflows_updateStep": + self.api_methods_to_call.remove(method(workflow_step_edit_id="S789", inputs={}, outputs=[])["method"]) + await async_method(workflow_step_edit_id="S789", inputs={}, outputs=[]) + elif method_name == "channels_archive": + self.api_methods_to_call.remove(method(channel="C123")["method"]) + await async_method(channel="C123") + elif method_name == "channels_create": + self.api_methods_to_call.remove(method(name="channel-name")["method"]) + await async_method(name="channel-name") + elif method_name == "channels_history": + self.api_methods_to_call.remove(method(channel="C123")["method"]) + await async_method(channel="C123") + elif method_name == "channels_info": + self.api_methods_to_call.remove(method(channel="C123")["method"]) + await async_method(channel="C123") + elif method_name == "channels_invite": + self.api_methods_to_call.remove(method(channel="C123", user="U123")["method"]) + await async_method(channel="C123", user="U123") + elif method_name == "channels_join": + self.api_methods_to_call.remove(method(name="channel-name")["method"]) + await async_method(name="channel-name") + elif method_name == "channels_kick": + self.api_methods_to_call.remove(method(channel="C123", user="U123")["method"]) + await async_method(channel="C123", user="U123") + elif method_name == "channels_leave": + self.api_methods_to_call.remove(method(channel="C123")["method"]) + await async_method(channel="C123") + elif method_name == "channels_mark": + self.api_methods_to_call.remove(method(channel="C123", ts="123.123")["method"]) + await async_method(channel="C123", ts="123.123") + elif method_name == "channels_rename": + self.api_methods_to_call.remove(method(channel="C123", name="new-name")["method"]) + await async_method(channel="C123", name="new-name") + elif method_name == "channels_replies": + self.api_methods_to_call.remove(method(channel="C123", thread_ts="123.123")["method"]) + await async_method(channel="C123", thread_ts="123.123") + elif method_name == "channels_setPurpose": + self.api_methods_to_call.remove(method(channel="C123", purpose="The purpose")["method"]) + await async_method(channel="C123", purpose="The purpose") + elif method_name == "channels_setTopic": + self.api_methods_to_call.remove(method(channel="C123", topic="The topic")["method"]) + await async_method(channel="C123", topic="The topic") + elif method_name == "channels_unarchive": + self.api_methods_to_call.remove(method(channel="C123")["method"]) + await async_method(channel="C123") + elif method_name == "groups_archive": + self.api_methods_to_call.remove(method(channel="G123")["method"]) + await async_method(channel="G123") + elif method_name == "groups_create": + self.api_methods_to_call.remove(method(name="private-channel-name")["method"]) + await async_method(name="private-channel-name") + elif method_name == "groups_createChild": + self.api_methods_to_call.remove(method(channel="G123")["method"]) + await async_method(channel="G123") + elif method_name == "groups_history": + self.api_methods_to_call.remove(method(channel="G123")["method"]) + await async_method(channel="G123") + elif method_name == "groups_info": + self.api_methods_to_call.remove(method(channel="G123")["method"]) + await async_method(channel="G123") + elif method_name == "groups_invite": + self.api_methods_to_call.remove(method(channel="G123", user="U123")["method"]) + await async_method(channel="G123", user="U123") + elif method_name == "groups_kick": + self.api_methods_to_call.remove(method(channel="G123", user="U123")["method"]) + await async_method(channel="G123", user="U123") + elif method_name == "groups_leave": + self.api_methods_to_call.remove(method(channel="G123")["method"]) + await async_method(channel="G123") + elif method_name == "groups_mark": + self.api_methods_to_call.remove(method(channel="C123", ts="123.123")["method"]) + await async_method(channel="C123", ts="123.123") + elif method_name == "groups_open": + self.api_methods_to_call.remove(method(channel="G123")["method"]) + await async_method(channel="G123") + elif method_name == "groups_rename": + self.api_methods_to_call.remove(method(channel="G123", name="new-name")["method"]) + await async_method(channel="G123", name="x") + elif method_name == "groups_replies": + self.api_methods_to_call.remove(method(channel="G123", thread_ts="123.123")["method"]) + await async_method(channel="G123", thread_ts="x") + elif method_name == "groups_setPurpose": + self.api_methods_to_call.remove(method(channel="G123", purpose="The purpose")["method"]) + await async_method(channel="G123", purpose="x") + elif method_name == "groups_setTopic": + self.api_methods_to_call.remove(method(channel="G123", topic="The topic")["method"]) + await async_method(channel="G123", topic="x") + elif method_name == "groups_unarchive": + self.api_methods_to_call.remove(method(channel="G123")["method"]) + await async_method(channel="G123") + elif method_name == "im_close": + self.api_methods_to_call.remove(method(channel="D123")["method"]) + await async_method(channel="G123") + elif method_name == "im_history": + self.api_methods_to_call.remove(method(channel="D123")["method"]) + await async_method(channel="D123") + elif method_name == "im_mark": + self.api_methods_to_call.remove(method(channel="D123", ts="123.123")["method"]) + await async_method(channel="D123", ts="x") + elif method_name == "im_open": + self.api_methods_to_call.remove(method(user="U123")["method"]) + await async_method(user="U123") + elif method_name == "im_replies": + self.api_methods_to_call.remove(method(channel="D123", thread_ts="123.123")["method"]) + await async_method(channel="D123", thread_ts="x") + elif method_name == "mpim_close": + self.api_methods_to_call.remove(method(channel="D123")["method"]) + await async_method(channel="D123") + elif method_name == "mpim_history": + self.api_methods_to_call.remove(method(channel="D123")["method"]) + await async_method(channel="D123") + elif method_name == "mpim_mark": + self.api_methods_to_call.remove(method(channel="D123", ts="123.123")["method"]) + await async_method(channel="D123", ts="x") + elif method_name == "mpim_open": + self.api_methods_to_call.remove(method(users=["U123", "U234"])["method"]) + method(users="U123,U234") + await async_method(users=["U123", "U234"]) + elif method_name == "mpim_replies": + self.api_methods_to_call.remove(method(channel="D123", thread_ts="123.123")["method"]) + await async_method(channel="D123", thread_ts="123.123") + elif method_name == "admin_conversations_restrictAccess_addGroup": + self.api_methods_to_call.remove(method(channel_id="D123", group_id="G123")["method"]) + await async_method(channel_id="D123", group_id="G123") + elif method_name == "admin_conversations_restrictAccess_listGroups": + self.api_methods_to_call.remove(method(channel_id="D123", group_id="G123")["method"]) + await async_method(channel_id="D123", group_id="G123") + elif method_name == "admin_conversations_restrictAccess_removeGroup": + self.api_methods_to_call.remove(method(channel_id="D123", group_id="G123", team_id="T13")["method"]) + await async_method(channel_id="D123", group_id="G123", team_id="T123") + elif method_name == "admin_conversations_create": + self.api_methods_to_call.remove(method(is_private=False, name="Foo", team_id="T123")["method"]) + await async_method(is_private=False, name="Foo", team_id="T123") + elif method_name == "admin_conversations_delete": + self.api_methods_to_call.remove(method(channel_id="C123")["method"]) + await async_method(channel_id="C123") + elif method_name == "admin_conversations_invite": + self.api_methods_to_call.remove(method(channel_id="C123", user_ids=["U123", "U456"])["method"]) + await async_method(channel_id="C123", user_ids=["U123", "U456"]) + elif method_name == "admin_conversations_archive": + self.api_methods_to_call.remove(method(channel_id="C123")["method"]) + await async_method(channel_id="C123") + elif method_name == "admin_conversations_unarchive": + self.api_methods_to_call.remove(method(channel_id="C123")["method"]) + await async_method(channel_id="C123") + elif method_name == "admin_conversations_rename": + self.api_methods_to_call.remove(method(channel_id="C123", name="Foo")["method"]) + await async_method(channel_id="C123", name="Foo") + elif method_name == "admin_conversations_search": + self.api_methods_to_call.remove(method()["method"]) + await async_method() + elif method_name == "admin_conversations_convertToPrivate": + self.api_methods_to_call.remove(method(channel_id="C123")["method"]) + await async_method(channel_id="C123") + elif method_name == "admin_conversations_setConversationPrefs": + self.api_methods_to_call.remove( + method( + channel_id="C123", + prefs={"who_can_post": "type:admin,user:U1234,subteam:S1234"}, + )["method"] + ) + await async_method( + channel_id="C123", + prefs={"who_can_post": "type:admin,user:U1234,subteam:S1234"}, + ) + elif method_name == "admin_conversations_getConversationPrefs": + self.api_methods_to_call.remove(method(channel_id="C123")["method"]) + await async_method(channel_id="C123") + elif method_name == "admin_conversations_setTeams": + self.api_methods_to_call.remove(method(channel_id="C123")["method"]) + await async_method(channel_id="C123") + elif method_name == "admin_conversations_getTeams": + self.api_methods_to_call.remove(method(channel_id="C123")["method"]) + await async_method(channel_id="C123") + elif method_name == "admin_conversations_disconnectShared": + self.api_methods_to_call.remove(method(channel_id="C123")["method"]) + await async_method(channel_id="C123") + elif method_name == "admin_conversations_ekm_listOriginalConnectedChannelInfo": + self.api_methods_to_call.remove(method()["method"]) + await async_method() + elif method_name == "admin_conversations_getCustomRetention": + self.api_methods_to_call.remove(method(channel_id="C123")["method"]) + await async_method(channel_id="C123") + elif method_name == "admin_conversations_removeCustomRetention": + self.api_methods_to_call.remove(method(channel_id="C123")["method"]) + await async_method(channel_id="C123") + elif method_name == "admin_conversations_setCustomRetention": + self.api_methods_to_call.remove(method(channel_id="C123", duration_days=365)["method"]) + await async_method(channel_id="C123", duration_days=365) + elif method_name == WebClient.admin_conversations_bulkArchive.__name__: + self.api_methods_to_call.remove(method(channel_ids=["C123", "C234"])["method"]) + await async_method(channel_ids=["C123", "C234"]) + elif method_name == WebClient.admin_conversations_bulkDelete.__name__: + self.api_methods_to_call.remove(method(channel_ids=["C123", "C234"])["method"]) + await async_method(channel_ids=["C123", "C234"]) + elif method_name == WebClient.admin_conversations_bulkMove.__name__: + self.api_methods_to_call.remove(method(channel_ids=["C123", "C234"], target_team_id="T123")["method"]) + await async_method(channel_ids=["C123", "C234"], target_team_id="T123") + elif method_name == "admin_conversations_convertToPublic": + self.api_methods_to_call.remove(method(channel_id="C123")["method"]) + await async_method(channel_id="C123") + elif method_name == "admin_conversations_lookup": + self.api_methods_to_call.remove(method(team_ids=["T111", "T222"], last_message_activity_before=10)["method"]) + await async_method(team_ids=["T111", "T222"], last_message_activity_before=10) + elif method_name == "users_discoverableContacts_lookup": + self.api_methods_to_call.remove(method(email="foo@example.com")["method"]) + await async_method(email="foo@example.com") + else: + self.api_methods_to_call.remove(method(*{})["method"]) + await async_method(*{}) + else: + # Verify if the expected method is supported + self.assertTrue(callable(method), f"{method_name} is not supported yet") + + @async_test + async def test_coverage(self): + print(self.api_methods_to_call) + for api_method in self.all_api_methods: + if self.api_methods_to_call.count(api_method) == 0: + continue + method_name = api_method.replace(".", "_") + method = getattr(self.client, method_name, None) + async_method = getattr(self.async_client, method_name, None) + await self.run_method(method_name, method, async_method) + + self.assertEqual(self.api_methods_to_call, [], "All methods should be supported") + + @async_test + async def test_legacy_coverage(self): + for api_method in self.all_api_methods: + if self.api_methods_to_call.count(api_method) == 0: + continue + method_name = api_method.replace(".", "_") + method = getattr(self.legacy_client, method_name, None) + async_method = getattr(self.async_client, method_name, None) + await self.run_method(method_name, method, async_method) + + self.assertEqual(self.api_methods_to_call, [], "All methods should be supported") diff --git a/tests/slack_sdk_async/web/test_web_client_issue_829.py b/tests/slack_sdk_async/web/test_web_client_issue_829.py new file mode 100644 index 000000000..a7f438e18 --- /dev/null +++ b/tests/slack_sdk_async/web/test_web_client_issue_829.py @@ -0,0 +1,28 @@ +import unittest + +import slack_sdk.errors as err +from slack_sdk.web.async_client import AsyncWebClient +from tests.helpers import async_test +from tests.slack_sdk.web.mock_web_api_handler import MockHandler +from tests.mock_web_api_server import setup_mock_web_api_server_async, cleanup_mock_web_api_server_async + + +class TestWebClient_Issue_829(unittest.TestCase): + def setUp(self): + setup_mock_web_api_server_async(self, MockHandler) + + def tearDown(self): + cleanup_mock_web_api_server_async(self) + + @async_test + async def test_html_response_body_issue_829_async(self): + client = AsyncWebClient(base_url="http://localhost:8888") + try: + await client.users_list(token="xoxb-error_html_response") + self.fail("SlackApiError expected here") + except err.SlackApiError as e: + self.assertEqual( + "The request to the Slack API failed. (url: http://localhost:8888/users.list, status: 503)\n" + "The server responded with: {}", + str(e), + ) diff --git a/tests/slack_sdk_async/web/test_web_client_issue_921_custom_logger.py b/tests/slack_sdk_async/web/test_web_client_issue_921_custom_logger.py new file mode 100644 index 000000000..9239206cf --- /dev/null +++ b/tests/slack_sdk_async/web/test_web_client_issue_921_custom_logger.py @@ -0,0 +1,37 @@ +import unittest +from logging import Logger + +from slack_sdk.web.async_client import AsyncWebClient +from tests.helpers import async_test +from tests.slack_sdk.web.mock_web_api_handler import MockHandler +from tests.mock_web_api_server import setup_mock_web_api_server_async, cleanup_mock_web_api_server_async + + +class TestWebClient_Issue_921_CustomLogger(unittest.TestCase): + def setUp(self): + setup_mock_web_api_server_async(self, MockHandler) + + def tearDown(self): + cleanup_mock_web_api_server_async(self) + + @async_test + async def test_if_it_uses_custom_logger(self): + logger = CustomLogger("test-logger") + client = AsyncWebClient( + base_url="http://localhost:8888", + token="xoxb-api_test", + logger=logger, + ) + await client.chat_postMessage(channel="C111", text="hello") + self.assertTrue(logger.called) + + +class CustomLogger(Logger): + called: bool + + def __init__(self, name, level="DEBUG"): + Logger.__init__(self, name, level) + self.called = False + + def debug(self, msg, *args, **kwargs): + self.called = True diff --git a/tests/slack_sdk_async/web/test_web_client_msg_text_content_warnings.py b/tests/slack_sdk_async/web/test_web_client_msg_text_content_warnings.py new file mode 100644 index 000000000..085b103d9 --- /dev/null +++ b/tests/slack_sdk_async/web/test_web_client_msg_text_content_warnings.py @@ -0,0 +1,117 @@ +import unittest +import warnings + +from slack_sdk.web.async_client import AsyncWebClient +from tests.helpers import async_test +from tests.slack_sdk.web.mock_web_api_handler import MockHandler +from tests.mock_web_api_server import setup_mock_web_api_server_async, cleanup_mock_web_api_server_async + + +class TestWebClientMessageTextContentWarnings(unittest.TestCase): + def setUp(self): + setup_mock_web_api_server_async(self, MockHandler) + + def tearDown(self): + cleanup_mock_web_api_server_async(self) + + @async_test + async def test_missing_text_warning_chat_postMessage(self): + client = AsyncWebClient(base_url="http://localhost:8888", token="xoxb-api_test") + with self.assertWarnsRegex(UserWarning, "`text` argument is missing"): + resp = await client.chat_postMessage(channel="C111", blocks=[]) + self.assertIsNone(resp["error"]) + + @async_test + async def test_missing_text_warning_chat_postEphemeral(self): + client = AsyncWebClient(base_url="http://localhost:8888", token="xoxb-api_test") + with self.assertWarnsRegex(UserWarning, "`text` argument is missing"): + resp = await client.chat_postEphemeral(channel="C111", user="U111", blocks=[]) + self.assertIsNone(resp["error"]) + + @async_test + async def test_missing_text_warning_chat_scheduleMessage(self): + client = AsyncWebClient(base_url="http://localhost:8888", token="xoxb-api_test") + with self.assertWarnsRegex(UserWarning, "`text` argument is missing"): + resp = await client.chat_scheduleMessage(channel="C111", post_at="299876400", text="", blocks=[]) + self.assertIsNone(resp["error"]) + + @async_test + async def test_missing_text_warning_chat_update(self): + client = AsyncWebClient(base_url="http://localhost:8888", token="xoxb-api_test") + with self.assertWarnsRegex(UserWarning, "`text` argument is missing"): + resp = await client.chat_update(channel="C111", ts="111.222", blocks=[]) + self.assertIsNone(resp["error"]) + + @async_test + async def test_missing_fallback_warning_chat_postMessage(self): + client = AsyncWebClient(base_url="http://localhost:8888", token="xoxb-api_test") + with self.assertWarnsRegex(UserWarning, "`fallback` argument is missing"): + resp = await client.chat_postMessage(channel="C111", blocks=[], attachments=[{"text": "hi"}]) + self.assertIsNone(resp["error"]) + + @async_test + async def test_missing_fallback_warning_chat_postEphemeral(self): + client = AsyncWebClient(base_url="http://localhost:8888", token="xoxb-api_test") + with self.assertWarnsRegex(UserWarning, "`fallback` argument is missing"): + resp = await client.chat_postEphemeral(channel="C111", user="U111", blocks=[], attachments=[{"text": "hi"}]) + self.assertIsNone(resp["error"]) + + @async_test + async def test_missing_fallback_warning_chat_scheduleMessage(self): + client = AsyncWebClient(base_url="http://localhost:8888", token="xoxb-api_test") + with self.assertWarnsRegex(UserWarning, "`text` argument is missing"): + resp = await client.chat_scheduleMessage( + channel="C111", + post_at="299876400", + text="", + blocks=[], + attachments=[{"text": "hi"}], + ) + self.assertIsNone(resp["error"]) + + @async_test + async def test_missing_fallback_warning_chat_update(self): + client = AsyncWebClient(base_url="http://localhost:8888", token="xoxb-api_test") + with self.assertWarnsRegex(UserWarning, "`text` argument is missing"): + resp = await client.chat_update(channel="C111", ts="111.222", blocks=[], attachments=[{"text": "hi"}]) + self.assertIsNone(resp["error"]) + + @async_test + async def test_no_warning_when_markdown_text_is_provided_chat_postMessage(self): + client = AsyncWebClient(base_url="http://localhost:8888", token="xoxb-api_test") + with warnings.catch_warnings(record=True) as warning_list: + warnings.simplefilter("always") + resp = await client.chat_postMessage(channel="C111", markdown_text="# hello") + + self.assertEqual(warning_list, []) + self.assertIsNone(resp["error"]) + + @async_test + async def test_no_warning_when_markdown_text_is_provided_chat_postEphemeral(self): + client = AsyncWebClient(base_url="http://localhost:8888", token="xoxb-api_test") + with warnings.catch_warnings(record=True) as warning_list: + warnings.simplefilter("always") + resp = await client.chat_postEphemeral(channel="C111", user="U111", markdown_text="# hello") + + self.assertEqual(warning_list, []) + self.assertIsNone(resp["error"]) + + @async_test + async def test_no_warning_when_markdown_text_is_provided_chat_scheduleMessage(self): + client = AsyncWebClient(base_url="http://localhost:8888", token="xoxb-api_test") + with warnings.catch_warnings(record=True) as warning_list: + warnings.simplefilter("always") + resp = await client.chat_scheduleMessage(channel="C111", post_at="299876400", markdown_text="# hello") + + self.assertEqual(warning_list, []) + self.assertIsNone(resp["error"]) + + @async_test + async def test_no_warning_when_markdown_text_is_provided_chat_update(self): + client = AsyncWebClient(base_url="http://localhost:8888", token="xoxb-api_test") + with warnings.catch_warnings(record=True) as warning_list: + warnings.simplefilter("always") + resp = await client.chat_update(channel="C111", ts="111.222", markdown_text="# hello") + + self.assertEqual(warning_list, []) + self.assertIsNone(resp["error"]) diff --git a/tests/slack_sdk_async/web/test_web_client_url_format.py b/tests/slack_sdk_async/web/test_web_client_url_format.py new file mode 100644 index 000000000..d80786a1a --- /dev/null +++ b/tests/slack_sdk_async/web/test_web_client_url_format.py @@ -0,0 +1,67 @@ +import unittest + +from slack_sdk.web.async_client import AsyncWebClient +from tests.slack_sdk_async.helpers import async_test +from tests.slack_sdk.web.mock_web_api_handler import MockHandler +from tests.mock_web_api_server import ( + setup_mock_web_api_server_async, + cleanup_mock_web_api_server_async, + assert_received_request_count_async, +) + + +class TestAsyncWebClientUrlFormat(unittest.TestCase): + def setUp(self): + setup_mock_web_api_server_async(self, MockHandler) + self.client = AsyncWebClient(token="xoxb-api_test", base_url="http://localhost:8888") + self.client_base_url_slash = AsyncWebClient(token="xoxb-api_test", base_url="http://localhost:8888/") + self.client_api = AsyncWebClient(token="xoxb-api_test", base_url="http://localhost:8888/api") + self.client_api_slash = AsyncWebClient(token="xoxb-api_test", base_url="http://localhost:8888/api/") + + def tearDown(self): + cleanup_mock_web_api_server_async(self) + + @async_test + async def test_base_url_without_slash_api_method_without_slash(self): + await self.client.api_call("chat.postMessage") + await assert_received_request_count_async(self, "/chat.postMessage", 1) + + @async_test + async def test_base_url_without_slash_api_method_with_slash(self): + await self.client.api_call("/chat.postMessage") + await assert_received_request_count_async(self, "/chat.postMessage", 1) + + @async_test + async def test_base_url_with_slash_api_method_without_slash(self): + await self.client_base_url_slash.api_call("chat.postMessage") + await assert_received_request_count_async(self, "/chat.postMessage", 1) + + @async_test + async def test_base_url_with_slash_api_method_with_slash(self): + await self.client_base_url_slash.api_call("/chat.postMessage") + await assert_received_request_count_async(self, "/chat.postMessage", 1) + + @async_test + async def test_base_url_without_slash_api_method_with_slash_and_trailing_slash(self): + await self.client.api_call("/chat.postMessage/") + await assert_received_request_count_async(self, "/chat.postMessage/", 1) + + @async_test + async def test_base_url_with_api(self): + await self.client_api.api_call("chat.postMessage") + await assert_received_request_count_async(self, "/api/chat.postMessage", 1) + + @async_test + async def test_base_url_with_api_method_without_slash_method_with_slash(self): + await self.client_api.api_call("/chat.postMessage") + await assert_received_request_count_async(self, "/api/chat.postMessage", 1) + + @async_test + async def test_base_url_with_api_slash(self): + await self.client_api_slash.api_call("chat.postMessage") + await assert_received_request_count_async(self, "/api/chat.postMessage", 1) + + @async_test + async def test_base_url_with_api_slash_and_method_with_slash(self): + await self.client_api_slash.api_call("/chat.postMessage") + await assert_received_request_count_async(self, "/api/chat.postMessage", 1) diff --git a/tests/slack_sdk_async/webhook/__init__.py b/tests/slack_sdk_async/webhook/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/slack_sdk_async/webhook/test_async_webhook.py b/tests/slack_sdk_async/webhook/test_async_webhook.py new file mode 100644 index 000000000..c84a0fc93 --- /dev/null +++ b/tests/slack_sdk_async/webhook/test_async_webhook.py @@ -0,0 +1,217 @@ +import unittest +from logging import Logger + +from slack_sdk.models.attachments import Attachment, AttachmentField +from slack_sdk.models.blocks import SectionBlock, ImageBlock +from slack_sdk.webhook.async_client import AsyncWebhookClient, WebhookResponse +from tests.slack_sdk_async.helpers import async_test +from tests.slack_sdk.webhook.mock_web_api_server import MockHandler +from tests.mock_web_api_server import setup_mock_web_api_server_async, cleanup_mock_web_api_server_async + + +class TestAsyncWebhook(unittest.TestCase): + def setUp(self): + setup_mock_web_api_server_async(self, MockHandler) + + def tearDown(self): + cleanup_mock_web_api_server_async(self) + + @async_test + async def test_send(self): + client = AsyncWebhookClient("http://localhost:8888") + + resp: WebhookResponse = await client.send(text="hello!") + self.assertEqual(200, resp.status_code) + self.assertEqual("ok", resp.body) + + resp = await client.send(text="hello!", response_type="in_channel") + self.assertEqual("ok", resp.body) + + @async_test + async def test_send_blocks(self): + client = AsyncWebhookClient("http://localhost:8888") + + resp = await client.send( + text="hello!", + response_type="ephemeral", + blocks=[ + SectionBlock(text="Some text"), + ImageBlock(image_url="image.jpg", alt_text="an image"), + ], + ) + self.assertEqual("ok", resp.body) + + resp = await client.send( + text="hello!", + response_type="ephemeral", + blocks=[ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "This is a mrkdwn section block :ghost: *this is bold*, and ~this is crossed out~, and ", + }, + }, + {"type": "divider"}, + { + "type": "section", + "text": {"type": "mrkdwn", "text": "Pick a date for the deadline."}, + "accessory": { + "type": "datepicker", + "initial_date": "1990-04-28", + "placeholder": { + "type": "plain_text", + "text": "Select a date", + }, + }, + }, + ], + ) + self.assertEqual("ok", resp.body) + + resp = await client.send( + text="hello!", + response_type="ephemeral", + blocks=[ + SectionBlock(text="Some text"), + ImageBlock(image_url="image.jpg", alt_text="an image"), + ], + ) + self.assertEqual("ok", resp.body) + + @async_test + async def test_send_attachments(self): + client = AsyncWebhookClient("http://localhost:8888") + + resp = await client.send( + text="hello!", + response_type="ephemeral", + attachments=[ + { + "color": "#f2c744", + "blocks": [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "This is a mrkdwn section block :ghost: *this is bold*, and ~this is crossed out~, and ", + }, + }, + {"type": "divider"}, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "Pick a date for the deadline.", + }, + "accessory": { + "type": "datepicker", + "initial_date": "1990-04-28", + "placeholder": { + "type": "plain_text", + "text": "Select a date", + }, + }, + }, + ], + } + ], + ) + self.assertEqual("ok", resp.body) + + resp = await client.send( + text="hello!", + response_type="ephemeral", + attachments=[ + Attachment( + text="attachment text", + title="Attachment", + fallback="fallback_text", + pretext="some_pretext", + title_link="link in title", + fields=[AttachmentField(title=f"field_{i}_title", value=f"field_{i}_value") for i in range(5)], + color="#FFFF00", + author_name="John Doe", + author_link="http://johndoeisthebest.com", + author_icon="http://johndoeisthebest.com/avatar.jpg", + thumb_url="thumbnail URL", + footer="and a footer", + footer_icon="link to footer icon", + ts=123456789, + markdown_in=["fields"], + ) + ], + ) + self.assertEqual("ok", resp.body) + + @async_test + async def test_send_dict(self): + client = AsyncWebhookClient("http://localhost:8888") + resp: WebhookResponse = await client.send_dict({"text": "hello!"}) + self.assertEqual(200, resp.status_code) + self.assertEqual("ok", resp.body) + + @async_test + async def test_timeout_issue_712(self): + client = AsyncWebhookClient(url="http://localhost:8888/timeout", timeout=1) + with self.assertRaises(Exception): + await client.send_dict({"text": "hello!"}) + + @async_test + async def test_proxy_issue_714(self): + client = AsyncWebhookClient(url="http://localhost:8888", proxy="http://invalid-host:9999") + with self.assertRaises(Exception): + await client.send_dict({"text": "hello!"}) + + @async_test + async def test_user_agent_customization_issue_769(self): + client = AsyncWebhookClient( + url="http://localhost:8888/user-agent-this_is-test", + user_agent_prefix="this_is", + user_agent_suffix="test", + ) + resp = await client.send_dict({"text": "hi!"}) + self.assertEqual(resp.body, "ok") + + @async_test + async def test_issue_919_response_url_flag_options(self): + client = AsyncWebhookClient("http://localhost:8888") + resp = await client.send( + text="hello!", + response_type="ephemeral", + replace_original=True, + blocks=[ + SectionBlock(text="Some text"), + ImageBlock(image_url="image.jpg", alt_text="an image"), + ], + ) + self.assertEqual("ok", resp.body) + + resp = await client.send( + text="hello!", + response_type="ephemeral", + delete_original=True, + blocks=[ + SectionBlock(text="Some text"), + ImageBlock(image_url="image.jpg", alt_text="an image"), + ], + ) + self.assertEqual("ok", resp.body) + + @async_test + async def test_if_it_uses_custom_logger_issue_921(self): + logger = CustomLogger("test-logger") + client = AsyncWebhookClient(url="http://localhost:8888", logger=logger) + await client.send_dict({"text": "hi!"}) + self.assertTrue(logger.called) + + +class CustomLogger(Logger): + called: bool + + def __init__(self, name, level="DEBUG"): + Logger.__init__(self, name, level) + self.called = False + + def debug(self, msg, *args, **kwargs): + self.called = True diff --git a/tests/slack_sdk_async/webhook/test_async_webhook_http_retry.py b/tests/slack_sdk_async/webhook/test_async_webhook_http_retry.py new file mode 100644 index 000000000..9f85b61be --- /dev/null +++ b/tests/slack_sdk_async/webhook/test_async_webhook_http_retry.py @@ -0,0 +1,41 @@ +import unittest + +from slack_sdk.http_retry.builtin_async_handlers import AsyncRateLimitErrorRetryHandler +from slack_sdk.webhook.async_client import AsyncWebhookClient +from tests.slack_sdk_async.helpers import async_test +from tests.slack_sdk.webhook.mock_web_api_server import MockHandler +from tests.mock_web_api_server import setup_mock_web_api_server_async, cleanup_mock_web_api_server_async +from ..my_retry_handler import MyRetryHandler + + +class TestAsyncWebhook_HttpRetries(unittest.TestCase): + def setUp(self): + setup_mock_web_api_server_async(self, MockHandler) + + def tearDown(self): + cleanup_mock_web_api_server_async(self) + + @async_test + async def test_send(self): + retry_handler = MyRetryHandler(max_retry_count=2) + client = AsyncWebhookClient( + "http://localhost:8888/remote_disconnected", + retry_handlers=[retry_handler], + ) + try: + await client.send(text="hello!") + self.fail("An exception is expected") + except Exception as _: + pass + + self.assertEqual(2, retry_handler.call_count) + + @async_test + async def test_ratelimited(self): + client = AsyncWebhookClient( + "http://localhost:8888/ratelimited", + retry_handlers=[AsyncRateLimitErrorRetryHandler()], + ) + response = await client.send(text="hello!") + # Just running retries; no assertions for call count so far + self.assertEqual(429, response.status_code) diff --git a/tests/slack_sdk_fixture/channel.created.json b/tests/slack_sdk_fixture/channel.created.json new file mode 100644 index 000000000..9ccbffaeb --- /dev/null +++ b/tests/slack_sdk_fixture/channel.created.json @@ -0,0 +1,9 @@ +{ + "type": "channel_created", + "channel": { + "id": "C024BE91L", + "name": "fun", + "created": 1360782804, + "creator": "U024BE7LH" + } +} diff --git a/tests/slack_sdk_fixture/im.created.json b/tests/slack_sdk_fixture/im.created.json new file mode 100644 index 000000000..8f51519f1 --- /dev/null +++ b/tests/slack_sdk_fixture/im.created.json @@ -0,0 +1,9 @@ +{ + "type": "im_created", + "user": "U024BE7LH", + "channel": { + "id": "D024BE91L", + "user": "U123BL234", + "created": 1360782804 + } +} diff --git a/tests/slack_sdk_fixture/rtm.start.json b/tests/slack_sdk_fixture/rtm.start.json new file mode 100644 index 000000000..a0696eda9 --- /dev/null +++ b/tests/slack_sdk_fixture/rtm.start.json @@ -0,0 +1,321 @@ +{ + "ok": true, + "self": { + "id": "U10CX1234", + "name": "fakeuser", + "prefs": { + "highlight_words": "", + "user_colors": "", + "color_names_in_list": true, + "growls_enabled": true, + "tz": "America\/Los_Angeles", + "push_dm_alert": true, + "push_mention_alert": true, + "push_everything": true, + "push_idle_wait": 2, + "push_sound": "b2.mp3", + "push_loud_channels": "", + "push_mention_channels": "", + "push_loud_channels_set": "", + "email_alerts": "instant", + "email_alerts_sleep_until": 0, + "email_misc": true, + "email_weekly": true, + "welcome_message_hidden": false, + "all_channels_loud": true, + "loud_channels": "", + "never_channels": "", + "loud_channels_set": "", + "show_member_presence": true, + "search_sort": "timestamp", + "expand_inline_imgs": true, + "expand_internal_inline_imgs": true, + "expand_snippets": false, + "posts_formatting_guide": true, + "seen_welcome_2": true, + "seen_ssb_prompt": false, + "search_only_my_channels": false, + "emoji_mode": "default", + "has_invited": false, + "has_uploaded": false, + "has_created_channel": false, + "search_exclude_channels": "", + "messages_theme": "default", + "webapp_spellcheck": true, + "no_joined_overlays": false, + "no_created_overlays": false, + "dropbox_enabled": false, + "seen_user_menu_tip_card": true, + "seen_team_menu_tip_card": true, + "seen_channel_menu_tip_card": true, + "seen_message_input_tip_card": true, + "seen_channels_tip_card": true, + "seen_domain_invite_reminder": false, + "seen_member_invite_reminder": false, + "seen_flexpane_tip_card": true, + "seen_search_input_tip_card": true, + "mute_sounds": false, + "arrow_history": false, + "tab_ui_return_selects": true, + "obey_inline_img_limit": true, + "new_msg_snd": "knock_brush.mp3", + "collapsible": false, + "collapsible_by_click": true, + "require_at": false, + "mac_ssb_bounce": "", + "mac_ssb_bullet": true, + "expand_non_media_attachments": true, + "show_typing": true, + "pagekeys_handled": true, + "last_snippet_type": "", + "display_real_names_override": 0, + "time24": false, + "enter_is_special_in_tbt": false, + "graphic_emoticons": false, + "convert_emoticons": true, + "autoplay_chat_sounds": true, + "ss_emojis": true, + "sidebar_behavior": "", + "mark_msgs_read_immediately": true, + "start_scroll_at_oldest": true, + "snippet_editor_wrap_long_lines": false, + "ls_disabled": false, + "sidebar_theme": "default", + "sidebar_theme_custom_values": "", + "f_key_search": false, + "k_key_omnibox": true, + "speak_growls": false, + "mac_speak_voice": "com.apple.speech.synthesis.voice.Alex", + "mac_speak_speed": 250, + "comma_key_prefs": false, + "at_channel_suppressed_channels": "", + "push_at_channel_suppressed_channels": "", + "prompted_for_email_disabling": false, + "full_text_extracts": false, + "no_text_in_notifications": false, + "muted_channels": "", + "no_macssb1_banner": true, + "no_winssb1_banner": false, + "privacy_policy_seen": true, + "search_exclude_bots": false, + "fuzzy_matching": false, + "load_lato_2": false, + "fuller_timestamps": false, + "last_seen_at_channel_warning": 0, + "enable_flexpane_rework": false, + "flex_resize_window": false, + "msg_preview": false, + "msg_preview_displaces": true, + "msg_preview_persistent": true, + "emoji_autocomplete_big": false, + "winssb_run_from_tray": true + }, + "created": 1421456475, + "manual_presence": "active" + }, + "team": { + "id": "T03CX4S34", + "name": "TESTteam, INC", + "email_domain": "", + "domain": "testteaminc", + "msg_edit_window_mins": -1, + "prefs": { + "default_channels": [ + "C01CX1234", + "C05BX1234" + ], + "msg_edit_window_mins": -1, + "allow_message_deletion": true, + "hide_referers": true, + "display_real_names": false, + "who_can_at_everyone": "regular", + "who_can_at_channel": "ra", + "warn_before_at_channel": "always", + "who_can_create_channels": "regular", + "who_can_archive_channels": "regular", + "who_can_create_groups": "ra", + "who_can_post_general": "ra", + "who_can_kick_channels": "admin", + "who_can_kick_groups": "regular", + "retention_type": 0, + "retention_duration": 0, + "group_retention_type": 0, + "group_retention_duration": 0, + "dm_retention_type": 0, + "dm_retention_duration": 0, + "require_at_for_mention": 0, + "compliance_export_start": 0 + }, + "icon": { + "image_34": "https:\/\/slack.global.ssl.fastly.net\/b3b7\/img\/avatars-teams\/ava_0025-34.png", + "image_44": "https:\/\/slack.global.ssl.fastly.net\/b3b7\/img\/avatars-teams\/ava_0025-44.png", + "image_68": "https:\/\/slack.global.ssl.fastly.net\/b3b7\/img\/avatars-teams\/ava_0025-68.png", + "image_88": "https:\/\/slack.global.ssl.fastly.net\/b3b7\/img\/avatars-teams\/ava_0025-88.png", + "image_102": "https:\/\/slack.global.ssl.fastly.net\/b3b7\/img\/avatars-teams\/ava_0025-102.png", + "image_132": "https:\/\/slack.global.ssl.fastly.net\/b3b7\/img\/avatars-teams\/ava_0025-132.png", + "image_default": true + }, + "over_storage_limit": false + }, + "latest_event_ts": "1426103085.000000", + "channels": [ + { + "id": "C01CX1234", + "name": "general", + "is_channel": true, + "created": 1421456475, + "creator": "U03CX4S38", + "is_archived": false, + "is_general": true, + "is_member": true, + "last_read": "0000000000.000000", + "latest": { + "type": "message", + "user": "U03CX4S38", + "text": "a", + "ts": "1425499421.000004" + }, + "unread_count": 0, + "unread_count_display": 0, + "members": [ + "U03CX4S38" + ], + "topic": { + "value": "", + "creator": "", + "last_set": 0 + }, + "purpose": { + "value": "This channel is for team-wide communication and announcements. All team members are in this channel.", + "creator": "", + "last_set": 0 + } + }, + { + "id": "C05BX1234", + "name": "random", + "is_channel": true, + "created": 1421456475, + "creator": "U03CX4S38", + "is_archived": false, + "is_general": false, + "is_member": true, + "last_read": "0000000000.000000", + "latest": null, + "unread_count": 0, + "unread_count_display": 0, + "members": [ + "U03CX4S38" + ], + "topic": { + "value": "", + "creator": "", + "last_set": 0 + }, + "purpose": { + "value": "A place for non-work banter, links, articles of interest, humor or anything else which you'd like concentrated in some place other than work-related channels.", + "creator": "", + "last_set": 0 + } + } + ], + "groups": [], + "ims": [ + { + "id": "D03CX4S3E", + "is_im": true, + "user": "USLACKBOT", + "created": 1421456475, + "last_read": "1425318850.000003", + "latest": { + "type": "message", + "user": "USLACKBOT", + "text": "To start, what is your first name?", + "ts": "1425318850.000003" + }, + "unread_count": 0, + "unread_count_display": 0, + "is_open": true + } + ], + "users": [ + { + "id": "U10CX1234", + "name": "fakeuser", + "deleted": false, + "status": null, + "color": "9f69e7", + "profile": { + "email": "fakeuser@example.com", + "image_24": "https:\/\/secure.gravatar.com\/avatar\/4f1bd7fd71e645fa19620504b4c0e3ba.jpg?s=24&d=https%3A%2F%2Fslack.global.ssl.fastly.net%2F3654%2Fimg%2Favatars%2Fava_0002-24.png", + "image_32": "https:\/\/secure.gravatar.com\/avatar\/4f1bd7fd71e645fa19620504b4c0e3ba.jpg?s=32&d=https%3A%2F%2Fslack.global.ssl.fastly.net%2F3654%2Fimg%2Favatars%2Fava_0002-32.png", + "image_48": "https:\/\/secure.gravatar.com\/avatar\/4f1bd7fd71e645fa19620504b4c0e3ba.jpg?s=48&d=https%3A%2F%2Fslack.global.ssl.fastly.net%2F272a%2Fimg%2Favatars%2Fava_0002-48.png", + "image_72": "https:\/\/secure.gravatar.com\/avatar\/4f1bd7fd71e645fa19620504b4c0e3ba.jpg?s=72&d=https%3A%2F%2Fslack.global.ssl.fastly.net%2F3654%2Fimg%2Favatars%2Fava_0002-72.png", + "image_192": "https:\/\/secure.gravatar.com\/avatar\/4f1bd7fd71e645fa19620504b4c0e3ba.jpg?s=192&d=https%3A%2F%2Fslack.global.ssl.fastly.net%2F3654%2Fimg%2Favatars%2Fava_0002.png" + }, + "is_admin": true, + "is_owner": true, + "is_primary_owner": true, + "is_restricted": false, + "is_ultra_restricted": false, + "is_bot": false, + "has_files": false, + "presence": "away" + }, + { + "id": "U10CX1235", + "name": "userwithoutemail", + "deleted": false, + "status": null, + "color": "9f69e7", + "profile": { + "image_24": "https:\/\/secure.gravatar.com\/avatar\/4f1bd7fd71e645fa19620504b4c0e3ba.jpg?s=24&d=https%3A%2F%2Fslack.global.ssl.fastly.net%2F3654%2Fimg%2Favatars%2Fava_0002-24.png", + "image_32": "https:\/\/secure.gravatar.com\/avatar\/4f1bd7fd71e645fa19620504b4c0e3ba.jpg?s=32&d=https%3A%2F%2Fslack.global.ssl.fastly.net%2F3654%2Fimg%2Favatars%2Fava_0002-32.png", + "image_48": "https:\/\/secure.gravatar.com\/avatar\/4f1bd7fd71e645fa19620504b4c0e3ba.jpg?s=48&d=https%3A%2F%2Fslack.global.ssl.fastly.net%2F272a%2Fimg%2Favatars%2Fava_0002-48.png", + "image_72": "https:\/\/secure.gravatar.com\/avatar\/4f1bd7fd71e645fa19620504b4c0e3ba.jpg?s=72&d=https%3A%2F%2Fslack.global.ssl.fastly.net%2F3654%2Fimg%2Favatars%2Fava_0002-72.png", + "image_192": "https:\/\/secure.gravatar.com\/avatar\/4f1bd7fd71e645fa19620504b4c0e3ba.jpg?s=192&d=https%3A%2F%2Fslack.global.ssl.fastly.net%2F3654%2Fimg%2Favatars%2Fava_0002.png" + }, + "is_admin": true, + "is_owner": true, + "is_primary_owner": true, + "is_restricted": false, + "is_ultra_restricted": false, + "is_bot": false, + "has_files": false, + "presence": "away" + }, + { + "id": "USLACKBOT", + "name": "slackbot", + "deleted": false, + "status": null, + "color": "757575", + "real_name": "Slack Bot", + "tz": null, + "tz_label": "Pacific Daylight Time", + "tz_offset": -25200, + "profile": { + "first_name": "Slack", + "last_name": "Bot", + "image_24": "https:\/\/slack-assets2.s3-us-west-2.amazonaws.com\/10068\/img\/slackbot_24.png", + "image_32": "https:\/\/slack-assets2.s3-us-west-2.amazonaws.com\/10068\/img\/slackbot_32.png", + "image_48": "https:\/\/slack-assets2.s3-us-west-2.amazonaws.com\/10068\/img\/slackbot_48.png", + "image_72": "https:\/\/slack-assets2.s3-us-west-2.amazonaws.com\/10068\/img\/slackbot_72.png", + "image_192": "https:\/\/slack-assets2.s3-us-west-2.amazonaws.com\/10068\/img\/slackbot_192.png", + "real_name": "Slack Bot", + "real_name_normalized": "Slack Bot", + "email": null + }, + "is_admin": false, + "is_owner": false, + "is_primary_owner": false, + "is_restricted": false, + "is_ultra_restricted": false, + "is_bot": false, + "presence": "active" + } + ], + "bots": [], + "cache_version": "v5-dog", + "url": "wss:\/\/cerberus-xxxx.lb.slack-msgs.com\/websocket\/ifkp3MKfNXd6ftbrEGllwcHn" +} diff --git a/tests/slack_sdk_fixture/slack_logo.png b/tests/slack_sdk_fixture/slack_logo.png new file mode 100644 index 000000000..232a00cf1 Binary files /dev/null and b/tests/slack_sdk_fixture/slack_logo.png differ diff --git a/tests/slack_sdk_fixture/slack_logo_new.png b/tests/slack_sdk_fixture/slack_logo_new.png new file mode 100644 index 000000000..2b95382ce Binary files /dev/null and b/tests/slack_sdk_fixture/slack_logo_new.png differ diff --git a/tests/slack_sdk_fixture/view_home_001.json b/tests/slack_sdk_fixture/view_home_001.json new file mode 100644 index 000000000..21e0244f0 --- /dev/null +++ b/tests/slack_sdk_fixture/view_home_001.json @@ -0,0 +1,433 @@ +{ + "type": "home", + "blocks": [ + { + "type": "section", + "text": { + "type": "plain_text", + "text": "This is a plain text section block.", + "emoji": true + } + }, + { + "type": "image", + "title": { + "type": "plain_text", + "text": "image1", + "emoji": true + }, + "image_url": "https://api.slack.com/img/blocks/bkb_template_images/beagle.png", + "alt_text": "image1" + }, + { + "type": "context", + "elements": [ + { + "type": "mrkdwn", + "text": "Last updated: Jan 1, 2019" + } + ] + }, + { + "type": "divider" + }, + { + "type": "actions", + "elements": [ + { + "type": "checkboxes", + "options": [ + { + "text": { + "type": "plain_text", + "text": "*this is plain_text text*", + "emoji": true + }, + "description": { + "type": "plain_text", + "text": "*this is plain_text text*", + "emoji": true + }, + "value": "value-0" + }, + { + "text": { + "type": "plain_text", + "text": "*this is plain_text text*", + "emoji": true + }, + "description": { + "type": "plain_text", + "text": "*this is plain_text text*", + "emoji": true + }, + "value": "value-1" + }, + { + "text": { + "type": "plain_text", + "text": "*this is plain_text text*", + "emoji": true + }, + "description": { + "type": "plain_text", + "text": "*this is plain_text text*", + "emoji": true + }, + "value": "value-2" + } + ] + }, + { + "type": "checkboxes", + "options": [ + { + "text": { + "type": "mrkdwn", + "text": "*this is mrkdwn text*" + }, + "description": { + "type": "mrkdwn", + "text": "*this is mrkdwn text*" + }, + "value": "value-0" + }, + { + "text": { + "type": "mrkdwn", + "text": "*this is mrkdwn text*" + }, + "description": { + "type": "mrkdwn", + "text": "*this is mrkdwn text*" + }, + "value": "value-1" + }, + { + "text": { + "type": "mrkdwn", + "text": "*this is mrkdwn text*" + }, + "description": { + "type": "mrkdwn", + "text": "*this is mrkdwn text*" + }, + "value": "value-2" + } + ] + } + ] + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "You can add an image next to text in this block." + }, + "accessory": { + "type": "image", + "image_url": "https://api.slack.com/img/blocks/bkb_template_images/plants.png", + "alt_text": "plants" + } + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "You can add a button alongside text in your message. " + }, + "accessory": { + "type": "button", + "text": { + "type": "plain_text", + "text": "Button", + "emoji": true + }, + "value": "click_me_123" + } + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "Pick an item from the dropdown list" + }, + "accessory": { + "type": "static_select", + "placeholder": { + "type": "plain_text", + "text": "Select an item", + "emoji": true + }, + "options": [ + { + "text": { + "type": "plain_text", + "text": "Choice 1", + "emoji": true + }, + "value": "value-0" + }, + { + "text": { + "type": "plain_text", + "text": "Choice 2", + "emoji": true + }, + "value": "value-1" + }, + { + "text": { + "type": "plain_text", + "text": "Choice 3", + "emoji": true + }, + "value": "value-2" + } + ] + } + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "Pick one or more items from the list" + }, + "accessory": { + "type": "multi_static_select", + "placeholder": { + "type": "plain_text", + "text": "Select items", + "emoji": true + }, + "options": [ + { + "text": { + "type": "plain_text", + "text": "Choice 1", + "emoji": true + }, + "value": "value-0" + }, + { + "text": { + "type": "plain_text", + "text": "Choice 2", + "emoji": true + }, + "value": "value-1" + }, + { + "text": { + "type": "plain_text", + "text": "Choice 3", + "emoji": true + }, + "value": "value-2" + } + ] + } + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "This block has an overflow menu." + }, + "accessory": { + "type": "overflow", + "options": [ + { + "text": { + "type": "plain_text", + "text": "Option 1", + "emoji": true + }, + "value": "value-0" + }, + { + "text": { + "type": "plain_text", + "text": "Option 2", + "emoji": true + }, + "value": "value-1" + }, + { + "text": { + "type": "plain_text", + "text": "Option 3", + "emoji": true + }, + "value": "value-2" + }, + { + "text": { + "type": "plain_text", + "text": "Option 4", + "emoji": true + }, + "value": "value-3" + } + ] + } + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "Pick a date for the deadline." + }, + "accessory": { + "type": "datepicker", + "initial_date": "1990-04-28", + "placeholder": { + "type": "plain_text", + "text": "Select a date", + "emoji": true + } + } + }, + { + "type": "section", + "fields": [ + { + "type": "plain_text", + "text": "*this is plain_text text*", + "emoji": true + }, + { + "type": "plain_text", + "text": "*this is plain_text text*", + "emoji": true + }, + { + "type": "plain_text", + "text": "*this is plain_text text*", + "emoji": true + }, + { + "type": "plain_text", + "text": "*this is plain_text text*", + "emoji": true + }, + { + "type": "plain_text", + "text": "*this is plain_text text*", + "emoji": true + } + ] + }, + { + "type": "section", + "fields": [ + { + "type": "mrkdwn", + "text": "This is a section block with _another_ fields radio button accessory" + }, + { + "type": "mrkdwn", + "text": "This is a section block with *fields* radio button accessory" + }, + { + "type": "mrkdwn", + "text": "This is a section block." + } + ], + "accessory": { + "type": "radio_buttons", + "initial_option": { + "text": { + "type": "plain_text", + "text": "Option 3" + }, + "value": "option 3", + "description": { + "type": "plain_text", + "text": "Description for option 3" + } + }, + "options": [ + { + "text": { + "type": "plain_text", + "text": "Option 1" + }, + "value": "option 1", + "description": { + "type": "plain_text", + "text": "Description for option 1" + } + }, + { + "text": { + "type": "plain_text", + "text": "Option 2" + }, + "value": "option 2", + "description": { + "type": "plain_text", + "text": "Description for option 2" + } + }, + { + "text": { + "type": "plain_text", + "text": "Option 3" + }, + "value": "option 3", + "description": { + "type": "plain_text", + "text": "Description for option 3" + } + } + ] + } + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "This is a section block with checkboxes." + }, + "accessory": { + "type": "checkboxes", + "options": [ + { + "text": { + "type": "mrkdwn", + "text": "*this is mrkdwn text*" + }, + "description": { + "type": "mrkdwn", + "text": "*this is mrkdwn text*" + }, + "value": "value-0" + }, + { + "text": { + "type": "mrkdwn", + "text": "*this is mrkdwn text*" + }, + "description": { + "type": "mrkdwn", + "text": "*this is mrkdwn text*" + }, + "value": "value-1" + }, + { + "text": { + "type": "mrkdwn", + "text": "*this is mrkdwn text*" + }, + "description": { + "type": "mrkdwn", + "text": "*this is mrkdwn text*" + }, + "value": "value-2" + } + ] + } + } + ] +} \ No newline at end of file diff --git a/tests/slack_sdk_fixture/view_home_002.json b/tests/slack_sdk_fixture/view_home_002.json new file mode 100644 index 000000000..64f4a2592 --- /dev/null +++ b/tests/slack_sdk_fixture/view_home_002.json @@ -0,0 +1,117 @@ +{ + "type": "home", + "blocks": [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "*Here's what you can do with Project Tracker:*" + } + }, + { + "type": "actions", + "elements": [ + { + "type": "button", + "text": { + "type": "plain_text", + "text": "Create New Task", + "emoji": true + }, + "style": "primary", + "value": "create_task" + }, + { + "type": "button", + "text": { + "type": "plain_text", + "text": "Create New Project", + "emoji": true + }, + "value": "create_project" + }, + { + "type": "button", + "text": { + "type": "plain_text", + "text": "Help", + "emoji": true + }, + "value": "help" + } + ] + }, + { + "type": "context", + "elements": [ + { + "type": "image", + "image_url": "https://api.slack.com/img/blocks/bkb_template_images/placeholder.png", + "alt_text": "placeholder" + } + ] + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "*Your Configurations*" + } + }, + { + "type": "divider" + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "*#public-relations*\n posts new tasks, comments, and project updates to " + }, + "accessory": { + "type": "button", + "text": { + "type": "plain_text", + "text": "Edit", + "emoji": true + }, + "value": "public-relations" + } + }, + { + "type": "divider" + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "*#team-updates*\n posts project updates to " + }, + "accessory": { + "type": "button", + "text": { + "type": "plain_text", + "text": "Edit", + "emoji": true + }, + "value": "public-relations" + } + }, + { + "type": "divider" + }, + { + "type": "actions", + "elements": [ + { + "type": "button", + "text": { + "type": "plain_text", + "text": "New Configuration", + "emoji": true + }, + "value": "new_configuration" + } + ] + } + ] +} \ No newline at end of file diff --git a/tests/slack_sdk_fixture/view_home_003.json b/tests/slack_sdk_fixture/view_home_003.json new file mode 100644 index 000000000..b26677a1b --- /dev/null +++ b/tests/slack_sdk_fixture/view_home_003.json @@ -0,0 +1,236 @@ +{ + "type": "home", + "blocks": [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "*Today, 22 October*" + }, + "accessory": { + "type": "button", + "text": { + "type": "plain_text", + "text": "Manage App Settings", + "emoji": true + }, + "value": "settings" + } + }, + { + "type": "actions", + "elements": [ + { + "type": "datepicker", + "initial_date": "2019-10-22", + "placeholder": { + "type": "plain_text", + "text": "Select a date", + "emoji": true + } + } + ] + }, + { + "type": "divider" + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "**\n11:30am — 12:30pm | SF500 · 7F · Saturn (5)\nStatus: ✅ Going" + }, + "accessory": { + "type": "overflow", + "options": [ + { + "text": { + "type": "plain_text", + "text": "View Event Details", + "emoji": true + }, + "value": "view_event_details" + }, + { + "text": { + "type": "plain_text", + "text": "Change Response", + "emoji": true + }, + "value": "change_response" + } + ] + } + }, + { + "type": "actions", + "elements": [ + { + "type": "button", + "text": { + "type": "plain_text", + "text": "Join Video Call", + "emoji": true + }, + "style": "primary", + "value": "join" + } + ] + }, + { + "type": "divider" + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "**\n1:30pm — 2:00pm | SF500 · 4F · Finch (4)" + }, + "accessory": { + "type": "overflow", + "options": [ + { + "text": { + "type": "plain_text", + "text": "View Event Details", + "emoji": true + }, + "value": "view_event_details" + }, + { + "text": { + "type": "plain_text", + "text": "Change Response", + "emoji": true + }, + "value": "change_response" + } + ] + } + }, + { + "type": "actions", + "elements": [ + { + "type": "static_select", + "placeholder": { + "type": "plain_text", + "text": "Going?", + "emoji": true + }, + "options": [ + { + "text": { + "type": "plain_text", + "text": "Going", + "emoji": true + }, + "value": "going" + }, + { + "text": { + "type": "plain_text", + "text": "Maybe", + "emoji": true + }, + "value": "maybe" + }, + { + "text": { + "type": "plain_text", + "text": "Not going", + "emoji": true + }, + "value": "decline" + } + ] + } + ] + }, + { + "type": "divider" + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "**\n4:00pm — 5:30pm | SF500 · 7F · Saturn (5)\nStatus: ✅ Going" + }, + "accessory": { + "type": "overflow", + "options": [ + { + "text": { + "type": "plain_text", + "text": "View Event Details", + "emoji": true + }, + "value": "view_event_details" + }, + { + "text": { + "type": "plain_text", + "text": "Change Response", + "emoji": true + }, + "value": "change_response" + } + ] + } + }, + { + "type": "actions", + "elements": [ + { + "type": "button", + "text": { + "type": "plain_text", + "text": "Join Video Call", + "emoji": true + }, + "style": "primary", + "value": "join" + } + ] + }, + { + "type": "divider" + }, + { + "type": "context", + "elements": [ + { + "type": "image", + "image_url": "https://api.slack.com/img/blocks/bkb_template_images/placeholder.png", + "alt_text": "placeholder" + } + ] + }, + { + "type": "context", + "elements": [ + { + "type": "mrkdwn", + "text": "Past events" + } + ] + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "*Marketing team breakfast*\n8:30am — 9:30am | SF500 · 7F · Saturn (5)" + } + }, + { + "type": "divider" + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "*Coffee chat w/ candidate*\n10:30am — 11:00am | SF500 · 10F · Cafe" + } + } + ] +} \ No newline at end of file diff --git a/tests/slack_sdk_fixture/view_home_004.json b/tests/slack_sdk_fixture/view_home_004.json new file mode 100644 index 000000000..ec133c6e9 --- /dev/null +++ b/tests/slack_sdk_fixture/view_home_004.json @@ -0,0 +1,188 @@ +{ + "type": "home", + "blocks": [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "*Budget Performance*" + }, + "accessory": { + "type": "button", + "text": { + "type": "plain_text", + "text": "Manage App Settings", + "emoji": true + }, + "value": "app_settings" + } + }, + { + "type": "divider" + }, + { + "type": "section", + "fields": [ + { + "type": "mrkdwn", + "text": "*Current Quarter*\nBudget: $18,000 (ends in 53 days)\nSpend: $4,289.70\nRemain: $13,710.30" + }, + { + "type": "mrkdwn", + "text": "*Top Expense Categories*\n:airplane: Flights · 30%\n:taxi: Taxi / Uber / Lyft · 24% \n:knife_fork_plate: Client lunch / meetings · 18%" + } + ] + }, + { + "type": "context", + "elements": [ + { + "type": "image", + "image_url": "https://api.slack.com/img/blocks/bkb_template_images/placeholder.png", + "alt_text": "placeholder" + } + ] + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "*Expenses Awaiting Your Approval*" + } + }, + { + "type": "divider" + }, + { + "type": "context", + "elements": [ + { + "type": "mrkdwn", + "text": "Submitted by" + }, + { + "type": "image", + "image_url": "https://api.slack.com/img/blocks/bkb_template_images/profile_3.png", + "alt_text": "Dwight Schrute" + }, + { + "type": "mrkdwn", + "text": "*Dwight Schrute*" + } + ] + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "*Team Lunch (Internal)*\nCost: *$85.50USD*\nDate: *10/16/2019*\nService Provider: *Honest Sandwiches* \nExpense no. **" + }, + "accessory": { + "type": "image", + "image_url": "https://api.slack.com/img/blocks/bkb_template_images/creditcard.png", + "alt_text": "credit card" + } + }, + { + "type": "actions", + "elements": [ + { + "type": "button", + "text": { + "type": "plain_text", + "text": "Approve", + "emoji": true + }, + "style": "primary", + "value": "approve" + }, + { + "type": "button", + "text": { + "type": "plain_text", + "text": "Decline", + "emoji": true + }, + "style": "danger", + "value": "decline" + }, + { + "type": "button", + "text": { + "type": "plain_text", + "text": "View Details", + "emoji": true + }, + "value": "details" + } + ] + }, + { + "type": "divider" + }, + { + "type": "context", + "elements": [ + { + "type": "mrkdwn", + "text": "Submitted by" + }, + { + "type": "image", + "image_url": "https://api.slack.com/img/blocks/bkb_template_images/profile_2.png", + "alt_text": "Pam Beasely" + }, + { + "type": "mrkdwn", + "text": "*Pam Beasely*" + } + ] + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "*Flights to New York*\nCost: *$520.78USD*\nDate: *10/18/2019*\nService Provider: *Delta Airways*\nExpense no. **" + }, + "accessory": { + "type": "image", + "image_url": "https://api.slack.com/img/blocks/bkb_template_images/plane.png", + "alt_text": "plane" + } + }, + { + "type": "actions", + "elements": [ + { + "type": "button", + "text": { + "type": "plain_text", + "text": "Approve", + "emoji": true + }, + "style": "primary", + "value": "approve" + }, + { + "type": "button", + "text": { + "type": "plain_text", + "text": "Decline", + "emoji": true + }, + "style": "danger", + "value": "decline" + }, + { + "type": "button", + "text": { + "type": "plain_text", + "text": "View Details", + "emoji": true + }, + "value": "details" + } + ] + } + ] +} \ No newline at end of file diff --git a/tests/slack_sdk_fixture/view_home_005.json b/tests/slack_sdk_fixture/view_home_005.json new file mode 100644 index 000000000..60dd7a4e2 --- /dev/null +++ b/tests/slack_sdk_fixture/view_home_005.json @@ -0,0 +1,237 @@ +{ + "type": "home", + "blocks": [ + { + "type": "actions", + "elements": [ + { + "type": "button", + "text": { + "type": "plain_text", + "text": "Create New TODO List", + "emoji": true + }, + "style": "primary", + "value": "create_task" + }, + { + "type": "button", + "text": { + "type": "plain_text", + "text": "Help", + "emoji": true + }, + "value": "help" + } + ] + }, + { + "type": "context", + "elements": [ + { + "type": "image", + "image_url": "https://api.slack.com/img/blocks/bkb_template_images/placeholder.png", + "alt_text": "placeholder" + } + ] + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "*Today*" + }, + "accessory": { + "type": "overflow", + "options": [ + { + "text": { + "type": "plain_text", + "text": ":memo: Edit", + "emoji": true + }, + "value": "edit" + }, + { + "text": { + "type": "plain_text", + "text": ":x: Delete", + "emoji": true + }, + "value": "delete" + } + ] + } + }, + { + "type": "actions", + "elements": [ + { + "type": "checkboxes", + "initial_options": [ + { + "text": { + "type": "mrkdwn", + "text": "~*Get into the garden :house_with_garden:*~" + }, + "value": "option 1" + } + ], + "options": [ + { + "text": { + "type": "mrkdwn", + "text": "~*Get into the garden :house_with_garden:*~" + }, + "value": "option 1" + }, + { + "text": { + "type": "mrkdwn", + "text": "*Get the groundskeeper wet :sweat_drops:*" + }, + "value": "option 2" + }, + { + "text": { + "type": "mrkdwn", + "text": "*Steal the groundskeeper's keys :old_key:*" + }, + "value": "option 3" + }, + { + "text": { + "type": "mrkdwn", + "text": "*Make the groundskeeper wear his sun hat :male-farmer:*" + }, + "value": "option 4" + }, + { + "text": { + "type": "mrkdwn", + "text": "*Rake in the lake :ocean:*" + }, + "value": "option 5" + }, + { + "text": { + "type": "mrkdwn", + "text": "*Have a picnic :knife_fork_plate:*" + }, + "value": "option 6", + "description": { + "type": "mrkdwn", + "text": "Bring to the picnic: sandwich, apple, pumpkin, carrot, basket" + } + } + ] + }, + { + "type": "button", + "text": { + "type": "plain_text", + "text": "Add Item", + "emoji": true + }, + "style": "primary" + } + ] + }, + { + "type": "divider" + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "*Tomorrow*" + }, + "accessory": { + "type": "overflow", + "options": [ + { + "text": { + "type": "plain_text", + "text": ":memo: Edit", + "emoji": true + }, + "value": "edit" + }, + { + "text": { + "type": "plain_text", + "text": ":x: Delete", + "emoji": true + }, + "value": "delete" + } + ] + } + }, + { + "type": "actions", + "elements": [ + { + "type": "checkboxes", + "options": [ + { + "text": { + "type": "mrkdwn", + "text": "*Break the broom :anger:*" + }, + "value": "option 1" + }, + { + "text": { + "type": "mrkdwn", + "text": "*Trap the boy in the phone booth :phone:*" + }, + "value": "option 2" + }, + { + "text": { + "type": "mrkdwn", + "text": "*Make the boy wear the wrong glasses :nerd_face:*" + }, + "value": "option 3" + }, + { + "text": { + "type": "mrkdwn", + "text": "*Make someone buy back their own stuff :money_with_wings:*" + }, + "value": "option 4" + }, + { + "text": { + "type": "mrkdwn", + "text": "*Get on TV :tv:*" + }, + "value": "option 5" + }, + { + "text": { + "type": "mrkdwn", + "text": "*Go shopping :shopping_trolley:*" + }, + "value": "option 6", + "description": { + "type": "mrkdwn", + "text": "Toothbrush, hairbrush, tinned food, cleaner, fruits & vegetables" + } + } + ] + }, + { + "type": "button", + "text": { + "type": "plain_text", + "text": "Add Item", + "emoji": true + }, + "style": "primary" + } + ] + } + ] +} \ No newline at end of file diff --git a/tests/slack_sdk_fixture/view_home_006.json b/tests/slack_sdk_fixture/view_home_006.json new file mode 100644 index 000000000..447779170 --- /dev/null +++ b/tests/slack_sdk_fixture/view_home_006.json @@ -0,0 +1,25 @@ +{ + "id": "VMHU10V25", + "team_id": "T8N4K1JN", + "type": "home", + "blocks": [ + { + "type": "section", + "block_id": "2WGp9", + "text": { + "type": "mrkdwn", + "text": "A simple section with some sample sentence.", + "verbatim": false + } + } + ], + "private_metadata": "Shh it is a secret", + "callback_id": "identify_your_home_tab", + "hash": "156772938.1827394", + "clear_on_close": false, + "notify_on_close": false, + "root_view_id": "VMHU10V25", + "app_id": "AA4928AQ", + "external_id": "some-unique-id", + "bot_id": "BA13894H" +} \ No newline at end of file diff --git a/tests/slack_sdk_fixture/view_modal_001.json b/tests/slack_sdk_fixture/view_modal_001.json new file mode 100644 index 000000000..8907eb3cd --- /dev/null +++ b/tests/slack_sdk_fixture/view_modal_001.json @@ -0,0 +1,192 @@ +{ + "type": "modal", + "callback_id": "modal-id", + "title": { + "type": "plain_text", + "text": "Workplace check-in", + "emoji": true + }, + "submit": { + "type": "plain_text", + "text": "Submit", + "emoji": true + }, + "close": { + "type": "plain_text", + "text": "Cancel", + "emoji": true + }, + "blocks": [ + { + "type": "section", + "text": { + "type": "plain_text", + "text": ":wave: Hey David!\n\nWe'd love to hear from you how we can make this place the best place you’ve ever worked.", + "emoji": true + } + }, + { + "type": "divider" + }, + { + "type": "input", + "label": { + "type": "plain_text", + "text": "You enjoy working here at Pistachio & Co", + "emoji": true + }, + "element": { + "type": "radio_buttons", + "options": [ + { + "text": { + "type": "plain_text", + "text": "Strongly agree", + "emoji": true + }, + "value": "1" + }, + { + "text": { + "type": "plain_text", + "text": "Agree", + "emoji": true + }, + "value": "2" + }, + { + "text": { + "type": "plain_text", + "text": "Neither agree nor disagree", + "emoji": true + }, + "value": "3" + }, + { + "text": { + "type": "plain_text", + "text": "Disagree", + "emoji": true + }, + "value": "4" + }, + { + "text": { + "type": "plain_text", + "text": "Strongly disagree", + "emoji": true + }, + "value": "5" + } + ] + } + }, + { + "type": "input", + "label": { + "type": "plain_text", + "text": "What do you want for our team weekly lunch?", + "emoji": true + }, + "element": { + "type": "multi_static_select", + "placeholder": { + "type": "plain_text", + "text": "Select your favorites", + "emoji": true + }, + "options": [ + { + "text": { + "type": "plain_text", + "text": ":pizza: Pizza", + "emoji": true + }, + "value": "value-0" + }, + { + "text": { + "type": "plain_text", + "text": ":fried_shrimp: Thai food", + "emoji": true + }, + "value": "value-1" + }, + { + "text": { + "type": "plain_text", + "text": ":desert_island: Hawaiian", + "emoji": true + }, + "value": "value-2" + }, + { + "text": { + "type": "plain_text", + "text": ":meat_on_bone: Texas BBQ", + "emoji": true + }, + "value": "value-3" + }, + { + "text": { + "type": "plain_text", + "text": ":hamburger: Burger", + "emoji": true + }, + "value": "value-4" + }, + { + "text": { + "type": "plain_text", + "text": ":taco: Tacos", + "emoji": true + }, + "value": "value-5" + }, + { + "text": { + "type": "plain_text", + "text": ":green_salad: Salad", + "emoji": true + }, + "value": "value-6" + }, + { + "text": { + "type": "plain_text", + "text": ":stew: Indian", + "emoji": true + }, + "value": "value-7" + } + ] + } + }, + { + "type": "input", + "label": { + "type": "plain_text", + "text": "What can we do to improve your experience working here?", + "emoji": true + }, + "element": { + "type": "plain_text_input", + "multiline": true + } + }, + { + "type": "input", + "label": { + "type": "plain_text", + "text": "Anything else you want to tell us?", + "emoji": true + }, + "element": { + "type": "plain_text_input", + "multiline": true + }, + "optional": true + } + ] +} \ No newline at end of file diff --git a/tests/slack_sdk_fixture/view_modal_002.json b/tests/slack_sdk_fixture/view_modal_002.json new file mode 100644 index 000000000..aa9c7a676 --- /dev/null +++ b/tests/slack_sdk_fixture/view_modal_002.json @@ -0,0 +1,197 @@ +{ + "type": "modal", + "title": { + "type": "plain_text", + "text": "Your accommodation", + "emoji": true + }, + "submit": { + "type": "plain_text", + "text": "Submit", + "emoji": true + }, + "close": { + "type": "plain_text", + "text": "Cancel", + "emoji": true + }, + "blocks": [ + { + "type": "section", + "text": { + "type": "plain_text", + "text": "Please choose an option where you'd like to stay from Oct 21 - Oct 23 (2 nights).", + "emoji": true + } + }, + { + "type": "divider" + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "*Airstream Suite*\n*Share with another person*. Private walk-in bathroom. TV. Heating. Kitchen with microwave, basic cooking utensils, wine glasses and silverware." + }, + "accessory": { + "type": "image", + "image_url": "https://api.slack.com/img/blocks/bkb_template_images/Streamline-Beach.png", + "alt_text": "Airstream Suite" + } + }, + { + "type": "context", + "elements": [ + { + "type": "mrkdwn", + "text": "1x Queen Bed" + }, + { + "type": "mrkdwn", + "text": "|" + }, + { + "type": "mrkdwn", + "text": "$220 / night" + } + ] + }, + { + "type": "actions", + "elements": [ + { + "type": "button", + "text": { + "type": "plain_text", + "text": "Choose", + "emoji": true + }, + "value": "click_me_123" + }, + { + "type": "button", + "text": { + "type": "plain_text", + "text": "View Details", + "emoji": true + }, + "value": "click_me_123" + } + ] + }, + { + "type": "divider" + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "*Redwood Suite*\n*Share with 2 other person*. Studio home. Modern bathroom. TV. Heating. Full kitchen. Patio with lounge chairs and campfire style fire pit and grill." + }, + "accessory": { + "type": "image", + "image_url": "https://api.slack.com/img/blocks/bkb_template_images/redwoodcabin.png", + "alt_text": "Redwood Suite" + } + }, + { + "type": "context", + "elements": [ + { + "type": "mrkdwn", + "text": "1x King Bed" + }, + { + "type": "mrkdwn", + "text": "|" + }, + { + "type": "mrkdwn", + "text": "$350 / night" + } + ] + }, + { + "type": "actions", + "elements": [ + { + "type": "button", + "text": { + "type": "plain_text", + "text": "✓ Your Choice", + "emoji": true + }, + "style": "primary", + "value": "click_me_123" + }, + { + "type": "button", + "text": { + "type": "plain_text", + "text": "View Details", + "emoji": true + }, + "value": "click_me_123" + } + ] + }, + { + "type": "divider" + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "*Luxury Tent*\n*One person only*. Shared modern bathrooms and showers in lounge building. Temperature control with heated blankets. Lights and electrical outlets." + }, + "accessory": { + "type": "image", + "image_url": "https://api.slack.com/img/blocks/bkb_template_images/tent.png", + "alt_text": "Redwood Suite" + } + }, + { + "type": "context", + "elements": [ + { + "type": "mrkdwn", + "text": "1x Queen Bed" + }, + { + "type": "mrkdwn", + "text": "|" + }, + { + "type": "mrkdwn", + "text": "$260 / night" + } + ] + }, + { + "type": "actions", + "elements": [ + { + "type": "button", + "text": { + "type": "plain_text", + "text": "Choose", + "emoji": true + }, + "value": "click_me_123" + }, + { + "type": "button", + "text": { + "type": "plain_text", + "text": "View Details", + "emoji": true + }, + "value": "click_me_123" + } + ] + }, + { + "type": "divider" + } + ] +} \ No newline at end of file diff --git a/tests/slack_sdk_fixture/view_modal_003.json b/tests/slack_sdk_fixture/view_modal_003.json new file mode 100644 index 000000000..ed8bc5b22 --- /dev/null +++ b/tests/slack_sdk_fixture/view_modal_003.json @@ -0,0 +1,144 @@ +{ + "type": "modal", + "title": { + "type": "plain_text", + "text": "App menu", + "emoji": true + }, + "submit": { + "type": "plain_text", + "text": "Submit", + "emoji": true + }, + "close": { + "type": "plain_text", + "text": "Cancel", + "emoji": true + }, + "blocks": [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "*Hi !* Here's how I can help you:" + } + }, + { + "type": "divider" + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": ":calendar: *Create event*\nCreate a new event" + }, + "accessory": { + "type": "button", + "text": { + "type": "plain_text", + "text": "Create event", + "emoji": true + }, + "style": "primary", + "value": "click_me_123" + } + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": ":clipboard: *List of events*\nChoose from different event lists" + }, + "accessory": { + "type": "static_select", + "placeholder": { + "type": "plain_text", + "text": "Choose list", + "emoji": true + }, + "options": [ + { + "text": { + "type": "plain_text", + "text": "My events", + "emoji": true + }, + "value": "value-0" + }, + { + "text": { + "type": "plain_text", + "text": "All events", + "emoji": true + }, + "value": "value-1" + }, + { + "text": { + "type": "plain_text", + "text": "Event invites", + "emoji": true + }, + "value": "value-1" + } + ] + } + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": ":gear: *Settings*\nManage your notifications and team settings" + }, + "accessory": { + "type": "static_select", + "placeholder": { + "type": "plain_text", + "text": "Edit settings", + "emoji": true + }, + "options": [ + { + "text": { + "type": "plain_text", + "text": "Notifications", + "emoji": true + }, + "value": "value-0" + }, + { + "text": { + "type": "plain_text", + "text": "Team settings", + "emoji": true + }, + "value": "value-1" + } + ] + } + }, + { + "type": "actions", + "elements": [ + { + "type": "button", + "text": { + "type": "plain_text", + "text": "Send feedback", + "emoji": true + }, + "value": "click_me_123" + }, + { + "type": "button", + "text": { + "type": "plain_text", + "text": "FAQs", + "emoji": true + }, + "value": "click_me_123" + } + ] + } + ] +} \ No newline at end of file diff --git a/tests/slack_sdk_fixture/view_modal_004.json b/tests/slack_sdk_fixture/view_modal_004.json new file mode 100644 index 000000000..66da3749a --- /dev/null +++ b/tests/slack_sdk_fixture/view_modal_004.json @@ -0,0 +1,70 @@ +{ + "type": "modal", + "title": { + "type": "plain_text", + "text": "Notification settings", + "emoji": true + }, + "submit": { + "type": "plain_text", + "text": "Submit", + "emoji": true + }, + "close": { + "type": "plain_text", + "text": "Cancel", + "emoji": true + }, + "blocks": [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "* posts into *\n\nSelect which notifications to send:" + } + }, + { + "type": "actions", + "elements": [ + { + "type": "checkboxes", + "options": [ + { + "text": { + "type": "plain_text", + "text": "New tasks" + }, + "value": "tasks", + "description": { + "type": "plain_text", + "text": "When new tasks are added to project" + } + }, + { + "text": { + "type": "plain_text", + "text": "New comments" + }, + "value": "comments", + "description": { + "type": "plain_text", + "text": "When new comments are added" + } + }, + { + "text": { + "type": "plain_text", + "text": "Project updates" + }, + "value": "updates", + "description": { + "type": "plain_text", + "text": "When project is updated" + } + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/tests/slack_sdk_fixture/view_modal_005.json b/tests/slack_sdk_fixture/view_modal_005.json new file mode 100644 index 000000000..94589796e --- /dev/null +++ b/tests/slack_sdk_fixture/view_modal_005.json @@ -0,0 +1,169 @@ +{ + "type": "modal", + "title": { + "type": "plain_text", + "text": "Your itinerary", + "emoji": true + }, + "submit": { + "type": "plain_text", + "text": "Submit", + "emoji": true + }, + "close": { + "type": "plain_text", + "text": "Cancel", + "emoji": true + }, + "blocks": [ + { + "type": "section", + "block_id": "1", + "text": { + "type": "mrkdwn", + "text": ":tada: You're all set! This is your booking summary." + } + }, + { + "type": "divider", + "block_id": "2" + }, + { + "type": "section", + "block_id": "3", + "fields": [ + { + "type": "mrkdwn", + "text": "*Attendee*\nKatie Chen" + }, + { + "type": "mrkdwn", + "text": "*Date*\nOct 22-23" + } + ] + }, + { + "type": "context", + "block_id": "4", + "elements": [ + { + "type": "mrkdwn", + "text": ":house: Accommodation" + } + ] + }, + { + "type": "divider", + "block_id": "5" + }, + { + "type": "section", + "block_id": "6", + "text": { + "type": "mrkdwn", + "text": "*Redwood Suite*\n*Share with 2 other person.* Studio home. Modern bathroom. TV. Heating. Full kitchen. Patio with lounge chairs and campfire style fire pit and grill." + }, + "accessory": { + "type": "image", + "image_url": "https://api.slack.com/img/blocks/bkb_template_images/redwood-suite.png", + "alt_text": "Redwood Suite" + } + }, + { + "type": "context", + "block_id": "7", + "elements": [ + { + "type": "mrkdwn", + "text": ":fork_and_knife: Food & Dietary restrictions" + } + ] + }, + { + "type": "divider", + "block_id": "8" + }, + { + "type": "section", + "block_id": "9", + "text": { + "type": "mrkdwn", + "text": "*All-rounder*\nYou eat most meats, seafood, dairy and vegetables." + } + }, + { + "type": "context", + "block_id": "10", + "elements": [ + { + "type": "mrkdwn", + "text": ":woman-running: Activities" + } + ] + }, + { + "type": "divider", + "block_id": "11" + }, + { + "type": "section", + "block_id": "12", + "text": { + "type": "mrkdwn", + "text": "*Winery tour and tasting*" + }, + "fields": [ + { + "type": "plain_text", + "text": "Wednesday, Oct 22 2019, 2pm-5pm", + "emoji": true + }, + { + "type": "plain_text", + "text": "Hosted by Sandra Mullens", + "emoji": true + } + ] + }, + { + "type": "section", + "block_id": "13", + "text": { + "type": "mrkdwn", + "text": "*Sunrise hike to Mount Amazing*" + }, + "fields": [ + { + "type": "plain_text", + "text": "Thursday, Oct 23 2019, 5:30am", + "emoji": true + }, + { + "type": "plain_text", + "text": "Hosted by Jordan Smith", + "emoji": true + } + ] + }, + { + "type": "section", + "block_id": "14", + "text": { + "type": "mrkdwn", + "text": "*Design systems brainstorm*" + }, + "fields": [ + { + "type": "plain_text", + "text": "Thursday, Oct 23 2019, 11a", + "emoji": true + }, + { + "type": "plain_text", + "text": "Hosted by Mary Lee", + "emoji": true + } + ] + } + ] +} \ No newline at end of file diff --git a/tests/slack_sdk_fixture/view_modal_006.json b/tests/slack_sdk_fixture/view_modal_006.json new file mode 100644 index 000000000..9080a7298 --- /dev/null +++ b/tests/slack_sdk_fixture/view_modal_006.json @@ -0,0 +1,424 @@ +{ + "type": "modal", + "title": { + "type": "plain_text", + "text": "Ticket app", + "emoji": true + }, + "submit": { + "type": "plain_text", + "text": "Submit", + "emoji": true + }, + "close": { + "type": "plain_text", + "text": "Cancel", + "emoji": true + }, + "blocks": [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "Pick a ticket list from the dropdown" + }, + "accessory": { + "type": "static_select", + "placeholder": { + "type": "plain_text", + "text": "Select an item", + "emoji": true + }, + "options": [ + { + "text": { + "type": "plain_text", + "text": "All Tickets", + "emoji": true + }, + "value": "all_tickets" + }, + { + "text": { + "type": "plain_text", + "text": "Assigned To Me", + "emoji": true + }, + "value": "assigned_to_me" + }, + { + "text": { + "type": "plain_text", + "text": "Issued By Me", + "emoji": true + }, + "value": "issued_by_me" + } + ], + "initial_option": { + "text": { + "type": "plain_text", + "text": "Assigned To Me", + "emoji": true + }, + "value": "assigned_to_me" + } + } + }, + { + "type": "divider" + }, + { + "type": "context", + "elements": [ + { + "type": "image", + "image_url": "https://api.slack.com/img/blocks/bkb_template_images/highpriority.png", + "alt_text": "High Priority" + }, + { + "type": "mrkdwn", + "text": "*High Priority*" + } + ] + }, + { + "type": "divider" + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "**" + }, + "accessory": { + "type": "overflow", + "options": [ + { + "text": { + "type": "plain_text", + "text": ":white_check_mark: Mark as done", + "emoji": true + }, + "value": "done" + }, + { + "text": { + "type": "plain_text", + "text": ":pencil: Edit", + "emoji": true + }, + "value": "edit" + }, + { + "text": { + "type": "plain_text", + "text": ":x: Delete", + "emoji": true + }, + "value": "delete" + } + ] + } + }, + { + "type": "context", + "elements": [ + { + "type": "mrkdwn", + "text": "Awaiting Release" + }, + { + "type": "image", + "image_url": "https://api.slack.com/img/blocks/bkb_template_images/task-icon.png", + "alt_text": "Task Icon" + }, + { + "type": "mrkdwn", + "text": "Task" + }, + { + "type": "image", + "image_url": "https://api.slack.com/img/blocks/bkb_template_images/profile_1.png", + "alt_text": "Michael Scott" + }, + { + "type": "mrkdwn", + "text": "" + } + ] + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "**" + }, + "accessory": { + "type": "overflow", + "options": [ + { + "text": { + "type": "plain_text", + "text": ":white_check_mark: Mark as done", + "emoji": true + }, + "value": "done" + }, + { + "text": { + "type": "plain_text", + "text": ":pencil: Edit", + "emoji": true + }, + "value": "edit" + }, + { + "text": { + "type": "plain_text", + "text": ":x: Delete", + "emoji": true + }, + "value": "delete" + } + ] + } + }, + { + "type": "context", + "elements": [ + { + "type": "mrkdwn", + "text": "Open" + }, + { + "type": "image", + "image_url": "https://api.slack.com/img/blocks/bkb_template_images/newfeature.png", + "alt_text": "New Feature Icon" + }, + { + "type": "mrkdwn", + "text": "New Feature" + }, + { + "type": "image", + "image_url": "https://api.slack.com/img/blocks/bkb_template_images/profile_2.png", + "alt_text": "Pam Beasely" + }, + { + "type": "mrkdwn", + "text": "" + } + ] + }, + { + "type": "divider" + }, + { + "type": "context", + "elements": [ + { + "type": "image", + "image_url": "https://api.slack.com/img/blocks/bkb_template_images/mediumpriority.png", + "alt_text": "palm tree" + }, + { + "type": "mrkdwn", + "text": "*Medium Priority*" + } + ] + }, + { + "type": "divider" + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "**" + }, + "accessory": { + "type": "overflow", + "options": [ + { + "text": { + "type": "plain_text", + "text": ":white_check_mark: Mark as done", + "emoji": true + }, + "value": "done" + }, + { + "text": { + "type": "plain_text", + "text": ":pencil: Edit", + "emoji": true + }, + "value": "edit" + }, + { + "text": { + "type": "plain_text", + "text": ":x: Delete", + "emoji": true + }, + "value": "delete" + } + ] + } + }, + { + "type": "context", + "elements": [ + { + "type": "mrkdwn", + "text": "Awaiting Release" + }, + { + "type": "image", + "image_url": "https://api.slack.com/img/blocks/bkb_template_images/task-icon.png", + "alt_text": "Task Icon" + }, + { + "type": "mrkdwn", + "text": "Task" + }, + { + "type": "image", + "image_url": "https://api.slack.com/img/blocks/bkb_template_images/profile_1.png", + "alt_text": "Michael Scott" + }, + { + "type": "mrkdwn", + "text": "" + } + ] + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "**" + }, + "accessory": { + "type": "overflow", + "options": [ + { + "text": { + "type": "plain_text", + "text": ":white_check_mark: Mark as done", + "emoji": true + }, + "value": "done" + }, + { + "text": { + "type": "plain_text", + "text": ":pencil: Edit", + "emoji": true + }, + "value": "edit" + }, + { + "text": { + "type": "plain_text", + "text": ":x: Delete", + "emoji": true + }, + "value": "delete" + } + ] + } + }, + { + "type": "context", + "elements": [ + { + "type": "mrkdwn", + "text": "Open" + }, + { + "type": "image", + "image_url": "https://api.slack.com/img/blocks/bkb_template_images/newfeature.png", + "alt_text": "New Feature Icon" + }, + { + "type": "mrkdwn", + "text": "New Feature" + }, + { + "type": "image", + "image_url": "https://api.slack.com/img/blocks/bkb_template_images/profile_2.png", + "alt_text": "Pam Beasely" + }, + { + "type": "mrkdwn", + "text": "" + } + ] + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "**" + }, + "accessory": { + "type": "overflow", + "options": [ + { + "text": { + "type": "plain_text", + "text": ":white_check_mark: Mark as done", + "emoji": true + }, + "value": "done" + }, + { + "text": { + "type": "plain_text", + "text": ":pencil: Edit", + "emoji": true + }, + "value": "edit" + }, + { + "text": { + "type": "plain_text", + "text": ":x: Delete", + "emoji": true + }, + "value": "delete" + } + ] + } + }, + { + "type": "context", + "elements": [ + { + "type": "mrkdwn", + "text": "Awaiting Release" + }, + { + "type": "image", + "image_url": "https://api.slack.com/img/blocks/bkb_template_images/task-icon.png", + "alt_text": "Task Icon" + }, + { + "type": "mrkdwn", + "text": "Task" + }, + { + "type": "image", + "image_url": "https://api.slack.com/img/blocks/bkb_template_images/profile_1.png", + "alt_text": "Michael Scott" + }, + { + "type": "mrkdwn", + "text": "" + } + ] + } + ] +} \ No newline at end of file diff --git a/tests/slack_sdk_fixture/view_modal_007.json b/tests/slack_sdk_fixture/view_modal_007.json new file mode 100644 index 000000000..e8b338654 --- /dev/null +++ b/tests/slack_sdk_fixture/view_modal_007.json @@ -0,0 +1,524 @@ +{ + "type": "modal", + "title": { + "type": "plain_text", + "text": "My App", + "emoji": true + }, + "submit": { + "type": "plain_text", + "text": "Submit", + "emoji": true + }, + "close": { + "type": "plain_text", + "text": "Cancel", + "emoji": true + }, + "blocks": [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "This is a mrkdwn section block :ghost: *this is bold*, and ~this is crossed out~, and " + } + }, + { + "type": "image", + "title": { + "type": "plain_text", + "text": "image1", + "emoji": true + }, + "image_url": "https://api.slack.com/img/blocks/bkb_template_images/beagle.png", + "alt_text": "image1" + }, + { + "type": "context", + "elements": [ + { + "type": "mrkdwn", + "text": "For more info, contact " + } + ] + }, + { + "type": "divider" + }, + { + "type": "actions", + "elements": [ + { + "type": "datepicker", + "initial_date": "1990-04-28", + "placeholder": { + "type": "plain_text", + "text": "Select a date", + "emoji": true + } + }, + { + "type": "datepicker", + "initial_date": "1990-04-28", + "placeholder": { + "type": "plain_text", + "text": "Select a date", + "emoji": true + } + } + ] + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "You can add an image next to text in this block." + }, + "accessory": { + "type": "image", + "image_url": "https://api.slack.com/img/blocks/bkb_template_images/plants.png", + "alt_text": "plants" + } + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "You can add a button alongside text in your message. " + }, + "accessory": { + "type": "button", + "text": { + "type": "plain_text", + "text": "Button", + "emoji": true + }, + "value": "click_me_123" + } + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "Pick an item from the dropdown list" + }, + "accessory": { + "type": "static_select", + "placeholder": { + "type": "plain_text", + "text": "Select an item", + "emoji": true + }, + "options": [ + { + "text": { + "type": "plain_text", + "text": "Choice 1", + "emoji": true + }, + "value": "value-0" + }, + { + "text": { + "type": "plain_text", + "text": "Choice 2", + "emoji": true + }, + "value": "value-1" + }, + { + "text": { + "type": "plain_text", + "text": "Choice 3", + "emoji": true + }, + "value": "value-2" + } + ] + } + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "Pick one or more items from the list" + }, + "accessory": { + "type": "multi_static_select", + "placeholder": { + "type": "plain_text", + "text": "Select items", + "emoji": true + }, + "options": [ + { + "text": { + "type": "plain_text", + "text": "Choice 1", + "emoji": true + }, + "value": "value-0" + }, + { + "text": { + "type": "plain_text", + "text": "Choice 2", + "emoji": true + }, + "value": "value-1" + }, + { + "text": { + "type": "plain_text", + "text": "Choice 3", + "emoji": true + }, + "value": "value-2" + } + ] + } + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "This block has an overflow menu." + }, + "accessory": { + "type": "overflow", + "options": [ + { + "text": { + "type": "plain_text", + "text": "Option 1", + "emoji": true + }, + "value": "value-0" + }, + { + "text": { + "type": "plain_text", + "text": "Option 2", + "emoji": true + }, + "value": "value-1" + }, + { + "text": { + "type": "plain_text", + "text": "Option 3", + "emoji": true + }, + "value": "value-2" + }, + { + "text": { + "type": "plain_text", + "text": "Option 4", + "emoji": true + }, + "value": "value-3" + } + ] + } + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "Pick a date for the deadline." + }, + "accessory": { + "type": "datepicker", + "initial_date": "1990-04-28", + "placeholder": { + "type": "plain_text", + "text": "Select a date", + "emoji": true + } + } + }, + { + "type": "section", + "fields": [ + { + "type": "plain_text", + "text": "*this is plain_text text*", + "emoji": true + }, + { + "type": "plain_text", + "text": "*this is plain_text text*", + "emoji": true + }, + { + "type": "plain_text", + "text": "*this is plain_text text*", + "emoji": true + }, + { + "type": "plain_text", + "text": "*this is plain_text text*", + "emoji": true + }, + { + "type": "plain_text", + "text": "*this is plain_text text*", + "emoji": true + } + ] + }, + { + "type": "section", + "fields": [ + { + "type": "mrkdwn", + "text": "This is a section block with _another_ fields radio button accessory" + }, + { + "type": "mrkdwn", + "text": "This is a section block with *fields* radio button accessory" + }, + { + "type": "mrkdwn", + "text": "This is a section block." + } + ], + "accessory": { + "type": "radio_buttons", + "initial_option": { + "text": { + "type": "plain_text", + "text": "Option 3" + }, + "value": "option 3", + "description": { + "type": "plain_text", + "text": "Description for option 3" + } + }, + "options": [ + { + "text": { + "type": "plain_text", + "text": "Option 1" + }, + "value": "option 1", + "description": { + "type": "plain_text", + "text": "Description for option 1" + } + }, + { + "text": { + "type": "plain_text", + "text": "Option 2" + }, + "value": "option 2", + "description": { + "type": "plain_text", + "text": "Description for option 2" + } + }, + { + "text": { + "type": "plain_text", + "text": "Option 3" + }, + "value": "option 3", + "description": { + "type": "plain_text", + "text": "Description for option 3" + } + } + ] + } + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "This is a section block with checkboxes." + }, + "accessory": { + "type": "checkboxes", + "options": [ + { + "text": { + "type": "plain_text", + "text": "*this is plain_text text*", + "emoji": true + }, + "description": { + "type": "plain_text", + "text": "*this is plain_text text*", + "emoji": true + }, + "value": "value-0" + }, + { + "text": { + "type": "plain_text", + "text": "*this is plain_text text*", + "emoji": true + }, + "description": { + "type": "plain_text", + "text": "*this is plain_text text*", + "emoji": true + }, + "value": "value-1" + }, + { + "text": { + "type": "plain_text", + "text": "*this is plain_text text*", + "emoji": true + }, + "description": { + "type": "plain_text", + "text": "*this is plain_text text*", + "emoji": true + }, + "value": "value-2" + } + ] + } + }, + { + "type": "input", + "element": { + "type": "plain_text_input" + }, + "label": { + "type": "plain_text", + "text": "Label", + "emoji": true + } + }, + { + "type": "input", + "element": { + "type": "plain_text_input", + "multiline": true + }, + "label": { + "type": "plain_text", + "text": "Label", + "emoji": true + } + }, + { + "type": "input", + "element": { + "type": "datepicker", + "initial_date": "1990-04-28", + "placeholder": { + "type": "plain_text", + "text": "Select a date", + "emoji": true + } + }, + "label": { + "type": "plain_text", + "text": "Label", + "emoji": true + } + }, + { + "type": "input", + "element": { + "type": "static_select", + "placeholder": { + "type": "plain_text", + "text": "Select an item", + "emoji": true + }, + "options": [ + { + "text": { + "type": "plain_text", + "text": "*this is plain_text text*", + "emoji": true + }, + "value": "value-0" + }, + { + "text": { + "type": "plain_text", + "text": "*this is plain_text text*", + "emoji": true + }, + "value": "value-1" + }, + { + "text": { + "type": "plain_text", + "text": "*this is plain_text text*", + "emoji": true + }, + "value": "value-2" + } + ] + }, + "label": { + "type": "plain_text", + "text": "Label", + "emoji": true + } + }, + { + "type": "input", + "element": { + "type": "multi_users_select", + "placeholder": { + "type": "plain_text", + "text": "Select users", + "emoji": true + } + }, + "label": { + "type": "plain_text", + "text": "Label", + "emoji": true + } + }, + { + "type": "input", + "element": { + "type": "checkboxes", + "options": [ + { + "text": { + "type": "plain_text", + "text": "*this is plain_text text*", + "emoji": true + }, + "value": "value-0" + }, + { + "text": { + "type": "plain_text", + "text": "*this is plain_text text*", + "emoji": true + }, + "value": "value-1" + }, + { + "text": { + "type": "plain_text", + "text": "*this is plain_text text*", + "emoji": true + }, + "value": "value-2" + } + ] + }, + "label": { + "type": "plain_text", + "text": "Label", + "emoji": true + } + } + ] +} \ No newline at end of file diff --git a/tests/slack_sdk_fixture/view_modal_008.json b/tests/slack_sdk_fixture/view_modal_008.json new file mode 100644 index 000000000..e2999be32 --- /dev/null +++ b/tests/slack_sdk_fixture/view_modal_008.json @@ -0,0 +1,39 @@ +{ + "type": "modal", + "title": { + "type": "plain_text", + "text": "My App", + "emoji": true + }, + "submit": { + "type": "plain_text", + "text": "Submit", + "emoji": true + }, + "close": { + "type": "plain_text", + "text": "Cancel", + "emoji": true + }, + "private_metadata": "something important here", + "blocks": [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "This is a mrkdwn section block :ghost: *this is bold*, and ~this is crossed out~, and " + } + } + ], + "state": { + "values": { + "multi-line": { + "ml-value": { + "type": "plain_text_input", + "value": "This is my example inputted value" + } + } + } + }, + "hash": "156663117.cd33ad1f" +} \ No newline at end of file diff --git a/tests/slack_sdk_fixture/view_modal_009.json b/tests/slack_sdk_fixture/view_modal_009.json new file mode 100644 index 000000000..74640861e --- /dev/null +++ b/tests/slack_sdk_fixture/view_modal_009.json @@ -0,0 +1,46 @@ +{ + "id": "VNM522E2U", + "team_id": "T9M4RL1JM", + "type": "modal", + "title": { + "type": "plain_text", + "text": "Pushed Modal", + "emoji": true + }, + "close": { + "type": "plain_text", + "text": "Back", + "emoji": true + }, + "submit": { + "type": "plain_text", + "text": "Save", + "emoji": true + }, + "blocks": [ + { + "type": "input", + "block_id": "edit_details", + "element": { + "type": "plain_text_input", + "action_id": "detail_input" + }, + "label": { + "type": "plain_text", + "text": "Edit details" + } + } + ], + "private_metadata": "secret", + "callback_id": "view_4", + "external_id": "some-unique-id", + "state": { + "values": {} + }, + "hash": "1569362015.55b5e41b", + "clear_on_close": true, + "notify_on_close": false, + "root_view_id": "VNN729E3U", + "app_id": "AAD3351BQ", + "bot_id": "BADF7A34H" +} \ No newline at end of file diff --git a/tests/slack_sdk_fixture/view_modal_010.json b/tests/slack_sdk_fixture/view_modal_010.json new file mode 100644 index 000000000..b01739874 --- /dev/null +++ b/tests/slack_sdk_fixture/view_modal_010.json @@ -0,0 +1,41 @@ +{ + "id": "VMHU10V25", + "team_id": "T8N4K1JN", + "type": "modal", + "title": { + "type": "plain_text", + "text": "Quite a plain modal" + }, + "submit": { + "type": "plain_text", + "text": "Create" + }, + "blocks": [ + { + "type": "input", + "block_id": "a_block_id", + "label": { + "type": "plain_text", + "text": "A simple label", + "emoji": true + }, + "optional": false, + "element": { + "type": "plain_text_input", + "action_id": "an_action_id" + } + } + ], + "private_metadata": "Shh it is a secret", + "callback_id": "identify_your_modals", + "external_id": "some-unique-id", + "state": { + "values": {} + }, + "hash": "156772938.1827394", + "clear_on_close": false, + "notify_on_close": false, + "root_view_id": "VMHU10V25", + "app_id": "AA4928AQ", + "bot_id": "BA13894H" +} \ No newline at end of file diff --git a/tests/slack_sdk_fixture/web_response_admin_convo_pagination.json b/tests/slack_sdk_fixture/web_response_admin_convo_pagination.json new file mode 100644 index 000000000..6b7bd7efd --- /dev/null +++ b/tests/slack_sdk_fixture/web_response_admin_convo_pagination.json @@ -0,0 +1,28 @@ +{ + "ok": true, + "conversations": [ + { + "id": "C013T0FTKU3", + "name": "random", + "purpose": "test", + "member_count": 11, + "created": 1589700571, + "creator_id": "W111", + "is_private": false, + "is_archived": false, + "is_general": false, + "last_activity_ts": 1599130434000900, + "is_ext_shared": false, + "is_global_shared": false, + "is_org_default": false, + "is_org_mandatory": false, + "is_org_shared": false, + "is_frozen": false, + "internal_team_ids_count": 1, + "internal_team_ids_sample_team": "T111", + "pending_connected_team_ids": [], + "is_pending_ext_shared": false + } + ], + "next_cursor": "1" +} diff --git a/tests/slack_sdk_fixture/web_response_admin_convo_pagination_1.json b/tests/slack_sdk_fixture/web_response_admin_convo_pagination_1.json new file mode 100644 index 000000000..3a6a2ffa3 --- /dev/null +++ b/tests/slack_sdk_fixture/web_response_admin_convo_pagination_1.json @@ -0,0 +1,5 @@ +{ + "ok": true, + "conversations": [], + "next_cursor": "" +} diff --git a/tests/slack_sdk_fixture/web_response_api_test.json b/tests/slack_sdk_fixture/web_response_api_test.json new file mode 100644 index 000000000..47971aa3c --- /dev/null +++ b/tests/slack_sdk_fixture/web_response_api_test.json @@ -0,0 +1,3 @@ +{ + "ok": true +} \ No newline at end of file diff --git a/tests/slack_sdk_fixture/web_response_api_test_false.json b/tests/slack_sdk_fixture/web_response_api_test_false.json new file mode 100644 index 000000000..8cba4747d --- /dev/null +++ b/tests/slack_sdk_fixture/web_response_api_test_false.json @@ -0,0 +1,3 @@ +{ + "ok": false +} \ No newline at end of file diff --git a/tests/slack_sdk_fixture/web_response_chat_stream_test.json b/tests/slack_sdk_fixture/web_response_chat_stream_test.json new file mode 100644 index 000000000..2b5f29d01 --- /dev/null +++ b/tests/slack_sdk_fixture/web_response_chat_stream_test.json @@ -0,0 +1,4 @@ +{ + "ok": true, + "ts": "123.123" +} diff --git a/tests/slack_sdk_fixture/web_response_chat_stream_test_missing_ts.json b/tests/slack_sdk_fixture/web_response_chat_stream_test_missing_ts.json new file mode 100644 index 000000000..0287aedde --- /dev/null +++ b/tests/slack_sdk_fixture/web_response_chat_stream_test_missing_ts.json @@ -0,0 +1,3 @@ +{ + "ok": true +} diff --git a/tests/slack_sdk_fixture/web_response_chat_stream_test_token1.json b/tests/slack_sdk_fixture/web_response_chat_stream_test_token1.json new file mode 100644 index 000000000..0287aedde --- /dev/null +++ b/tests/slack_sdk_fixture/web_response_chat_stream_test_token1.json @@ -0,0 +1,3 @@ +{ + "ok": true +} diff --git a/tests/slack_sdk_fixture/web_response_chat_stream_test_token2.json b/tests/slack_sdk_fixture/web_response_chat_stream_test_token2.json new file mode 100644 index 000000000..0287aedde --- /dev/null +++ b/tests/slack_sdk_fixture/web_response_chat_stream_test_token2.json @@ -0,0 +1,3 @@ +{ + "ok": true +} diff --git a/tests/slack_sdk_fixture/web_response_conversations_list.json b/tests/slack_sdk_fixture/web_response_conversations_list.json new file mode 100644 index 000000000..e909a1dd1 --- /dev/null +++ b/tests/slack_sdk_fixture/web_response_conversations_list.json @@ -0,0 +1,82 @@ +{ + "ok": true, + "channels": [ + { + "id": "C111", + "name": "general", + "is_channel": true, + "is_group": false, + "is_im": false, + "created": 1421924831, + "is_archived": false, + "is_general": true, + "unlinked": 0, + "name_normalized": "general", + "is_shared": false, + "creator": "U111", + "is_ext_shared": false, + "is_org_shared": false, + "shared_team_ids": [ + "T111" + ], + "pending_shared": [], + "pending_connected_team_ids": [], + "is_pending_ext_shared": false, + "is_member": true, + "is_private": false, + "is_mpim": false, + "topic": { + "value": "", + "creator": "", + "last_set": 0 + }, + "purpose": { + "value": "This channel is for team-wide communication and announcements. All team members are in this channel.", + "creator": "", + "last_set": 0 + }, + "previous_names": [], + "num_members": 8 + }, + { + "id": "C222", + "name": "random", + "is_channel": true, + "is_group": false, + "is_im": false, + "created": 1421924831, + "is_archived": false, + "is_general": false, + "unlinked": 0, + "name_normalized": "random", + "is_shared": false, + "creator": "U111", + "is_ext_shared": false, + "is_org_shared": false, + "shared_team_ids": [ + "T111" + ], + "pending_shared": [], + "pending_connected_team_ids": [], + "is_pending_ext_shared": false, + "is_member": true, + "is_private": false, + "is_mpim": false, + "topic": { + "value": "", + "creator": "", + "last_set": 0 + }, + "purpose": { + "value": "A place for non-work banter, links, articles of interest, humor or anything else which you\u0027d like concentrated in some place other than work-related channels.", + "creator": "", + "last_set": 0 + }, + "previous_names": [], + "num_members": 10 + } + ], + "response_metadata": { + "next_cursor": "" + } +} \ No newline at end of file diff --git a/tests/slack_sdk_fixture/web_response_conversations_list_pagination.json b/tests/slack_sdk_fixture/web_response_conversations_list_pagination.json new file mode 100644 index 000000000..9d4b9b847 --- /dev/null +++ b/tests/slack_sdk_fixture/web_response_conversations_list_pagination.json @@ -0,0 +1,11 @@ +{ + "ok": true, + "channels": [ + { + "id": "C1" + } + ], + "response_metadata": { + "next_cursor": "has_page2" + } +} \ No newline at end of file diff --git a/tests/slack_sdk_fixture/web_response_conversations_list_pagination2.json b/tests/slack_sdk_fixture/web_response_conversations_list_pagination2.json new file mode 100644 index 000000000..8709f7938 --- /dev/null +++ b/tests/slack_sdk_fixture/web_response_conversations_list_pagination2.json @@ -0,0 +1,11 @@ +{ + "ok": true, + "channels": [ + { + "id": "C2" + } + ], + "response_metadata": { + "next_cursor": "page2" + } +} \ No newline at end of file diff --git a/tests/slack_sdk_fixture/web_response_conversations_list_pagination2_page2.json b/tests/slack_sdk_fixture/web_response_conversations_list_pagination2_page2.json new file mode 100644 index 000000000..5175f163c --- /dev/null +++ b/tests/slack_sdk_fixture/web_response_conversations_list_pagination2_page2.json @@ -0,0 +1,8 @@ +{ + "ok": true, + "channels": [ + { + "id": "C3" + } + ] +} \ No newline at end of file diff --git a/tests/slack_sdk_fixture/web_response_conversations_list_pagination_has_page2.json b/tests/slack_sdk_fixture/web_response_conversations_list_pagination_has_page2.json new file mode 100644 index 000000000..f34bd4f2c --- /dev/null +++ b/tests/slack_sdk_fixture/web_response_conversations_list_pagination_has_page2.json @@ -0,0 +1,11 @@ +{ + "ok": true, + "channels": [ + { + "id": "C2" + } + ], + "response_metadata": { + "next_cursor": "has_page3" + } +} \ No newline at end of file diff --git a/tests/slack_sdk_fixture/web_response_conversations_list_pagination_has_page3.json b/tests/slack_sdk_fixture/web_response_conversations_list_pagination_has_page3.json new file mode 100644 index 000000000..5175f163c --- /dev/null +++ b/tests/slack_sdk_fixture/web_response_conversations_list_pagination_has_page3.json @@ -0,0 +1,8 @@ +{ + "ok": true, + "channels": [ + { + "id": "C3" + } + ] +} \ No newline at end of file diff --git a/tests/slack_sdk_fixture/web_response_fatal_error_only_once.json b/tests/slack_sdk_fixture/web_response_fatal_error_only_once.json new file mode 100644 index 000000000..47971aa3c --- /dev/null +++ b/tests/slack_sdk_fixture/web_response_fatal_error_only_once.json @@ -0,0 +1,3 @@ +{ + "ok": true +} \ No newline at end of file diff --git a/tests/slack_sdk_fixture/web_response_rate_limited_only_once.json b/tests/slack_sdk_fixture/web_response_rate_limited_only_once.json new file mode 100644 index 000000000..47971aa3c --- /dev/null +++ b/tests/slack_sdk_fixture/web_response_rate_limited_only_once.json @@ -0,0 +1,3 @@ +{ + "ok": true +} \ No newline at end of file diff --git a/tests/slack_sdk_fixture/web_response_ratelimited_only_once.json b/tests/slack_sdk_fixture/web_response_ratelimited_only_once.json new file mode 100644 index 000000000..47971aa3c --- /dev/null +++ b/tests/slack_sdk_fixture/web_response_ratelimited_only_once.json @@ -0,0 +1,3 @@ +{ + "ok": true +} \ No newline at end of file diff --git a/tests/slack_sdk_fixture/web_response_users_list_pagination.json b/tests/slack_sdk_fixture/web_response_users_list_pagination.json new file mode 100644 index 000000000..11019de02 --- /dev/null +++ b/tests/slack_sdk_fixture/web_response_users_list_pagination.json @@ -0,0 +1,10 @@ +{ + "ok": true, + "members": [ + "Bob", + "cat" + ], + "response_metadata": { + "next_cursor": 1 + } +} \ No newline at end of file diff --git a/tests/slack_sdk_fixture/web_response_users_list_pagination_1.json b/tests/slack_sdk_fixture/web_response_users_list_pagination_1.json new file mode 100644 index 000000000..e9e4ca59e --- /dev/null +++ b/tests/slack_sdk_fixture/web_response_users_list_pagination_1.json @@ -0,0 +1,10 @@ +{ + "ok": true, + "members": [ + "Kevin", + "dog" + ], + "response_metadata": { + "next_cursor": "" + } +} \ No newline at end of file diff --git a/tests/slack_sdk_fixture/web_response_users_setPhoto.json b/tests/slack_sdk_fixture/web_response_users_setPhoto.json new file mode 100644 index 000000000..47971aa3c --- /dev/null +++ b/tests/slack_sdk_fixture/web_response_users_setPhoto.json @@ -0,0 +1,3 @@ +{ + "ok": true +} \ No newline at end of file diff --git "a/tests/slack_sdk_fixture/\346\227\245\346\234\254\350\252\236.txt" "b/tests/slack_sdk_fixture/\346\227\245\346\234\254\350\252\236.txt" new file mode 100644 index 000000000..e83710b95 --- /dev/null +++ "b/tests/slack_sdk_fixture/\346\227\245\346\234\254\350\252\236.txt" @@ -0,0 +1 @@ +日本語の文書です。 \ No newline at end of file diff --git a/tests/test_aiohttp_version_checker.py b/tests/test_aiohttp_version_checker.py new file mode 100644 index 000000000..ee1998992 --- /dev/null +++ b/tests/test_aiohttp_version_checker.py @@ -0,0 +1,42 @@ +import logging +import unittest + +from slack_sdk.aiohttp_version_checker import validate_aiohttp_version + + +class TestAiohttpVersionChecker(unittest.TestCase): + def setUp(self): + self.logger = logging.getLogger(__name__) + + def tearDown(self): + pass + + def test_not_recommended_versions(self): + state = {"counter": 0} + + def print(message: str): + state["counter"] = state["counter"] + 1 + + validate_aiohttp_version("2.1.3", print) + self.assertEqual(state["counter"], 1) + validate_aiohttp_version("3.6.3", print) + self.assertEqual(state["counter"], 2) + validate_aiohttp_version("3.7.0", print) + self.assertEqual(state["counter"], 3) + + def test_recommended_versions(self): + state = {"counter": 0} + + def print(message: str): + state["counter"] = state["counter"] + 1 + + validate_aiohttp_version("3.7.1", print) + self.assertEqual(state["counter"], 0) + validate_aiohttp_version("3.7.3", print) + self.assertEqual(state["counter"], 0) + validate_aiohttp_version("3.8.0", print) + self.assertEqual(state["counter"], 0) + validate_aiohttp_version("4.0.0", print) + self.assertEqual(state["counter"], 0) + validate_aiohttp_version("4.0.0rc1", print) + self.assertEqual(state["counter"], 0) diff --git a/tests/test_asyncio_event_loops.py b/tests/test_asyncio_event_loops.py new file mode 100644 index 000000000..afc7282dd --- /dev/null +++ b/tests/test_asyncio_event_loops.py @@ -0,0 +1,55 @@ +import logging +import unittest +import asyncio + +import pytest + +from slack import WebClient, RTMClient + + +class TestAsyncioEventLoops(unittest.TestCase): + """This test was added to verify the behavior of asyncio.new_event_loop(). + + Also, the tests here ensure WebClient and RTMClient don't generate a large number of event loops + even when a lot of those instances are created. + """ + + def setUp(self): + self.logger = logging.getLogger(__name__) + + def tearDown(self): + pass + + @pytest.mark.skip("The result of this test depends on the environment") + def test_the_cost_of_event_loop_creation(self): + # create 100 event loops + loops = [] + try: + upper_limit = 0 + for i in range(1000): + try: + loops.append(asyncio.new_event_loop()) + except OSError as e: + self.logger.info(f"Got an OSError when creating {i} event loops") + self.assertEqual(e.errno, 24) + self.assertEqual(e.strerror, "Too many open files") + upper_limit = i + break + self.assertTrue(upper_limit > 0) + finally: + for loop in loops: + loop.close() + + def test_web_client_never_generate_huge_number_of_event_loops(self): + num = 1000 + clients = [] + for i in range(num): + clients.append(WebClient(token="xoxb-test", run_async=False)) + self.assertEqual(len(clients), num) + + def test_rtm_client_never_generate_huge_number_of_event_loops(self): + num = 1000 + clients = [] + for i in range(num): + clients.append(RTMClient(token="xoxb-test", run_async=False)) + self.assertEqual(len(clients), num) diff --git a/tests/test_proxy_env_variable_loader.py b/tests/test_proxy_env_variable_loader.py new file mode 100644 index 000000000..3e9681f22 --- /dev/null +++ b/tests/test_proxy_env_variable_loader.py @@ -0,0 +1,40 @@ +import os +import unittest + +from slack_sdk.proxy_env_variable_loader import load_http_proxy_from_env +from tests.helpers import remove_os_env_temporarily, restore_os_env + + +class TestProxyEnvVariableLoader(unittest.TestCase): + def setUp(self): + self.old_env = remove_os_env_temporarily() + + def tearDown(self): + os.environ.clear() + restore_os_env(self.old_env) + + def test_load_lower_case(self): + os.environ["https_proxy"] = "http://localhost:9999" + url = load_http_proxy_from_env() + self.assertEqual(url, "http://localhost:9999") + + def test_load_upper_case(self): + os.environ["HTTPS_PROXY"] = "http://localhost:9999" + url = load_http_proxy_from_env() + self.assertEqual(url, "http://localhost:9999") + + def test_load_all_empty_case(self): + os.environ["HTTP_PROXY"] = "" + os.environ["http_proxy"] = "" + os.environ["HTTPS_PROXY"] = "" + os.environ["https_proxy"] = "" + url = load_http_proxy_from_env() + self.assertEqual(url, None) + + def test_proxy_url_is_none_case(self): + os.environ.pop("HTTPS_PROXY", None) + os.environ.pop("https_proxy", None) + os.environ.pop("HTTP_PROXY", None) + os.environ.pop("http_proxy", None) + url = load_http_proxy_from_env() + self.assertEqual(url, None) diff --git a/tests/web/classes/__init__.py b/tests/web/classes/__init__.py new file mode 100644 index 000000000..eea9b2100 --- /dev/null +++ b/tests/web/classes/__init__.py @@ -0,0 +1,46 @@ +STRING_51_CHARS = "SFOTYFUZTMDSOULXMKVFDOBQWNBAVGANMVLXQQZZQZQHBLJRZNY" +STRING_301_CHARS = ( + "ZFOMVKXETILJKBZPVKOYAUPNYWWWUICNEVXVPWNAMGCNHDBRMATGPMUHUZHUJKFWWLXBQXVDNCGJHAPKEK" + "DZCXKBXEHWCWBYDIGNYXTOFWWNLPBTVIGTNQKIQDHUAHZPWQDKKCHERBYKLAUOOKJXJJLGOPSCRVEHCOAD" + "BFYKJTXHMPPYWQVXCVGNNSXLNIHVKTVMEOIRXQDPLHIDZBAHUEDWXKXILEBOLILOYGZLNGCNXKWMFJWYYI" + "PIDUKJVGKTUERTPRMMMVZNAAOMZJFXFSEENCAMBOUJMYXTPHJEOPKDB" +) +STRING_3001_CHARS = ( + "UJSUOROQMIMCCCGFHQJVJXBCPWAOIMVOIIPFZGIZOBZWJHQLIABTGHXJMYVWYCFUIOWMVLJPJOHDVZRHUE" + "SVNQTHGXFKMGNBPRALVWQEYTFBKKKFUONDFRALDRZHKPGTWZAXOUFQJKOGTMYSFEDBEQQXIGKZMXNKDCEN" + "LSVHNGWVCIDMNSIZTBWBBVUMLPHRUCIZLZBFEGNFXZNJEZBUTNHNCYWWYSJSJDNOPPGHUPZLPJWDKEATZO" + "UGKZEGFTFBGZDNRITDFBDJLYDGETUHBDGFEELBJBDMSRBVFPXMRJXWULONCZRZZBNFOPARFNXPQONKEIKG" + "QDPJWCMGYSEIBAOLJNWPJVUSMJGCSQBLGZCWXJOYJHIZMNFMTLUQFGEBOONOZMGBWORFEUGYIUJAKLVAJZ" + "FTNOPOZNMUJPWRMGPKNQSBMZQRJXLRQJPYYUXLFUPICAFTXDTQIUOQRCSLWPHHUZAOPVTBRCXWUIXMFGYT" + "RBKPWJJXNQPLIAZAOKIMDWCDZABPLNOXYOZZBTHSDIPXXBKXKOSYYCITFSMNVIOCNGEMRKRBPCLBOCXBZQ" + "VVWKNJBPWQNJOJWAGAIBOBFRVDWLXVBLMBSXYLOAWMPLKJOVHABNNIFTKTKBIIBOSHYQZRUFPPPRDQPMUV" + "WMSWBLRUHKEMUFHIMZRUNNITKWYIWRXYPGFPXMNOABRWXGQFCWOYMMBYRQQLOIBFENIZBUIWLMDTIXCPXW" + "NNHBSRPSMCQIMYRCFCPLQQGVOHYZOUGFEXDTOETUKQAXOCNGYBYPYWDQHYOKPCCORGRNHXZAAYYZGSWWGS" + "CMJVCTAJUOMIMYRSVQGGPHCENXHLNFJJOEKIQWNYKBGKBMBJSFKKKYEPVXMOTAGFECZWQGVAEXHIAKTWYO" + "WFYMDMNNHWZGBHDEXYGRYQVXQXZJYAWLJLWUGQGPHAYJWJQWRQZBNAMNGEPVPPUMOFTOZNYLEXLWWUTABR" + "OLHPFFSWTZGYPAZJXRRPATWXKRDFQJRAEOBFNIWVZDKLNYXUFBOAWSDSKFYYRTADBBYHEWNZSTDXAAOQCD" + "WARSJZONQXRACMNBXZSEWZYBWADNDVRXBNJPJZQUNDYLBASCLCPFJWAMJUQAHBUZYDTIQPBPNJVVOHISZP" + "VGBDNXFIHYCABTSVNVILZUPPZXMPPZVBRTRHDGHTXXLBIYTMRDOUBYBVHVVKQAXAKISFJNUTRZKOCACJAX" + "ZXRRKMFOKYBHFUDBIXFAQSNUTYFNVQNGYWPJZGTLQUMOWXKKTUZGOUXAOVLQMMNKKECQCCOBNPPPXZYWZU" + "WHLHZQDIETDDPXWTILXGAYJKPHBXPLRFDPDSHFUPOIWRQDWQQNARPHPVKJPXZGGXOUVBYZSLUPVIJKWKNF" + "WMFKWYSYJJCCSCALMVPYIPHDKRXOWTUAYJFTAANCTVYDNSSIHGCWGKLDHFFBFSIFBMGHHFHZQSWOWZXOUW" + "PKNICGXPFMFIESHPDDMGSSWGBIAQVBANHLGDBYENRLSUARJXLQWPMOUSUKIIVXICBJPSWOEZPEUAJSLITV" + "XEQWSRENUJRJHPLBPFMBRPKGQNSYFWVLFLSQGGETKDUGYOLNFSMRVAZLQOAEKCUGNFEXRUDYSKBOQPYJAH" + "QHEIMSAAMTTYVJTHZDGQEITLERRYYQCTEQPTYQPHLMBDPCZZNNJYLGAGNXONCTIBSXEHXPYWBCTEEZLIYI" + "FMPYONXRVLSGZOEDZIMVDDPRXBKCKEPHOVLRBSPKMLZPXNRZVSSSYAOMGSVJODUZAJDYLGUZAFJMCOVGQX" + "ZUWQJENTEWQRFZYQTVEAHFQUWBUCFWHGRTMNQQFSPKKYYUBJVXKFQCCMBNGWNTRFGFKBFWTTPNDTGGWTAK" + "EOTXUPGFXOVWTOERFQSEZWVUYMGHVBQZIKIBJCNMKTZANNNOVMYTFLQYVNKTVZHFUJTPWNQWRYKGMYRYDC" + "WNTCUCYJCWXMMOJXUJSDWJKTTYOBFJFLBUCECGTVWKELCBDIKDUDOBLZLHYJQTVHXSUAFHDFDMETLHHEEJ" + "XJYWEOTXAUOZARSSQTBBXULKBBSTQHMJAAOUDIQCCETFWAINYIJCGXCILMDCAUYDMNZBDKIPVRCKCYKOIG" + "JHBLUHPOLDBWREFAZVEFFSOQQHMCXQYCQGMBHYKHJDBZXRAXLVZNYQXZEQYRSZHKKGCSOOEGNPFZDNGIMJ" + "QCXAEWWDYIGTQMJKBTMGSJAJCKIODCAEXVEGYCUBEEGCMARPJIKNAROJHYHKKTKGKKRVVSVYADCJXGSXAR" + "KGOUSUSZGJGFIKJDKJUIRQVSAHSTBCVOWZJDCCBWNNCBIYTCNOUPEYACCEWZNGETBTDJWQIEWRYIQXOZKP" + "ULDPCINLDFFPNORJHOZBSSYPPYNZTLXBRFZGBECKTTNVIHYNKGBXTTIXIKRBGVAPNWBPFNCGWQMZHBAHBX" + "MFEPSWVBUDLYDIVLZFHXTQJWUNWQHSWSCYFXQQSVORFQGUQIHUAJYFLBNBKJPOEIPYATRMNMGUTTVBOUHE" + "ZKXVAUEXCJYSCZEMGWTPXMQJEUWYHTFJQTBOQBEPQIPDYLBPIKKGPVYPOVLPPHYNGNWFTNQCDAATJVKRHC" + "OZGEBPFZZDPPZOWQCDFQZJAMXLVREYJQQFTQJKHMLRFJCVPVCTSVFVAGDVNXIGINSGHKGTWCKXNRZCZFVX" + "FPKZHPOMJTQOIVDIYKEVIIBAUHEDGOUNPCPMVLTZQLICXKKIYRJASBNDUZAONDDLQNVRXGWNQAOWSJSFWU" + "YWTTLOVXIJYERRZQCJMRZHCXEEAKYCLEICUWOJUXWHAPHQJDTBVRPVWTMCJRAUYCOTFXLLIQLOBASBMPED" + "KLDZDWDYAPXCKLZMEFIAOFYGFLBMURWVBFJDDEFXNIQOORYRMNROGVCOESSHSNIBNFRHPSWVAUQQVDMAHX" + "STDOVZMZEFRRFCKOLDOOFVOBCPRRLGYFJNXVPPUZONOSALUUI" +) diff --git a/tests/web/classes/test_actions.py b/tests/web/classes/test_actions.py new file mode 100644 index 000000000..ed14b840e --- /dev/null +++ b/tests/web/classes/test_actions.py @@ -0,0 +1,169 @@ +import unittest + +from slack.errors import SlackObjectFormationError +from slack.web.classes.actions import ( + ActionButton, + ActionChannelSelector, + ActionConversationSelector, + ActionExternalSelector, + ActionLinkButton, + ActionStaticSelector, + ActionUserSelector, +) +from slack.web.classes.objects import ConfirmObject, Option, OptionGroup +from tests.web.classes import STRING_3001_CHARS + + +class ButtonTests(unittest.TestCase): + def test_json(self): + self.assertDictEqual( + ActionButton(name="button_1", text="Click me!", value="btn_1").to_dict(), + { + "name": "button_1", + "text": "Click me!", + "value": "btn_1", + "type": "button", + }, + ) + + confirm = ConfirmObject(title="confirm_title", text="confirm_text") + self.assertDictEqual( + ActionButton( + name="button_1", + text="Click me!", + value="btn_1", + confirm=confirm, + style="danger", + ).to_dict(), + { + "name": "button_1", + "text": "Click me!", + "value": "btn_1", + "type": "button", + "confirm": confirm.to_dict("action"), + "style": "danger", + }, + ) + + def test_value_length(self): + with self.assertRaises(SlackObjectFormationError): + ActionButton(name="button_1", text="Click me!", value=STRING_3001_CHARS).to_dict() + + def test_style_validator(self): + b = ActionButton(name="button_1", text="Click me!", value="btn_1") + with self.assertRaises(SlackObjectFormationError): + b.style = "abcdefg" + b.to_dict() + + b.style = "primary" + b.to_dict() + + +class LinkButtonTests(unittest.TestCase): + def test_json(self): + self.assertDictEqual( + ActionLinkButton(text="Click me!", url="http://google.com").to_dict(), + {"url": "http://google.com", "text": "Click me!", "type": "button"}, + ) + + +class StaticActionSelectorTests(unittest.TestCase): + def setUp(self) -> None: + self.options = [ + Option.from_single_value("one"), + Option.from_single_value("two"), + Option.from_single_value("three"), + ] + + self.option_group = [OptionGroup(label="group_1", options=self.options)] + + def test_json(self): + self.assertDictEqual( + ActionStaticSelector(name="select_1", text="selector_1", options=self.options).to_dict(), + { + "name": "select_1", + "text": "selector_1", + "options": [o.to_dict("action") for o in self.options], + "type": "select", + "data_source": "static", + }, + ) + + self.assertDictEqual( + ActionStaticSelector(name="select_1", text="selector_1", options=self.option_group).to_dict(), + { + "name": "select_1", + "text": "selector_1", + "option_groups": [o.to_dict("action") for o in self.option_group], + "type": "select", + "data_source": "static", + }, + ) + + def test_options_length(self): + with self.assertRaises(SlackObjectFormationError): + ActionStaticSelector(name="select_1", text="selector_1", options=self.options * 34).to_dict() + + +class DynamicActionSelectorTests(unittest.TestCase): + selectors = {ActionUserSelector, ActionChannelSelector, ActionConversationSelector} + + def setUp(self) -> None: + self.selected_opt = Option.from_single_value("U12345") + + def test_json(self): + for component in self.selectors: + with self.subTest(msg=f"{component} json formation test"): + self.assertDictEqual( + component(name="select_1", text="selector_1").to_dict(), + { + "name": "select_1", + "text": "selector_1", + "type": "select", + "data_source": component.data_source, + }, + ) + + self.assertDictEqual( + component( + name="select_1", + text="selector_1", + # next line is a little silly, but so is writing the test + # three times + **{f"selected_{component.data_source[:-1]}": self.selected_opt}, + ).to_dict(), + { + "name": "select_1", + "text": "selector_1", + "type": "select", + "data_source": component.data_source, + "selected_options": [self.selected_opt.to_dict("action")], + }, + ) + + +class ExternalActionSelectorTests(unittest.TestCase): + def test_json(self): + option = Option.from_single_value("one") + + self.assertDictEqual( + ActionExternalSelector(name="select_1", text="selector_1", min_query_length=3).to_dict(), + { + "name": "select_1", + "text": "selector_1", + "min_query_length": 3, + "type": "select", + "data_source": "external", + }, + ) + + self.assertDictEqual( + ActionExternalSelector(name="select_1", text="selector_1", selected_option=option).to_dict(), + { + "name": "select_1", + "text": "selector_1", + "selected_options": [option.to_dict("action")], + "type": "select", + "data_source": "external", + }, + ) diff --git a/tests/web/classes/test_attachments.py b/tests/web/classes/test_attachments.py new file mode 100644 index 000000000..ede2900fe --- /dev/null +++ b/tests/web/classes/test_attachments.py @@ -0,0 +1,209 @@ +import unittest + +from slack.errors import SlackObjectFormationError +from slack.web.classes.actions import ActionButton, ActionLinkButton +from slack.web.classes.blocks import SectionBlock, ImageBlock +from slack.web.classes.attachments import ( + Attachment, + BlockAttachment, + AttachmentField, + InteractiveAttachment, +) +from tests.web.classes import STRING_301_CHARS + + +class FieldTests(unittest.TestCase): + def test_basic_json(self): + self.assertDictEqual( + AttachmentField(title="field", value="something", short=False).to_dict(), + {"title": "field", "value": "something", "short": False}, + ) + + +class AttachmentTests(unittest.TestCase): + def setUp(self) -> None: + self.simple = Attachment(text="some_text") + + def test_basic_json(self): + self.assertDictEqual(Attachment(text="some text").to_dict(), {"text": "some text", "fields": []}) + + self.assertDictEqual( + Attachment( + text="attachment text", + title="Attachment", + fallback="fallback_text", + pretext="some_pretext", + title_link="link in title", + fields=[AttachmentField(title=f"field_{i}_title", value=f"field_{i}_value") for i in range(5)], + color="#FFFF00", + author_name="John Doe", + author_link="http://johndoeisthebest.com", + author_icon="http://johndoeisthebest.com/avatar.jpg", + thumb_url="thumbnail URL", + footer="and a footer", + footer_icon="link to footer icon", + ts=123456789, + markdown_in=["fields"], + ).to_dict(), + { + "text": "attachment text", + "author_name": "John Doe", + "author_link": "http://johndoeisthebest.com", + "author_icon": "http://johndoeisthebest.com/avatar.jpg", + "footer": "and a footer", + "title": "Attachment", + "footer_icon": "link to footer icon", + "pretext": "some_pretext", + "ts": 123456789, + "fallback": "fallback_text", + "title_link": "link in title", + "color": "#FFFF00", + "thumb_url": "thumbnail URL", + "fields": [ + {"title": "field_0_title", "value": "field_0_value", "short": True}, + {"title": "field_1_title", "value": "field_1_value", "short": True}, + {"title": "field_2_title", "value": "field_2_value", "short": True}, + {"title": "field_3_title", "value": "field_3_value", "short": True}, + {"title": "field_4_title", "value": "field_4_value", "short": True}, + ], + "mrkdwn_in": ["fields"], + }, + ) + + def test_footer_length(self): + with self.assertRaises(SlackObjectFormationError): + self.simple.footer = STRING_301_CHARS + self.simple.to_dict() + + def test_ts_without_footer(self): + with self.assertRaises(SlackObjectFormationError): + self.simple.ts = 123456789 + self.simple.to_dict() + + def test_markdown_in_invalid(self): + with self.assertRaises(SlackObjectFormationError): + self.simple.markdown_in = ["nothing"] + self.simple.to_dict() + + def test_color_valid(self): + with self.assertRaises(SlackObjectFormationError): + self.simple.color = "red" + self.simple.to_dict() + + with self.assertRaises(SlackObjectFormationError): + self.simple.color = "#ZZZZZZ" + self.simple.to_dict() + + self.simple.color = "#bada55" + self.assertEqual(self.simple.to_dict()["color"], "#bada55") + + self.simple.color = "good" + self.assertEqual(self.simple.to_dict()["color"], "good") + + def test_image_url_and_thumb_url(self): + with self.assertRaises(SlackObjectFormationError): + self.simple.thumb_url = "some URL" + self.simple.image_url = "some URL" + self.simple.to_dict() + + self.simple.image_url = None + self.simple.to_dict() + + def author_name_without_author_link(self): + with self.assertRaises(SlackObjectFormationError): + self.simple.author_name = "http://google.com" + self.simple.to_dict() + + self.simple.author_name = None + self.simple.to_dict() + + def author_icon_without_author_name(self): + with self.assertRaises(SlackObjectFormationError): + self.simple.author_icon = "http://google.com/images.jpg" + self.simple.to_dict() + + self.simple.author_icon = None + self.simple.to_dict() + + +class InteractiveAttachmentTests(unittest.TestCase): + def test_basic_json(self): + actions = [ + ActionButton(name="button_1", text="Click me", value="button_value_1"), + ActionLinkButton(text="navigate", url="http://google.com"), + ] + self.assertDictEqual( + InteractiveAttachment(text="some text", callback_id="abc123", actions=actions).to_dict(), + { + "text": "some text", + "fields": [], + "callback_id": "abc123", + "actions": [a.to_dict() for a in actions], + }, + ) + + self.assertDictEqual( + InteractiveAttachment( + actions=actions, + callback_id="cb_123", + text="attachment text", + title="Attachment", + fallback="fallback_text", + pretext="some_pretext", + title_link="link in title", + fields=[AttachmentField(title=f"field_{i}_title", value=f"field_{i}_value") for i in range(5)], + color="#FFFF00", + author_name="John Doe", + author_link="http://johndoeisthebest.com", + author_icon="http://johndoeisthebest.com/avatar.jpg", + thumb_url="thumbnail URL", + footer="and a footer", + footer_icon="link to footer icon", + ts=123456789, + markdown_in=["fields"], + ).to_dict(), + { + "text": "attachment text", + "callback_id": "cb_123", + "actions": [a.to_dict() for a in actions], + "author_name": "John Doe", + "author_link": "http://johndoeisthebest.com", + "author_icon": "http://johndoeisthebest.com/avatar.jpg", + "footer": "and a footer", + "title": "Attachment", + "footer_icon": "link to footer icon", + "pretext": "some_pretext", + "ts": 123456789, + "fallback": "fallback_text", + "title_link": "link in title", + "color": "#FFFF00", + "thumb_url": "thumbnail URL", + "fields": [ + {"title": "field_0_title", "value": "field_0_value", "short": True}, + {"title": "field_1_title", "value": "field_1_value", "short": True}, + {"title": "field_2_title", "value": "field_2_value", "short": True}, + {"title": "field_3_title", "value": "field_3_value", "short": True}, + {"title": "field_4_title", "value": "field_4_value", "short": True}, + ], + "mrkdwn_in": ["fields"], + }, + ) + + def test_actions_length(self): + actions = [ActionButton(name="button_1", text="Click me", value="button_value_1")] * 6 + + with self.assertRaises(SlackObjectFormationError): + InteractiveAttachment(text="some text", callback_id="abc123", actions=actions).to_dict(), + + +class BlockAttachmentTests(unittest.TestCase): + def test_basic_json(self): + blocks = [ + SectionBlock(text="Some text"), + ImageBlock(image_url="image.jpg", alt_text="an image"), + ] + + self.assertDictEqual( + BlockAttachment(fallback="foo", blocks=blocks).to_dict(), + {"fallback": "foo", "blocks": [b.to_dict() for b in blocks]}, + ) diff --git a/tests/web/classes/test_blocks.py b/tests/web/classes/test_blocks.py new file mode 100644 index 000000000..4e9ce2fb2 --- /dev/null +++ b/tests/web/classes/test_blocks.py @@ -0,0 +1,674 @@ +import unittest +from typing import List + +from slack.errors import SlackObjectFormationError +from slack.web.classes.blocks import ( + ActionsBlock, + ContextBlock, + DividerBlock, + HeaderBlock, + ImageBlock, + SectionBlock, + InputBlock, + FileBlock, + Block, + CallBlock, +) +from slack.web.classes.elements import ButtonElement, ImageElement, LinkButtonElement +from slack.web.classes.objects import PlainTextObject, MarkdownTextObject +from . import STRING_3001_CHARS + + +# https://docs.slack.dev/reference/block-kit/blocks + + +class BlockTests(unittest.TestCase): + def test_parse(self): + input = { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "A message *with some bold text* and _some italicized text_.", + }, + "unexpected_field": "test", + "unexpected_fields": [1, 2, 3], + "unexpected_object": {"something": "wrong"}, + } + block = Block.parse(input) + self.assertIsNotNone(block) + + self.assertDictEqual( + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "A message *with some bold text* and _some italicized text_.", + }, + }, + block.to_dict(), + ) + + +# ---------------------------------------------- +# Section +# ---------------------------------------------- + + +class SectionBlockTests(unittest.TestCase): + maxDiff = None + + def test_document_1(self): + input = { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "A message *with some bold text* and _some italicized text_.", + }, + } + self.assertDictEqual(input, SectionBlock(**input).to_dict()) + self.assertDictEqual(input, Block.parse(input).to_dict()) + + def test_document_2(self): + input = { + "type": "section", + "text": { + "text": "A message *with some bold text* and _some italicized text_.", + "type": "mrkdwn", + }, + "fields": [ + {"type": "mrkdwn", "text": "High"}, + {"type": "plain_text", "emoji": True, "text": "String"}, + ], + } + self.assertDictEqual(input, SectionBlock(**input).to_dict()) + + def test_document_3(self): + input = { + "type": "section", + "text": { + "text": "*Sally* has requested you set the deadline for the Nano launch project", + "type": "mrkdwn", + }, + "accessory": { + "type": "datepicker", + "action_id": "datepicker123", + "initial_date": "1990-04-28", + "placeholder": {"type": "plain_text", "text": "Select a date"}, + }, + } + self.assertDictEqual(input, SectionBlock(**input).to_dict()) + + def test_parse(self): + input = { + "type": "section", + "text": { + "type": "plain_text", + "text": "This is a plain text section block.", + "emoji": True, + }, + } + self.assertDictEqual(input, SectionBlock(**input).to_dict()) + + def test_json(self): + self.assertDictEqual( + { + "text": {"text": "some text", "type": "mrkdwn"}, + "block_id": "a_block", + "type": "section", + }, + SectionBlock(text="some text", block_id="a_block").to_dict(), + ) + + self.assertDictEqual( + { + "text": {"text": "some text", "type": "mrkdwn"}, + "fields": [ + {"text": "field0", "type": "mrkdwn"}, + {"text": "field1", "type": "mrkdwn"}, + {"text": "field2", "type": "mrkdwn"}, + {"text": "field3", "type": "mrkdwn"}, + {"text": "field4", "type": "mrkdwn"}, + ], + "type": "section", + }, + SectionBlock(text="some text", fields=[f"field{i}" for i in range(5)]).to_dict(), + ) + + button = LinkButtonElement(text="Click me!", url="http://google.com") + self.assertDictEqual( + { + "type": "section", + "text": {"text": "some text", "type": "mrkdwn"}, + "accessory": button.to_dict(), + }, + SectionBlock(text="some text", accessory=button).to_dict(), + ) + + def test_text_or_fields_populated(self): + with self.assertRaises(SlackObjectFormationError): + SectionBlock().to_dict() + + def test_fields_length(self): + with self.assertRaises(SlackObjectFormationError): + SectionBlock(fields=[f"field{i}" for i in range(11)]).to_dict() + + def test_issue_628(self): + elem = SectionBlock(text="1234567890" * 300) + elem.to_dict() # no exception + with self.assertRaises(SlackObjectFormationError): + elem = SectionBlock(text="1234567890" * 300 + "a") + elem.to_dict() + + @classmethod + def build_slack_block(cls, msg1, msg2, data): + blocks = [ + { + "type": "section", + "text": {"type": "mrkdwn", "text": f"*{msg1}*:\n{msg2}"}, + }, + {"type": "section", "fields": []}, + ] + names = list(set(data.keys()) - set("user_comments")) + fields = [{"type": "mrkdwn", "text": f"*{name}*:\n{data[name]}"} for name in names] + blocks[1]["fields"] = fields + return blocks + + @classmethod + def build_slack_block_native(cls, msg1, msg2, data): + blocks: List[SectionBlock] = [ + SectionBlock(text=MarkdownTextObject.parse(f"*{msg1}*:\n{msg2}")), + SectionBlock(fields=[]), + ] + names: List[str] = list(set(data.keys()) - set("user_comments")) + fields = [MarkdownTextObject.parse(f"*{name}*:\n{data[name]}") for name in names] + blocks[1].fields = fields + return list(b.to_dict() for b in blocks) + + def test_issue_500(self): + data = { + "first": "1", + "second": "2", + "third": "3", + "user_comments": {"first", "other"}, + } + expected = self.build_slack_block("category", "tech", data) + actual = self.build_slack_block_native("category", "tech", data) + self.assertDictEqual({"blocks": expected}, {"blocks": actual}) + + +# ---------------------------------------------- +# Divider +# ---------------------------------------------- + + +class DividerBlockTests(unittest.TestCase): + def test_document(self): + input = {"type": "divider"} + self.assertDictEqual(input, DividerBlock(**input).to_dict()) + self.assertDictEqual(input, Block.parse(input).to_dict()) + + def test_json(self): + self.assertDictEqual({"type": "divider"}, DividerBlock().to_dict()) + self.assertDictEqual({"type": "divider"}, DividerBlock(**{"type": "divider"}).to_dict()) + + def test_json_with_block_id(self): + self.assertDictEqual( + {"type": "divider", "block_id": "foo"}, + DividerBlock(block_id="foo").to_dict(), + ) + self.assertDictEqual( + {"type": "divider", "block_id": "foo"}, + DividerBlock(**{"type": "divider", "block_id": "foo"}).to_dict(), + ) + + +# ---------------------------------------------- +# Image +# ---------------------------------------------- + + +class ImageBlockTests(unittest.TestCase): + def test_document(self): + input = { + "type": "image", + "title": { + "type": "plain_text", + "text": "Please enjoy this photo of a kitten", + }, + "block_id": "image4", + "image_url": "http://placekitten.com/500/500", + "alt_text": "An incredibly cute kitten.", + } + self.assertDictEqual(input, ImageBlock(**input).to_dict()) + self.assertDictEqual(input, Block.parse(input).to_dict()) + + def test_json(self): + self.assertDictEqual( + { + "image_url": "http://google.com", + "alt_text": "not really an image", + "type": "image", + }, + ImageBlock(image_url="http://google.com", alt_text="not really an image").to_dict(), + ) + + def test_image_url_length(self): + with self.assertRaises(SlackObjectFormationError): + ImageBlock(image_url=STRING_3001_CHARS, alt_text="text").to_dict() + + def test_alt_text_length(self): + with self.assertRaises(SlackObjectFormationError): + ImageBlock(image_url="http://google.com", alt_text=STRING_3001_CHARS).to_dict() + + def test_title_length(self): + with self.assertRaises(SlackObjectFormationError): + ImageBlock(image_url="http://google.com", alt_text="text", title=STRING_3001_CHARS).to_dict() + + +# ---------------------------------------------- +# Actions +# ---------------------------------------------- + + +class ActionsBlockTests(unittest.TestCase): + def test_document_1(self): + input = { + "type": "actions", + "block_id": "actions1", + "elements": [ + { + "type": "static_select", + "placeholder": { + "type": "plain_text", + "text": "Which witch is the witchiest witch?", + }, + "action_id": "select_2", + "options": [ + { + "text": {"type": "plain_text", "text": "Matilda"}, + "value": "matilda", + }, + { + "text": {"type": "plain_text", "text": "Glinda"}, + "value": "glinda", + }, + { + "text": {"type": "plain_text", "text": "Granny Weatherwax"}, + "value": "grannyWeatherwax", + }, + { + "text": {"type": "plain_text", "text": "Hermione"}, + "value": "hermione", + }, + ], + }, + { + "type": "button", + "text": {"type": "plain_text", "text": "Cancel"}, + "value": "cancel", + "action_id": "button_1", + }, + ], + } + self.assertDictEqual(input, ActionsBlock(**input).to_dict()) + self.assertDictEqual(input, Block.parse(input).to_dict()) + + def test_document_2(self): + input = { + "type": "actions", + "block_id": "actionblock789", + "elements": [ + { + "type": "datepicker", + "action_id": "datepicker123", + "initial_date": "1990-04-28", + "placeholder": {"type": "plain_text", "text": "Select a date"}, + }, + { + "type": "overflow", + "options": [ + { + "text": { + "type": "plain_text", + "text": "*this is plain_text text*", + }, + "value": "value-0", + }, + { + "text": { + "type": "plain_text", + "text": "*this is plain_text text*", + }, + "value": "value-1", + }, + { + "text": { + "type": "plain_text", + "text": "*this is plain_text text*", + }, + "value": "value-2", + }, + { + "text": { + "type": "plain_text", + "text": "*this is plain_text text*", + }, + "value": "value-3", + }, + { + "text": { + "type": "plain_text", + "text": "*this is plain_text text*", + }, + "value": "value-4", + }, + ], + "action_id": "overflow", + }, + { + "type": "button", + "text": {"type": "plain_text", "text": "Click Me"}, + "value": "click_me_123", + "action_id": "button", + }, + ], + } + self.assertDictEqual(input, ActionsBlock(**input).to_dict()) + + def test_json(self): + self.elements = [ + ButtonElement(text="Click me", action_id="reg_button", value="1"), + LinkButtonElement(text="URL Button", url="http://google.com"), + ] + self.dict_elements = [] + for e in self.elements: + self.dict_elements.append(e.to_dict()) + + self.assertDictEqual( + {"elements": self.dict_elements, "type": "actions"}, + ActionsBlock(elements=self.elements).to_dict(), + ) + with self.assertRaises(SlackObjectFormationError): + ActionsBlock(elements=self.elements * 13).to_dict() + + +# ---------------------------------------------- +# Context +# ---------------------------------------------- + + +class ContextBlockTests(unittest.TestCase): + def test_document(self): + input = { + "type": "context", + "elements": [ + { + "type": "image", + "image_url": "https://image.freepik.com/free-photo/red-drawing-pin_1156-445.jpg", + "alt_text": "images", + }, + {"type": "mrkdwn", "text": "Location: **Dogpatch**"}, + ], + } + self.assertDictEqual(input, ContextBlock(**input).to_dict()) + self.assertDictEqual(input, Block.parse(input).to_dict()) + + def test_basic_json(self): + self.elements = [ + ImageElement( + image_url="https://api.slack.com/img/blocks/bkb_template_images/palmtree.png", + alt_text="palmtree", + ), + PlainTextObject(text="Just text"), + ] + e = { + "elements": [ + { + "type": "image", + "image_url": "https://api.slack.com/img/blocks/bkb_template_images/palmtree.png", + "alt_text": "palmtree", + }, + {"type": "plain_text", "text": "Just text"}, + ], + "type": "context", + } + d = ContextBlock(elements=self.elements).to_dict() + self.assertDictEqual(e, d) + + with self.assertRaises(SlackObjectFormationError): + ContextBlock(elements=self.elements * 6).to_dict() + + +# ---------------------------------------------- +# Input +# ---------------------------------------------- + + +class InputBlockTests(unittest.TestCase): + def test_document(self): + blocks = [ + { + "type": "input", + "element": {"type": "plain_text_input"}, + "label": {"type": "plain_text", "text": "Label", "emoji": True}, + }, + { + "type": "input", + "element": {"type": "plain_text_input", "multiline": True}, + "label": {"type": "plain_text", "text": "Label", "emoji": True}, + }, + { + "type": "input", + "element": { + "type": "datepicker", + "initial_date": "1990-04-28", + "placeholder": { + "type": "plain_text", + "text": "Select a date", + "emoji": True, + }, + }, + "label": {"type": "plain_text", "text": "Label", "emoji": True}, + }, + { + "type": "input", + "element": { + "type": "channels_select", + "placeholder": { + "type": "plain_text", + "text": "Select a channel", + "emoji": True, + }, + }, + "label": {"type": "plain_text", "text": "Label", "emoji": True}, + }, + { + "type": "input", + "element": { + "type": "multi_users_select", + "placeholder": { + "type": "plain_text", + "text": "Select users", + "emoji": True, + }, + }, + "label": {"type": "plain_text", "text": "Label", "emoji": True}, + }, + { + "type": "input", + "element": { + "type": "checkboxes", + "options": [ + { + "text": { + "type": "plain_text", + "text": "*this is plain_text text*", + "emoji": True, + }, + "value": "value-0", + }, + { + "text": { + "type": "plain_text", + "text": "*this is plain_text text*", + "emoji": True, + }, + "value": "value-1", + }, + { + "text": { + "type": "plain_text", + "text": "*this is plain_text text*", + "emoji": True, + }, + "value": "value-2", + }, + ], + }, + "label": {"type": "plain_text", "text": "Label", "emoji": True}, + }, + { + "type": "input", + "element": { + "type": "plain_text_input", + }, + "label": { + "type": "plain_text", + "text": "Label", + "emoji": True, + }, + "hint": { + "type": "plain_text", + "text": "some hint", + "emoji": True, + }, + }, + { + "dispatch_action": True, + "type": "input", + "element": { + "type": "plain_text_input", + "action_id": "plain_text_input-action", + }, + "label": {"type": "plain_text", "text": "Label", "emoji": True}, + }, + ] + for input in blocks: + self.assertDictEqual(input, InputBlock(**input).to_dict()) + self.assertDictEqual(input, Block.parse(input).to_dict()) + + +# ---------------------------------------------- +# File +# ---------------------------------------------- + + +class FileBlockTests(unittest.TestCase): + def test_document(self): + input = { + "type": "file", + "external_id": "ABCD1", + "source": "remote", + } + self.assertDictEqual(input, FileBlock(**input).to_dict()) + self.assertDictEqual(input, Block.parse(input).to_dict()) + + +# ---------------------------------------------- +# Call +# ---------------------------------------------- + + +class CallBlockTests(unittest.TestCase): + def test_with_real_payload(self): + self.maxDiff = None + input = { + "type": "call", + "call_id": "R00000000", + "api_decoration_available": False, + "call": { + "v1": { + "id": "R00000000", + "app_id": "A00000000", + "app_icon_urls": { + "image_32": "https://www.example.com/", + "image_36": "https://www.example.com/", + "image_48": "https://www.example.com/", + "image_64": "https://www.example.com/", + "image_72": "https://www.example.com/", + "image_96": "https://www.example.com/", + "image_128": "https://www.example.com/", + "image_192": "https://www.example.com/", + "image_512": "https://www.example.com/", + "image_1024": "https://www.example.com/", + "image_original": "https://www.example.com/", + }, + "date_start": 12345, + "active_participants": [ + {"slack_id": "U00000000"}, + { + "slack_id": "U00000000", + "external_id": "", + "avatar_url": "https://www.example.com/", + "display_name": "", + }, + ], + "all_participants": [ + {"slack_id": "U00000000"}, + { + "slack_id": "U00000000", + "external_id": "", + "avatar_url": "https://www.example.com/", + "display_name": "", + }, + ], + "display_id": "", + "join_url": "https://www.example.com/", + "name": "", + "created_by": "U00000000", + "date_end": 12345, + "channels": ["C00000000"], + "is_dm_call": False, + "was_rejected": False, + "was_missed": False, + "was_accepted": False, + "has_ended": False, + "desktop_app_join_url": "https://www.example.com/", + } + }, + } + self.assertDictEqual(input, CallBlock(**input).to_dict()) + self.assertDictEqual(input, Block.parse(input).to_dict()) + + +# ---------------------------------------------- +# Header +# ---------------------------------------------- + + +class HeaderBlockTests(unittest.TestCase): + def test_document(self): + input = { + "type": "header", + "block_id": "budget-header", + "text": {"type": "plain_text", "text": "Budget Performance"}, + } + self.assertDictEqual(input, HeaderBlock(**input).to_dict()) + self.assertDictEqual(input, Block.parse(input).to_dict()) + + def test_text_length_150(self): + input = { + "type": "header", + "block_id": "budget-header", + "text": {"type": "plain_text", "text": "1234567890" * 15}, + } + HeaderBlock(**input).validate_json() + + def test_text_length_151(self): + input = { + "type": "header", + "block_id": "budget-header", + "text": {"type": "plain_text", "text": ("1234567890" * 15) + "1"}, + } + with self.assertRaises(SlackObjectFormationError): + HeaderBlock(**input).validate_json() diff --git a/tests/web/classes/test_dialogs.py b/tests/web/classes/test_dialogs.py new file mode 100644 index 000000000..2f40a71bd --- /dev/null +++ b/tests/web/classes/test_dialogs.py @@ -0,0 +1,327 @@ +import unittest +from copy import copy + +from slack.errors import SlackObjectFormationError +from slack.web.classes.dialog_elements import ( + DialogChannelSelector, + DialogConversationSelector, + DialogExternalSelector, + DialogStaticSelector, + DialogTextArea, + DialogTextField, + DialogUserSelector, +) + +from slack.web.classes.dialogs import DialogBuilder +from slack.web.classes.objects import Option +from . import STRING_3001_CHARS, STRING_301_CHARS, STRING_51_CHARS + +TextComponents = {DialogTextField, DialogTextArea} + + +class CommonTextComponentTests(unittest.TestCase): + def test_json_validators(self): + for component in TextComponents: + with self.subTest(f"Component: {component}"): + with self.assertRaises(SlackObjectFormationError, msg="name length"): + component(name=STRING_301_CHARS, label="label ").to_dict() + + with self.assertRaises(SlackObjectFormationError, msg="label length"): + component(name="dialog", label=STRING_51_CHARS).to_dict() + + with self.assertRaises(SlackObjectFormationError, msg="placeholder length"): + component(name="dialog", label="Dialog", placeholder=STRING_301_CHARS).to_dict() + + with self.assertRaises(SlackObjectFormationError, msg="hint length"): + component(name="dialog", label="Dialog", hint=STRING_301_CHARS).to_dict() + + with self.assertRaises(SlackObjectFormationError, msg="value length"): + component(name="dialog", label="Dialog", value=STRING_3001_CHARS).to_dict() + + with self.assertRaises(SlackObjectFormationError, msg="min_length out of bounds"): + component( + name="dialog", + label="Dialog", + min_length=component.max_value_length + 1, + ).to_dict() + + with self.assertRaises(SlackObjectFormationError, msg="max_length out of bounds"): + component( + name="dialog", + label="Dialog", + max_length=component.max_value_length + 1, + ).to_dict() + + with self.assertRaises(SlackObjectFormationError, msg="min_length > max length"): + component(name="dialog", label="Dialog", min_length=100, max_length=50).to_dict() + + with self.assertRaises(SlackObjectFormationError, msg="subtype invalid"): + component(name="dialog", label="Dialog", subtype="abcdefg").to_dict() + + +class TextFieldComponentTests(unittest.TestCase): + def test_json(self): + self.assertDictEqual( + DialogTextField(name="dialog", label="Dialog").to_dict(), + { + "name": "dialog", + "label": "Dialog", + "min_length": 0, + "max_length": 150, + "optional": False, + "type": "text", + }, + ) + + def test_basic_json(self): + self.assertDictEqual( + DialogTextField( + name="dialog", + label="Dialog", + optional=True, + hint="Some hint", + max_length=100, + min_length=20, + ).to_dict(), + { + "min_length": 20, + "max_length": 100, + "name": "dialog", + "optional": True, + "label": "Dialog", + "type": "text", + "hint": "Some hint", + }, + ) + + +class TextAreaComponentTests(unittest.TestCase): + def test_basic_json_formation(self): + self.assertDictEqual( + DialogTextArea(name="dialog", label="Dialog").to_dict(), + { + "min_length": 0, + "max_length": 3000, + "name": "dialog", + "optional": False, + "label": "Dialog", + "type": "textarea", + }, + ) + + def test_complex_json_formation(self): + self.assertDictEqual( + DialogTextArea( + name="dialog", + label="Dialog", + optional=True, + hint="Some hint", + max_length=500, + min_length=100, + ).to_dict(), + { + "min_length": 100, + "max_length": 500, + "name": "dialog", + "optional": True, + "label": "Dialog", + "type": "textarea", + "hint": "Some hint", + }, + ) + + +class StaticDropdownTests(unittest.TestCase): + def test_basic_json_formation(self): + options = [ + Option.from_single_value("one"), + Option.from_single_value("two"), + Option.from_single_value("three"), + ] + self.assertDictEqual( + DialogStaticSelector(name="dialog", label="Dialog", options=options).to_dict(), + { + "optional": False, + "label": "Dialog", + "type": "select", + "name": "dialog", + "options": [ + {"label": "one", "value": "one"}, + {"label": "two", "value": "two"}, + {"label": "three", "value": "three"}, + ], + "data_source": "static", + }, + ) + + +class DynamicSelectorTests(unittest.TestCase): + selectors = {DialogUserSelector, DialogChannelSelector, DialogConversationSelector} + + def setUp(self) -> None: + self.selected_opt = Option.from_single_value("U12345") + + def test_json(self): + self.maxDiff = None + for component in self.selectors: + with self.subTest(msg=f"{component} json formation test"): + self.assertDictEqual( + component(name="select_1", label="selector_1").to_dict(), + { + "name": "select_1", + "label": "selector_1", + "type": "select", + "optional": False, + "data_source": component.data_source, + }, + ) + + passing_obj = component(name="select_1", label="selector_1", value=self.selected_opt).to_dict() + + passing_str = component(name="select_1", label="selector_1", value="U12345").to_dict() + + expected = { + "name": "select_1", + "label": "selector_1", + "type": "select", + "optional": False, + "data_source": component.data_source, + "value": "U12345", + } + self.assertDictEqual(passing_obj, expected) + self.assertDictEqual(passing_str, expected) + + +class ExternalSelectorTests(unittest.TestCase): + def test_basic_json_formation(self): + o = Option.from_single_value("one") + self.assertDictEqual( + DialogExternalSelector( + name="dialog", + label="Dialog", + value=o, + min_query_length=3, + optional=True, + placeholder="something", + ).to_dict(), + { + "optional": True, + "label": "Dialog", + "type": "select", + "name": "dialog", + "min_query_length": 3, + "placeholder": "something", + "selected_options": [o.to_dict("dialog")], + "data_source": "external", + }, + ) + + +class DialogBuilderTests(unittest.TestCase): + def setUp(self) -> None: + self.builder = ( + DialogBuilder() + .title("Dialog Title") + .callback_id("function_123") + .submit_label("SubmitDialog") + .notify_on_cancel(True) + .text_field( + name="signature", + label="Signature", + optional=True, + hint="Enter your signature", + ) + .text_area(name="message", label="Message", hint="Enter message to broadcast") + .conversation_selector(name="target", label="Choose Target") + ) + + def test_basic_methods(self): + self.assertEqual(self.builder._title, "Dialog Title") + self.assertEqual(self.builder._callback_id, "function_123") + self.assertEqual(self.builder._submit_label, "SubmitDialog") + self.assertTrue(self.builder._notify_on_cancel) + + def test_element_appending(self): + text_field, text_area, dropdown = self.builder._elements + + self.assertEqual(text_field.type, "text") + self.assertEqual(text_field.name, "signature") + self.assertEqual(text_field.label, "Signature") + self.assertTrue(text_field.optional) + self.assertEqual(text_field.hint, "Enter your signature") + + self.assertEqual(text_area.type, "textarea") + self.assertEqual(text_area.name, "message") + self.assertEqual(text_area.label, "Message") + self.assertEqual(text_area.hint, "Enter message to broadcast") + + self.assertEqual(dropdown.type, "select") + self.assertEqual(dropdown.name, "target") + self.assertEqual(dropdown.label, "Choose Target") + self.assertEqual(dropdown.data_source, "conversations") + + def test_build_without_errors(self): + valid = { + "title": "Dialog Title", + "callback_id": "function_123", + "elements": [ + { + "hint": "Enter your signature", + "min_length": 0, + "label": "Signature", + "name": "signature", + "optional": True, + "max_length": 150, + "type": "text", + }, + { + "hint": "Enter message to broadcast", + "min_length": 0, + "label": "Message", + "name": "message", + "optional": False, + "max_length": 3000, + "type": "textarea", + }, + { + "type": "select", + "label": "Choose Target", + "name": "target", + "optional": False, + "data_source": "conversations", + }, + ], + "notify_on_cancel": True, + "submit_label": "SubmitDialog", + } + + self.assertDictEqual(self.builder.to_dict(), valid) + + def test_build_validation(self): + empty_title = copy(self.builder) + # noinspection PyTypeChecker + empty_title.title(None) + with self.assertRaises(SlackObjectFormationError): + empty_title.to_dict() + + too_long_title = copy(self.builder) + too_long_title.title(STRING_51_CHARS) + with self.assertRaises(SlackObjectFormationError): + too_long_title.to_dict() + + empty_callback = copy(self.builder) + # noinspection PyTypeChecker + empty_callback.callback_id(None) + with self.assertRaises(SlackObjectFormationError): + empty_callback.to_dict() + + empty_dialog = copy(self.builder) + empty_dialog._elements = [] + with self.assertRaises(SlackObjectFormationError): + empty_dialog.to_dict() + + overfull_dialog = copy(self.builder) + for i in range(8): + overfull_dialog.text_field(name=f"element {i}", label="overflow") + with self.assertRaises(SlackObjectFormationError): + overfull_dialog.to_dict() diff --git a/tests/web/classes/test_elements.py b/tests/web/classes/test_elements.py new file mode 100644 index 000000000..215ec8aac --- /dev/null +++ b/tests/web/classes/test_elements.py @@ -0,0 +1,705 @@ +import unittest + +from slack.errors import SlackObjectFormationError +from slack.web.classes.elements import ( + ButtonElement, + DatePickerElement, + ExternalDataSelectElement, + ImageElement, + LinkButtonElement, + UserSelectElement, + StaticSelectElement, + CheckboxesElement, + StaticMultiSelectElement, + ExternalDataMultiSelectElement, + UserMultiSelectElement, + ConversationMultiSelectElement, + ChannelMultiSelectElement, + OverflowMenuElement, + PlainTextInputElement, + RadioButtonsElement, + ConversationSelectElement, + ChannelSelectElement, +) +from slack.web.classes.objects import ConfirmObject, Option +from . import STRING_3001_CHARS, STRING_301_CHARS + + +# ------------------------------------------------- +# Interactive Elements +# ------------------------------------------------- + + +class InteractiveElementTests(unittest.TestCase): + def test_action_id(self): + with self.assertRaises(SlackObjectFormationError): + ButtonElement(text="click me!", action_id=STRING_301_CHARS, value="clickable button").to_dict() + + +class ButtonElementTests(unittest.TestCase): + def test_document_1(self): + input = { + "type": "button", + "text": {"type": "plain_text", "text": "Click Me"}, + "value": "click_me_123", + "action_id": "button", + } + self.assertDictEqual(input, ButtonElement(**input).to_dict()) + + def test_document_2(self): + input = { + "type": "button", + "text": {"type": "plain_text", "text": "Save"}, + "style": "primary", + "value": "click_me_123", + "action_id": "button", + } + self.assertDictEqual(input, ButtonElement(**input).to_dict()) + + def test_document_3(self): + input = { + "type": "button", + "text": {"type": "plain_text", "text": "Link Button"}, + "url": "https://docs.slack.dev/block-kit/", + } + self.assertDictEqual(input, ButtonElement(**input).to_dict()) + self.assertDictEqual(input, LinkButtonElement(**input).to_dict()) + + def test_json(self): + self.assertDictEqual( + { + "text": {"emoji": True, "text": "button text", "type": "plain_text"}, + "action_id": "some_button", + "value": "button_123", + "type": "button", + }, + ButtonElement(text="button text", action_id="some_button", value="button_123").to_dict(), + ) + + confirm = ConfirmObject(title="really?", text="are you sure?") + self.assertDictEqual( + { + "text": {"emoji": True, "text": "button text", "type": "plain_text"}, + "action_id": "some_button", + "value": "button_123", + "type": "button", + "style": "primary", + "confirm": confirm.to_dict(), + }, + ButtonElement( + text="button text", + action_id="some_button", + value="button_123", + style="primary", + confirm=confirm, + ).to_dict(), + ) + + def test_text_length(self): + with self.assertRaises(SlackObjectFormationError): + ButtonElement(text=STRING_301_CHARS, action_id="button", value="click_me").to_dict() + + def test_value_length(self): + with self.assertRaises(SlackObjectFormationError): + ButtonElement(text="Button", action_id="button", value=STRING_3001_CHARS).to_dict() + + def test_invalid_style(self): + with self.assertRaises(SlackObjectFormationError): + ButtonElement(text="Button", action_id="button", value="button", style="invalid").to_dict() + + +class LinkButtonElementTests(unittest.TestCase): + def test_json(self): + button = LinkButtonElement(action_id="test", text="button text", url="http://google.com") + self.assertDictEqual( + { + "text": {"emoji": True, "text": "button text", "type": "plain_text"}, + "url": "http://google.com", + "type": "button", + "action_id": button.action_id, + }, + button.to_dict(), + ) + + def test_url_length(self): + with self.assertRaises(SlackObjectFormationError): + LinkButtonElement(text="Button", url=STRING_3001_CHARS).to_dict() + + +# ------------------------------------------------- +# Checkboxes +# ------------------------------------------------- + + +class CheckboxesElementTests(unittest.TestCase): + def test_document(self): + input = { + "type": "checkboxes", + "action_id": "this_is_an_action_id", + "initial_options": [{"value": "A1", "text": {"type": "plain_text", "text": "Checkbox 1"}}], + "options": [ + {"value": "A1", "text": {"type": "plain_text", "text": "Checkbox 1"}}, + {"value": "A2", "text": {"type": "plain_text", "text": "Checkbox 2"}}, + ], + } + self.assertDictEqual(input, CheckboxesElement(**input).to_dict()) + + +# ------------------------------------------------- +# DatePicker +# ------------------------------------------------- + + +class DatePickerElementTests(unittest.TestCase): + def test_document(self): + input = { + "type": "datepicker", + "action_id": "datepicker123", + "initial_date": "1990-04-28", + "placeholder": {"type": "plain_text", "text": "Select a date"}, + "confirm": { + "title": {"type": "plain_text", "text": "Are you sure?"}, + "text": { + "type": "mrkdwn", + "text": "Wouldn't you prefer a good game of _chess_?", + }, + "confirm": {"type": "plain_text", "text": "Do it"}, + "deny": {"type": "plain_text", "text": "Stop, I've changed my mind!"}, + }, + } + self.assertDictEqual(input, DatePickerElement(**input).to_dict()) + + def test_json(self): + for month in range(1, 12): + for day in range(1, 31): + date = f"2020-{month:02}-{day:02}" + self.assertDictEqual( + { + "action_id": "datepicker-action", + "initial_date": date, + "placeholder": { + "emoji": True, + "text": "Select a date", + "type": "plain_text", + }, + "type": "datepicker", + }, + DatePickerElement( + action_id="datepicker-action", + placeholder="Select a date", + initial_date=date, + ).to_dict(), + ) + + def test_issue_623(self): + elem = DatePickerElement(action_id="1", placeholder=None) + elem.to_dict() # no exception + elem = DatePickerElement(action_id="1") + elem.to_dict() # no exception + with self.assertRaises(SlackObjectFormationError): + elem = DatePickerElement(action_id="1", placeholder="12345" * 100) + elem.to_dict() + + +# ------------------------------------------------- +# Image +# ------------------------------------------------- + + +class ImageElementTests(unittest.TestCase): + def test_document(self): + input = { + "type": "image", + "image_url": "http://placekitten.com/700/500", + "alt_text": "Multiple cute kittens", + } + self.assertDictEqual(input, ImageElement(**input).to_dict()) + + def test_json(self): + self.assertDictEqual( + { + "image_url": "http://google.com", + "alt_text": "not really an image", + "type": "image", + }, + ImageElement(image_url="http://google.com", alt_text="not really an image").to_dict(), + ) + + def test_image_url_length(self): + with self.assertRaises(SlackObjectFormationError): + ImageElement(image_url=STRING_3001_CHARS, alt_text="text").to_dict() + + def test_alt_text_length(self): + with self.assertRaises(SlackObjectFormationError): + ImageElement(image_url="http://google.com", alt_text=STRING_3001_CHARS).to_dict() + + +# ------------------------------------------------- +# Static Select +# ------------------------------------------------- + + +class StaticMultiSelectElementTests(unittest.TestCase): + maxDiff = None + + def test_document(self): + input = { + "action_id": "text1234", + "type": "multi_static_select", + "placeholder": {"type": "plain_text", "text": "Select items"}, + "options": [ + { + "text": {"type": "plain_text", "text": "*this is plain_text text*"}, + "value": "value-0", + }, + { + "text": {"type": "plain_text", "text": "*this is plain_text text*"}, + "value": "value-1", + }, + { + "text": {"type": "plain_text", "text": "*this is plain_text text*"}, + "value": "value-2", + }, + ], + "max_selected_items": 1, + } + self.assertDictEqual(input, StaticMultiSelectElement(**input).to_dict()) + + +class StaticSelectElementTests(unittest.TestCase): + maxDiff = None + + def test_document_options(self): + input = { + "action_id": "text1234", + "type": "static_select", + "placeholder": {"type": "plain_text", "text": "Select an item"}, + "options": [ + { + "text": {"type": "plain_text", "text": "*this is plain_text text*"}, + "value": "value-0", + }, + { + "text": {"type": "plain_text", "text": "*this is plain_text text*"}, + "value": "value-1", + }, + { + "text": {"type": "plain_text", "text": "*this is plain_text text*"}, + "value": "value-2", + }, + ], + } + self.assertDictEqual(input, StaticSelectElement(**input).to_dict()) + + def test_document_option_groups(self): + input = { + "action_id": "text1234", + "type": "static_select", + "placeholder": {"type": "plain_text", "text": "Select an item"}, + "option_groups": [ + { + "label": {"type": "plain_text", "text": "Group 1"}, + "options": [ + { + "text": { + "type": "plain_text", + "text": "*this is plain_text text*", + }, + "value": "value-0", + }, + { + "text": { + "type": "plain_text", + "text": "*this is plain_text text*", + }, + "value": "value-1", + }, + { + "text": { + "type": "plain_text", + "text": "*this is plain_text text*", + }, + "value": "value-2", + }, + ], + }, + { + "label": {"type": "plain_text", "text": "Group 2"}, + "options": [ + { + "text": { + "type": "plain_text", + "text": "*this is plain_text text*", + }, + "value": "value-3", + } + ], + }, + ], + } + self.assertDictEqual(input, StaticSelectElement(**input).to_dict()) + + option_one = Option.from_single_value("one") + option_two = Option.from_single_value("two") + options = [option_one, option_two, Option.from_single_value("three")] + + def test_json(self): + dict_options = [] + for o in self.options: + dict_options.append(o.to_dict()) + + self.assertDictEqual( + { + "placeholder": { + "emoji": True, + "text": "selectedValue", + "type": "plain_text", + }, + "action_id": "dropdown", + "options": dict_options, + "initial_option": self.option_two.to_dict(), + "type": "static_select", + }, + StaticSelectElement( + placeholder="selectedValue", + action_id="dropdown", + options=self.options, + initial_option=self.option_two, + ).to_dict(), + ) + + self.assertDictEqual( + { + "placeholder": { + "emoji": True, + "text": "selectedValue", + "type": "plain_text", + }, + "action_id": "dropdown", + "options": dict_options, + "confirm": ConfirmObject(title="title", text="text").to_dict("block"), + "type": "static_select", + }, + StaticSelectElement( + placeholder="selectedValue", + action_id="dropdown", + options=self.options, + confirm=ConfirmObject(title="title", text="text"), + ).to_dict(), + ) + + def test_options_length(self): + with self.assertRaises(SlackObjectFormationError): + StaticSelectElement( + placeholder="select", + action_id="selector", + options=[self.option_one] * 101, + ).to_dict() + + +# ------------------------------------------------- +# External Data Source Select +# ------------------------------------------------- + + +class ExternalDataMultiSelectElementTests(unittest.TestCase): + maxDiff = None + + def test_document(self): + input = { + "action_id": "text1234", + "type": "multi_external_select", + "placeholder": {"type": "plain_text", "text": "Select items"}, + "min_query_length": 3, + } + self.assertDictEqual(input, ExternalDataMultiSelectElement(**input).to_dict()) + + def test_document_initial_options(self): + input = { + "action_id": "text1234", + "type": "multi_external_select", + "placeholder": {"type": "plain_text", "text": "Select items"}, + "initial_options": [ + { + "text": {"type": "plain_text", "text": "The default channel"}, + "value": "C1234567890", + } + ], + "min_query_length": 0, + "max_selected_items": 1, + } + self.assertDictEqual(input, ExternalDataMultiSelectElement(**input).to_dict()) + + +class ExternalDataSelectElementTests(unittest.TestCase): + maxDiff = None + + def test_document_1(self): + input = { + "action_id": "text1234", + "type": "external_select", + "placeholder": {"type": "plain_text", "text": "Select an item"}, + "min_query_length": 3, + } + self.assertDictEqual(input, ExternalDataSelectElement(**input).to_dict()) + + def test_document_2(self): + input = { + "action_id": "text1234", + "type": "external_select", + "placeholder": {"type": "plain_text", "text": "Select an item"}, + "initial_option": { + "text": {"type": "plain_text", "text": "The default channel"}, + "value": "C1234567890", + }, + "confirm": { + "title": {"type": "plain_text", "text": "Are you sure?"}, + "text": { + "type": "mrkdwn", + "text": "Wouldn't you prefer a good game of _chess_?", + }, + "confirm": {"type": "plain_text", "text": "Do it"}, + "deny": {"type": "plain_text", "text": "Stop, I've changed my mind!"}, + }, + "min_query_length": 3, + } + self.assertDictEqual(input, ExternalDataSelectElement(**input).to_dict()) + + def test_json(self): + self.assertDictEqual( + { + "placeholder": { + "emoji": True, + "text": "selectedValue", + "type": "plain_text", + }, + "action_id": "dropdown", + "min_query_length": 5, + "type": "external_select", + }, + ExternalDataSelectElement(placeholder="selectedValue", action_id="dropdown", min_query_length=5).to_dict(), + ) + self.assertDictEqual( + { + "placeholder": { + "emoji": True, + "text": "selectedValue", + "type": "plain_text", + }, + "action_id": "dropdown", + "confirm": ConfirmObject(title="title", text="text").to_dict("block"), + "type": "external_select", + }, + ExternalDataSelectElement( + placeholder="selectedValue", + action_id="dropdown", + confirm=ConfirmObject(title="title", text="text"), + ).to_dict(), + ) + + +# ------------------------------------------------- +# Users Select +# ------------------------------------------------- + + +class UserSelectMultiElementTests(unittest.TestCase): + def test_document(self): + input = { + "action_id": "text1234", + "type": "multi_users_select", + "placeholder": {"type": "plain_text", "text": "Select users"}, + "initial_users": ["U123", "U234"], + "max_selected_items": 1, + } + self.assertDictEqual(input, UserMultiSelectElement(**input).to_dict()) + + +class UserSelectElementTests(unittest.TestCase): + def test_document(self): + input = { + "action_id": "text1234", + "type": "users_select", + "placeholder": {"type": "plain_text", "text": "Select an item"}, + "initial_user": "U123", + } + self.assertDictEqual(input, UserSelectElement(**input).to_dict()) + + def test_json(self): + self.assertDictEqual( + { + "action_id": "a-123", + "type": "users_select", + "initial_user": "U123", + "placeholder": { + "type": "plain_text", + "text": "abc", + "emoji": True, + }, + }, + UserSelectElement( + placeholder="abc", + action_id="a-123", + initial_user="U123", + ).to_dict(), + ) + + +# ------------------------------------------------- +# Conversations Select +# ------------------------------------------------- + + +class ConversationSelectMultiElementTests(unittest.TestCase): + def test_document(self): + input = { + "action_id": "text1234", + "type": "multi_conversations_select", + "placeholder": {"type": "plain_text", "text": "Select conversations"}, + "initial_conversations": ["C123", "C234"], + "max_selected_items": 2, + "default_to_current_conversation": True, + "filter": {"include": ["public", "mpim"], "exclude_bot_users": True}, + } + self.assertDictEqual(input, ConversationMultiSelectElement(**input).to_dict()) + + +class ConversationSelectElementTests(unittest.TestCase): + def test_document(self): + input = { + "action_id": "text1234", + "type": "conversations_select", + "placeholder": {"type": "plain_text", "text": "Select an item"}, + "initial_conversation": "C123", + "response_url_enabled": True, + "default_to_current_conversation": True, + "filter": {"include": ["public", "mpim"], "exclude_bot_users": True}, + } + self.assertDictEqual(input, ConversationSelectElement(**input).to_dict()) + + +# ------------------------------------------------- +# Channels Select +# ------------------------------------------------- + + +class ChannelSelectMultiElementTests(unittest.TestCase): + def test_document(self): + input = { + "action_id": "text1234", + "type": "multi_channels_select", + "placeholder": {"type": "plain_text", "text": "Select channels"}, + "initial_channels": ["C123", "C234"], + "max_selected_items": 2, + } + self.assertDictEqual(input, ChannelMultiSelectElement(**input).to_dict()) + + +class ChannelSelectElementTests(unittest.TestCase): + def test_document(self): + input = { + "action_id": "text1234", + "type": "channels_select", + "placeholder": {"type": "plain_text", "text": "Select an item"}, + "response_url_enabled": True, + "initial_channel": "C123", + } + self.assertDictEqual(input, ChannelSelectElement(**input).to_dict()) + + +# ------------------------------------------------- +# Overflow Menu Select +# ------------------------------------------------- + + +class OverflowMenuElementTests(unittest.TestCase): + def test_document(self): + input = { + "type": "overflow", + "options": [ + { + "text": {"type": "plain_text", "text": "*this is plain_text text*"}, + "value": "value-0", + }, + { + "text": {"type": "plain_text", "text": "*this is plain_text text*"}, + "value": "value-1", + }, + { + "text": {"type": "plain_text", "text": "*this is plain_text text*"}, + "value": "value-2", + }, + { + "text": {"type": "plain_text", "text": "*this is plain_text text*"}, + "value": "value-3", + }, + { + "text": {"type": "plain_text", "text": "*this is plain_text text*"}, + # https://docs.slack.dev/reference/block-kit/composition-objects/option-object + "url": "https://www.example.com", + }, + ], + "action_id": "overflow", + } + self.assertDictEqual(input, OverflowMenuElement(**input).to_dict()) + + +# ------------------------------------------------- +# Input +# ------------------------------------------------- + + +class PlainTextInputElementTests(unittest.TestCase): + def test_document_1(self): + input = { + "type": "plain_text_input", + "action_id": "plain_input", + "placeholder": {"type": "plain_text", "text": "Enter some plain text"}, + } + self.assertDictEqual(input, PlainTextInputElement(**input).to_dict()) + + def test_document_2(self): + input = { + "type": "plain_text_input", + "action_id": "plain_input", + "placeholder": {"type": "plain_text", "text": "Enter some plain text"}, + "initial_value": "TODO", + "multiline": True, + "min_length": 1, + "max_length": 10, + } + self.assertDictEqual(input, PlainTextInputElement(**input).to_dict()) + + def test_document_3(self): + input = { + "type": "plain_text_input", + "multiline": True, + "dispatch_action_config": {"trigger_actions_on": ["on_character_entered"]}, + } + self.assertDictEqual(input, PlainTextInputElement(**input).to_dict()) + + +# ------------------------------------------------- +# Radio Buttons +# ------------------------------------------------- + + +class RadioButtonsElementTest(unittest.TestCase): + def test_document(self): + input = { + "type": "radio_buttons", + "action_id": "this_is_an_action_id", + "initial_option": { + "value": "A1", + "text": {"type": "plain_text", "text": "Radio 1"}, + }, + "options": [ + {"value": "A1", "text": {"type": "plain_text", "text": "Radio 1"}}, + {"value": "A2", "text": {"type": "plain_text", "text": "Radio 2"}}, + ], + "initial_option": { + "value": "A2", + "text": {"type": "plain_text", "text": "Radio 2"}, + }, + } + self.assertDictEqual(input, RadioButtonsElement(**input).to_dict()) diff --git a/tests/web/classes/test_init.py b/tests/web/classes/test_init.py new file mode 100644 index 000000000..0b95677e1 --- /dev/null +++ b/tests/web/classes/test_init.py @@ -0,0 +1,26 @@ +import unittest + +from slack.web.classes import extract_json +from slack.web.classes.objects import PlainTextObject, MarkdownTextObject + + +class TestInit(unittest.TestCase): + def test_from_list_of_json_objects(self): + json_objects = [ + PlainTextObject.from_str("foo"), + MarkdownTextObject.from_str("bar"), + ] + output = extract_json(json_objects) + expected = { + "result": [ + {"type": "plain_text", "text": "foo", "emoji": True}, + {"type": "mrkdwn", "text": "bar"}, + ] + } + self.assertDictEqual(expected, {"result": output}) + + def test_from_single_json_object(self): + single_json_object = PlainTextObject.from_str("foo") + output = extract_json(single_json_object) + expected = {"result": {"type": "plain_text", "text": "foo", "emoji": True}} + self.assertDictEqual(expected, {"result": output}) diff --git a/tests/web/classes/test_messages.py b/tests/web/classes/test_messages.py new file mode 100644 index 000000000..df07cbf0b --- /dev/null +++ b/tests/web/classes/test_messages.py @@ -0,0 +1,9 @@ +import unittest + +from slack_sdk.models.messages.message import Message + + +class MessageTests(unittest.TestCase): + def test_validate_json_fails(self): + msg = Message(text="Hi there") + self.assertIsNotNone(msg) diff --git a/tests/web/classes/test_objects.py b/tests/web/classes/test_objects.py new file mode 100644 index 000000000..1fd986c69 --- /dev/null +++ b/tests/web/classes/test_objects.py @@ -0,0 +1,499 @@ +import copy +import unittest +from typing import Optional, List, Union + +from slack.errors import SlackObjectFormationError +from slack.web.classes import JsonObject, JsonValidator +from slack.web.classes.objects import ( + ChannelLink, + ConfirmObject, + DateLink, + EveryoneLink, + HereLink, + Link, + MarkdownTextObject, + ObjectLink, + Option, + OptionGroup, + PlainTextObject, +) +from . import STRING_301_CHARS, STRING_51_CHARS + + +class SimpleJsonObject(JsonObject): + attributes = {"some", "test", "keys"} + + def __init__(self): + self.some = "this is" + self.test = "a test" + self.keys = "object" + + @JsonValidator("some validation message") + def test_valid(self): + return len(self.test) <= 10 + + @JsonValidator("this should never fail") + def always_valid_test(self): + return True + + +class KeyValueObject(JsonObject): + attributes = {"name", "value"} + + def __init__( + self, + *, + name: Optional[str] = None, + value: Optional[str] = None, + ): + self.name = name + self.value = value + + +class NestedObject(JsonObject): + attributes = {"initial", "options"} + + def __init__( + self, + *, + initial: Union[dict, KeyValueObject], + options: List[Union[dict, KeyValueObject]], + ): + self.initial = KeyValueObject(**initial) if isinstance(initial, dict) else initial + self.options = [KeyValueObject(**o) if isinstance(o, dict) else o for o in options] + + +class JsonObjectTests(unittest.TestCase): + def setUp(self) -> None: + self.good_test_object = SimpleJsonObject() + obj = SimpleJsonObject() + obj.test = STRING_51_CHARS + self.bad_test_object = obj + + def test_json_formation(self): + self.assertDictEqual( + self.good_test_object.to_dict(), + {"some": "this is", "test": "a test", "keys": "object"}, + ) + + def test_validate_json_fails(self): + with self.assertRaises(SlackObjectFormationError): + self.bad_test_object.validate_json() + + def test_to_dict_performs_validation(self): + with self.assertRaises(SlackObjectFormationError): + self.bad_test_object.to_dict() + + def test_get_non_null_attributes(self): + expected = {"name": "something"} + obj = KeyValueObject(name="something", value=None) + obj2 = copy.deepcopy(obj) + self.assertDictEqual(expected, obj.get_non_null_attributes()) + self.assertEqual(str(obj2), str(obj)) + + def test_get_non_null_attributes_nested(self): + expected = { + "initial": {"name": "something"}, + "options": [ + {"name": "something"}, + {"name": "message", "value": "That's great!"}, + ], + } + obj1 = KeyValueObject(name="something", value=None) + obj2 = KeyValueObject(name="message", value="That's great!") + options = [obj1, obj2] + nested = NestedObject(initial=obj1, options=options) + + self.assertEqual(type(obj1), KeyValueObject) + self.assertTrue(hasattr(obj1, "value")) + self.assertEqual(type(nested.initial), KeyValueObject) + + self.assertEqual(type(options[0]), KeyValueObject) + self.assertTrue(hasattr(options[0], "value")) + self.assertEqual(type(nested.options[0]), KeyValueObject) + self.assertTrue(hasattr(nested.options[0], "value")) + + dict_value = nested.get_non_null_attributes() + self.assertDictEqual(expected, dict_value) + + self.assertEqual(type(obj1), KeyValueObject) + self.assertTrue(hasattr(obj1, "value")) + self.assertEqual(type(nested.initial), KeyValueObject) + + self.assertEqual(type(options[0]), KeyValueObject) + self.assertTrue(hasattr(options[0], "value")) + self.assertEqual(type(nested.options[0]), KeyValueObject) + self.assertTrue(hasattr(nested.options[0], "value")) + + def test_get_non_null_attributes_nested_2(self): + expected = { + "initial": {"name": "something"}, + "options": [ + {"name": "something"}, + {"name": "message", "value": "That's great!"}, + ], + } + nested = NestedObject( + initial={"name": "something"}, + options=[ + {"name": "something"}, + {"name": "message", "value": "That's great!"}, + ], + ) + self.assertDictEqual(expected, nested.get_non_null_attributes()) + + +class JsonValidatorTests(unittest.TestCase): + def setUp(self) -> None: + self.validator_instance = JsonValidator("message") + self.class_instance = SimpleJsonObject() + + def test_isolated_class(self): + def does_nothing(): + return False + + wrapped = self.validator_instance(does_nothing) + + # noinspection PyUnresolvedReferences + self.assertTrue(wrapped.validator) + + def test_wrapped_class(self): + for attribute in dir(self.class_instance): + attr = getattr(self.class_instance, attribute, None) + if attribute in ("test_valid", "always_valid_test"): + self.assertTrue(attr.validator) + else: + with self.assertRaises(AttributeError): + # noinspection PyStatementEffect + attr.validator + + +class LinkTests(unittest.TestCase): + def test_without_text(self): + link = Link(url="http://google.com", text="") + self.assertEqual(f"{link}", "") + + def test_with_text(self): + link = Link(url="http://google.com", text="google") + self.assertEqual(f"{link}", "") + + +class DateLinkTests(unittest.TestCase): + def setUp(self) -> None: + self.epoch = 1234567890 + + def test_simple_formation(self): + datelink = DateLink(date=self.epoch, date_format="{date_long}", fallback=f"{self.epoch}") + self.assertEqual(f"{datelink}", f"") + + def test_with_url(self): + datelink = DateLink( + date=self.epoch, + date_format="{date_long}", + link="http://google.com", + fallback=f"{self.epoch}", + ) + self.assertEqual( + f"{datelink}", + f"", + ) + + +class ObjectLinkTests(unittest.TestCase): + def test_channel(self): + objlink = ObjectLink(object_id="C12345") + self.assertEqual(f"{objlink}", "<#C12345>") + + def test_group_message(self): + objlink = ObjectLink(object_id="G12345") + self.assertEqual(f"{objlink}", "<#G12345>") + + def test_subteam_message(self): + objlink = ObjectLink(object_id="S12345") + self.assertEqual(f"{objlink}", "") + + def test_with_label(self): + objlink = ObjectLink(object_id="C12345", text="abc") + self.assertEqual(f"{objlink}", "<#C12345|abc>") + + def test_unknown_prefix(self): + objlink = ObjectLink(object_id="Z12345") + self.assertEqual(f"{objlink}", "<@Z12345>") + + +class SpecialLinkTests(unittest.TestCase): + def test_channel_link(self): + self.assertEqual(f"{ChannelLink()}", "") + + def test_here_link(self): + self.assertEqual(f"{HereLink()}", "") + + def test_everyone_link(self): + self.assertEqual(f"{EveryoneLink()}", "") + + +class PlainTextObjectTests(unittest.TestCase): + def test_basic_json(self): + self.assertDictEqual( + {"text": "some text", "type": "plain_text"}, + PlainTextObject(text="some text").to_dict(), + ) + + self.assertDictEqual( + {"text": "some text", "emoji": False, "type": "plain_text"}, + PlainTextObject(text="some text", emoji=False).to_dict(), + ) + + def test_from_string(self): + plaintext = PlainTextObject(text="some text", emoji=True) + self.assertDictEqual(plaintext.to_dict(), PlainTextObject.direct_from_string("some text")) + + +class MarkdownTextObjectTests(unittest.TestCase): + def test_basic_json(self): + self.assertDictEqual( + {"text": "some text", "type": "mrkdwn"}, + MarkdownTextObject(text="some text").to_dict(), + ) + + self.assertDictEqual( + {"text": "some text", "verbatim": True, "type": "mrkdwn"}, + MarkdownTextObject(text="some text", verbatim=True).to_dict(), + ) + + def test_from_string(self): + markdown = MarkdownTextObject(text="some text") + self.assertDictEqual(markdown.to_dict(), MarkdownTextObject.direct_from_string("some text")) + + +class ConfirmObjectTests(unittest.TestCase): + def test_basic_json(self): + expected = { + "confirm": {"emoji": True, "text": "Yes", "type": "plain_text"}, + "deny": {"emoji": True, "text": "No", "type": "plain_text"}, + "text": {"text": "are you sure?", "type": "mrkdwn"}, + "title": {"emoji": True, "text": "some title", "type": "plain_text"}, + } + simple_object = ConfirmObject(title="some title", text="are you sure?") + self.assertDictEqual(expected, simple_object.to_dict()) + self.assertDictEqual(expected, simple_object.to_dict("block")) + self.assertDictEqual( + { + "text": "are you sure?", + "title": "some title", + "ok_text": "Okay", + "dismiss_text": "Cancel", + }, + simple_object.to_dict("action"), + ) + + def test_confirm_overrides(self): + confirm = ConfirmObject( + title="some title", + text="are you sure?", + confirm="I'm really sure", + deny="Nevermind", + ) + expected = { + "confirm": {"text": "I'm really sure", "type": "plain_text", "emoji": True}, + "deny": {"text": "Nevermind", "type": "plain_text", "emoji": True}, + "text": {"text": "are you sure?", "type": "mrkdwn"}, + "title": {"text": "some title", "type": "plain_text", "emoji": True}, + } + self.assertDictEqual(expected, confirm.to_dict()) + self.assertDictEqual(expected, confirm.to_dict("block")) + self.assertDictEqual( + { + "text": "are you sure?", + "title": "some title", + "ok_text": "I'm really sure", + "dismiss_text": "Nevermind", + }, + confirm.to_dict("action"), + ) + + def test_passing_text_objects(self): + direct_construction = ConfirmObject(title="title", text="Are you sure?") + + mrkdwn = MarkdownTextObject(text="Are you sure?") + + preconstructed = ConfirmObject(title="title", text=mrkdwn) + + self.assertDictEqual(direct_construction.to_dict(), preconstructed.to_dict()) + + plaintext = PlainTextObject(text="Are you sure?", emoji=False) + + passed_plaintext = ConfirmObject(title="title", text=plaintext) + + self.assertDictEqual( + { + "confirm": {"emoji": True, "text": "Yes", "type": "plain_text"}, + "deny": {"emoji": True, "text": "No", "type": "plain_text"}, + "text": {"emoji": False, "text": "Are you sure?", "type": "plain_text"}, + "title": {"emoji": True, "text": "title", "type": "plain_text"}, + }, + passed_plaintext.to_dict(), + ) + + def test_title_length(self): + with self.assertRaises(SlackObjectFormationError): + ConfirmObject(title=STRING_301_CHARS, text="Are you sure?").to_dict() + + def test_text_length(self): + with self.assertRaises(SlackObjectFormationError): + ConfirmObject(title="title", text=STRING_301_CHARS).to_dict() + + def test_text_length_with_object(self): + with self.assertRaises(SlackObjectFormationError): + plaintext = PlainTextObject(text=STRING_301_CHARS) + ConfirmObject(title="title", text=plaintext).to_dict() + + with self.assertRaises(SlackObjectFormationError): + markdown = MarkdownTextObject(text=STRING_301_CHARS) + ConfirmObject(title="title", text=markdown).to_dict() + + def test_confirm_length(self): + with self.assertRaises(SlackObjectFormationError): + ConfirmObject(title="title", text="Are you sure?", confirm=STRING_51_CHARS).to_dict() + + def test_deny_length(self): + with self.assertRaises(SlackObjectFormationError): + ConfirmObject(title="title", text="Are you sure?", deny=STRING_51_CHARS).to_dict() + + +class OptionTests(unittest.TestCase): + def setUp(self) -> None: + self.common = Option(label="an option", value="option_1") + + def test_block_style_json(self): + expected = { + "text": {"type": "plain_text", "text": "an option", "emoji": True}, + "value": "option_1", + } + self.assertDictEqual(expected, self.common.to_dict("block")) + self.assertDictEqual(expected, self.common.to_dict()) + + def test_dialog_style_json(self): + expected = {"label": "an option", "value": "option_1"} + self.assertDictEqual(expected, self.common.to_dict("dialog")) + + def test_action_style_json(self): + expected = {"text": "an option", "value": "option_1"} + self.assertDictEqual(expected, self.common.to_dict("action")) + + def test_from_single_value(self): + option = Option(label="option_1", value="option_1") + self.assertDictEqual( + option.to_dict("text"), + option.from_single_value("option_1").to_dict("text"), + ) + + def test_label_length(self): + with self.assertRaises(SlackObjectFormationError): + Option(label=STRING_301_CHARS, value="option_1").to_dict("text") + + def test_value_length(self): + with self.assertRaises(SlackObjectFormationError): + Option(label="option_1", value=STRING_301_CHARS).to_dict("text") + + +class OptionGroupTests(unittest.TestCase): + maxDiff = None + + def setUp(self) -> None: + self.common_options = [ + Option.from_single_value("one"), + Option.from_single_value("two"), + Option.from_single_value("three"), + ] + + self.common = OptionGroup(label="an option", options=self.common_options) + + def test_block_style_json(self): + expected = { + "label": {"emoji": True, "text": "an option", "type": "plain_text"}, + "options": [ + { + "text": {"emoji": True, "text": "one", "type": "plain_text"}, + "value": "one", + }, + { + "text": {"emoji": True, "text": "two", "type": "plain_text"}, + "value": "two", + }, + { + "text": {"emoji": True, "text": "three", "type": "plain_text"}, + "value": "three", + }, + ], + } + self.assertDictEqual(expected, self.common.to_dict("block")) + self.assertDictEqual(expected, self.common.to_dict()) + + def test_dialog_style_json(self): + self.assertDictEqual( + { + "label": "an option", + "options": [ + {"label": "one", "value": "one"}, + {"label": "two", "value": "two"}, + {"label": "three", "value": "three"}, + ], + }, + self.common.to_dict("dialog"), + ) + + def test_action_style_json(self): + self.assertDictEqual( + { + "text": "an option", + "options": [ + {"text": "one", "value": "one"}, + {"text": "two", "value": "two"}, + {"text": "three", "value": "three"}, + ], + }, + self.common.to_dict("action"), + ) + + def test_label_length(self): + with self.assertRaises(SlackObjectFormationError): + OptionGroup(label=STRING_301_CHARS, options=self.common_options).to_dict("text") + + def test_options_length(self): + with self.assertRaises(SlackObjectFormationError): + OptionGroup(label="option_group", options=self.common_options * 34).to_dict("text") + + def test_confirm_style(self): + obj = ConfirmObject.parse( + { + "title": {"type": "plain_text", "text": "Are you sure?"}, + "text": { + "type": "mrkdwn", + "text": "Wouldn't you prefer a good game of _chess_?", + }, + "confirm": {"type": "plain_text", "text": "Do it"}, + "deny": {"type": "plain_text", "text": "Stop, I've changed my mind!"}, + "style": "primary", + } + ) + obj.validate_json() + self.assertEqual("primary", obj.style) + + def test_confirm_style_validation(self): + with self.assertRaises(SlackObjectFormationError): + ConfirmObject.parse( + { + "title": {"type": "plain_text", "text": "Are you sure?"}, + "text": { + "type": "mrkdwn", + "text": "Wouldn't you prefer a good game of _chess_?", + }, + "confirm": {"type": "plain_text", "text": "Do it"}, + "deny": { + "type": "plain_text", + "text": "Stop, I've changed my mind!", + }, + "style": "something-wrong", + } + ).validate_json() diff --git a/tests/web/classes/test_views.py b/tests/web/classes/test_views.py new file mode 100644 index 000000000..dd08426ff --- /dev/null +++ b/tests/web/classes/test_views.py @@ -0,0 +1,447 @@ +import json +import logging +import unittest + +from slack.errors import SlackObjectFormationError +from slack.web.classes.blocks import ( + InputBlock, + SectionBlock, + DividerBlock, + ActionsBlock, + ContextBlock, +) +from slack.web.classes.elements import ( + PlainTextInputElement, + RadioButtonsElement, + CheckboxesElement, + ButtonElement, + ImageElement, +) +from slack.web.classes.objects import PlainTextObject, Option, MarkdownTextObject +from slack.web.classes.views import View, ViewState, ViewStateValue + + +class ViewTests(unittest.TestCase): + maxDiff = None + + def setUp(self) -> None: + self.logger = logging.getLogger(__name__) + + def verify_loaded_view_object(self, file): + input = json.load(file) + view = View(**input) + self.assertDictEqual(input, view.to_dict()) + + # -------------------------------- + # Modals + # -------------------------------- + + def test_valid_construction(self): + modal_view = View( + type="modal", + callback_id="modal-id", + title=PlainTextObject(text="Awesome Modal"), + submit=PlainTextObject(text="Submit"), + close=PlainTextObject(text="Cancel"), + blocks=[ + InputBlock( + block_id="b-id", + label=PlainTextObject(text="Input label"), + element=PlainTextInputElement(action_id="a-id"), + ), + InputBlock( + block_id="cb-id", + label=PlainTextObject(text="Label"), + element=CheckboxesElement( + action_id="a-cb-id", + options=[ + Option( + text=PlainTextObject(text="*this is plain_text text*"), + value="v1", + ), + Option( + text=MarkdownTextObject(text="*this is mrkdwn text*"), + value="v2", + ), + ], + ), + ), + SectionBlock( + block_id="sb-id", + text=MarkdownTextObject(text="This is a mrkdwn text section block."), + fields=[ + PlainTextObject(text="*this is plain_text text*", emoji=True), + MarkdownTextObject(text="*this is mrkdwn text*"), + PlainTextObject(text="*this is plain_text text*", emoji=True), + ], + ), + DividerBlock(), + SectionBlock( + block_id="rb-id", + text=MarkdownTextObject(text="This is a section block with radio button accessory"), + accessory=RadioButtonsElement( + initial_option=Option( + text=PlainTextObject(text="Option 1"), + value="option 1", + description=PlainTextObject(text="Description for option 1"), + ), + options=[ + Option( + text=PlainTextObject(text="Option 1"), + value="option 1", + description=PlainTextObject(text="Description for option 1"), + ), + Option( + text=PlainTextObject(text="Option 2"), + value="option 2", + description=PlainTextObject(text="Description for option 2"), + ), + ], + ), + ), + ], + ) + modal_view.validate_json() + + def test_invalid_type_value(self): + modal_view = View( + type="modallll", + callback_id="modal-id", + title=PlainTextObject(text="Awesome Modal"), + submit=PlainTextObject(text="Submit"), + close=PlainTextObject(text="Cancel"), + blocks=[ + InputBlock( + block_id="b-id", + label=PlainTextObject(text="Input label"), + element=PlainTextInputElement(action_id="a-id"), + ), + ], + ) + with self.assertRaises(SlackObjectFormationError): + modal_view.validate_json() + + def test_simple_state_values(self): + expected = { + "values": { + "b1": {"a1": {"type": "plain_text_input", "value": "Title"}}, + "b2": {"a2": {"type": "plain_text_input", "value": "Description"}}, + } + } + state = ViewState( + values={ + "b1": {"a1": ViewStateValue(type="plain_text_input", value="Title")}, + "b2": {"a2": {"type": "plain_text_input", "value": "Description"}}, + } + ) + self.assertDictEqual(expected, ViewState(**expected).to_dict()) + self.assertDictEqual(expected, state.to_dict()) + + def test_all_state_values(self): + # Testing with + # {"type":"modal","title":{"type":"plain_text","text":"My App","emoji":true},"submit":{"type":"plain_text","text":"Submit","emoji":true},"close":{"type":"plain_text","text":"Cancel","emoji":true},"blocks":[{"type":"input","element":{"type":"plain_text_input"},"label":{"type":"plain_text","text":"Label","emoji":true}},{"type":"input","element":{"type":"plain_text_input","multiline":true},"label":{"type":"plain_text","text":"Label","emoji":true}},{"type":"input","element":{"type":"datepicker","initial_date":"1990-04-28","placeholder":{"type":"plain_text","text":"Select a date","emoji":true}},"label":{"type":"plain_text","text":"Label","emoji":true}},{"type":"input","element":{"type":"users_select","placeholder":{"type":"plain_text","text":"Select a user","emoji":true}},"label":{"type":"plain_text","text":"Label","emoji":true}},{"type":"input","element":{"type":"multi_static_select","placeholder":{"type":"plain_text","text":"Select options","emoji":true},"options":[{"text":{"type":"plain_text","text":"*this is plain_text text*","emoji":true},"value":"value-0"},{"text":{"type":"plain_text","text":"*this is plain_text text*","emoji":true},"value":"value-1"},{"text":{"type":"plain_text","text":"*this is plain_text text*","emoji":true},"value":"value-2"}]},"label":{"type":"plain_text","text":"Label","emoji":true}},{"type":"input","element":{"type":"checkboxes","options":[{"text":{"type":"plain_text","text":"*this is plain_text text*","emoji":true},"value":"value-0"},{"text":{"type":"plain_text","text":"*this is plain_text text*","emoji":true},"value":"value-1"},{"text":{"type":"plain_text","text":"*this is plain_text text*","emoji":true},"value":"value-2"}]},"label":{"type":"plain_text","text":"Label","emoji":true}},{"type":"input","element":{"type":"radio_buttons","initial_option":{"text":{"type":"plain_text","text":"Option 1"},"value":"option 1","description":{"type":"plain_text","text":"Description for option 1"}},"options":[{"text":{"type":"plain_text","text":"Option 1"},"value":"option 1","description":{"type":"plain_text","text":"Description for option 1"}},{"text":{"type":"plain_text","text":"Option 2"},"value":"option 2","description":{"type":"plain_text","text":"Description for option 2"}},{"text":{"type":"plain_text","text":"Option 3"},"value":"option 3","description":{"type":"plain_text","text":"Description for option 3"}}]},"label":{"type":"plain_text","text":"Label","emoji":true}}]} + expected = { + "values": { + "b1": {"a1": {"type": "datepicker", "selected_date": "1990-04-12"}}, + "b2": {"a2": {"type": "plain_text_input", "value": "This is a test"}}, + # multiline + "b3": { + "a3": { + "type": "plain_text_input", + "value": "Something wrong\nPlease help me!", + } + }, + "b4": {"a4": {"type": "users_select", "selected_user": "U123"}}, + "b4-2": { + "a4-2": { + "type": "multi_users_select", + "selected_users": ["U123", "U234"], + } + }, + "b5": { + "a5": { + "type": "conversations_select", + "selected_conversation": "C123", + } + }, + "b5-2": { + "a5-2": { + "type": "multi_conversations_select", + "selected_conversations": ["C123", "C234"], + } + }, + "b6": {"a6": {"type": "channels_select", "selected_channel": "C123"}}, + "b6-2": { + "a6-2": { + "type": "multi_channels_select", + "selected_channels": ["C123", "C234"], + } + }, + "b7": { + "a7": { + "type": "multi_static_select", + "selected_options": [ + { + "text": { + "type": "plain_text", + "text": "*this is plain_text text*", + "emoji": True, + }, + "value": "value-0", + }, + { + "text": { + "type": "plain_text", + "text": "*this is plain_text text*", + "emoji": True, + }, + "value": "value-1", + }, + ], + } + }, + "b8": { + "a8": { + "type": "checkboxes", + "selected_options": [ + { + "text": { + "type": "plain_text", + "text": "*this is plain_text text*", + "emoji": True, + }, + "value": "value-0", + }, + { + "text": { + "type": "plain_text", + "text": "*this is plain_text text*", + "emoji": True, + }, + "value": "value-1", + }, + ], + } + }, + "b9": { + "a9": { + "type": "radio_buttons", + "selected_option": { + "text": { + "type": "plain_text", + "text": "Option 1", + "emoji": True, + }, + "value": "option 1", + "description": { + "type": "plain_text", + "text": "Description for option 1", + "emoji": True, + }, + }, + } + }, + } + } + state = ViewState( + values={ + "b1": {"a1": ViewStateValue(type="datepicker", selected_date="1990-04-12")}, + "b2": {"a2": ViewStateValue(type="plain_text_input", value="This is a test")}, + "b3": { + "a3": ViewStateValue( + type="plain_text_input", + value="Something wrong\nPlease help me!", + ) + }, + "b4": {"a4": ViewStateValue(type="users_select", selected_user="U123")}, + "b4-2": {"a4-2": ViewStateValue(type="multi_users_select", selected_users=["U123", "U234"])}, + "b5": {"a5": ViewStateValue(type="conversations_select", selected_conversation="C123")}, + "b5-2": { + "a5-2": ViewStateValue( + type="multi_conversations_select", + selected_conversations=["C123", "C234"], + ) + }, + "b6": {"a6": ViewStateValue(type="channels_select", selected_channel="C123")}, + "b6-2": {"a6-2": ViewStateValue(type="multi_channels_select", selected_channels=["C123", "C234"])}, + "b7": { + "a7": ViewStateValue( + type="multi_static_select", + selected_options=[ + Option( + text=PlainTextObject(text="*this is plain_text text*", emoji=True), + value="value-0", + ), + Option( + text=PlainTextObject(text="*this is plain_text text*", emoji=True), + value="value-1", + ), + ], + ) + }, + "b8": { + "a8": ViewStateValue( + type="checkboxes", + selected_options=[ + Option( + text=PlainTextObject(text="*this is plain_text text*", emoji=True), + value="value-0", + ), + Option( + text=PlainTextObject(text="*this is plain_text text*", emoji=True), + value="value-1", + ), + ], + ) + }, + "b9": { + "a9": ViewStateValue( + type="radio_buttons", + selected_option=Option( + text=PlainTextObject(text="Option 1", emoji=True), + value="option 1", + description=PlainTextObject(text="Description for option 1", emoji=True), + ), + ) + }, + } + ) + self.assertDictEqual(expected, ViewState(**expected).to_dict()) + self.assertDictEqual(expected, state.to_dict()) + + def test_load_modal_view_001(self): + with open("tests/data/view_modal_001.json") as file: + self.verify_loaded_view_object(file) + + def test_load_modal_view_002(self): + with open("tests/data/view_modal_002.json") as file: + self.verify_loaded_view_object(file) + + def test_load_modal_view_003(self): + with open("tests/data/view_modal_003.json") as file: + self.verify_loaded_view_object(file) + + def test_load_modal_view_004(self): + with open("tests/data/view_modal_004.json") as file: + self.verify_loaded_view_object(file) + + def test_load_modal_view_005(self): + with open("tests/data/view_modal_005.json") as file: + self.verify_loaded_view_object(file) + + def test_load_modal_view_006(self): + with open("tests/data/view_modal_006.json") as file: + self.verify_loaded_view_object(file) + + def test_load_modal_view_007(self): + with open("tests/data/view_modal_007.json") as file: + self.verify_loaded_view_object(file) + + def test_load_modal_view_008(self): + with open("tests/data/view_modal_008.json") as file: + self.verify_loaded_view_object(file) + + def test_load_modal_view_009(self): + with open("tests/data/view_modal_009.json") as file: + self.verify_loaded_view_object(file) + + def test_load_modal_view_010(self): + with open("tests/data/view_modal_010.json") as file: + self.verify_loaded_view_object(file) + + # -------------------------------- + # Home Tabs + # -------------------------------- + + def test_home_tab_construction(self): + home_tab_view = View( + type="home", + blocks=[ + SectionBlock( + text=MarkdownTextObject(text="*Here's what you can do with Project Tracker:*"), + ), + ActionsBlock( + elements=[ + ButtonElement( + text=PlainTextObject(text="Create New Task", emoji=True), + style="primary", + value="create_task", + ), + ButtonElement( + text=PlainTextObject(text="Create New Project", emoji=True), + value="create_project", + ), + ButtonElement( + text=PlainTextObject(text="Help", emoji=True), + value="help", + ), + ], + ), + ContextBlock( + elements=[ + ImageElement( + image_url="https://api.slack.com/img/blocks/bkb_template_images/placeholder.png", + alt_text="placeholder", + ), + ], + ), + SectionBlock( + text=MarkdownTextObject(text="*Your Configurations*"), + ), + DividerBlock(), + SectionBlock( + text=MarkdownTextObject( + text="*#public-relations*\n posts new tasks, comments, and project updates to " + ), + accessory=ButtonElement( + text=PlainTextObject(text="Edit", emoji=True), + value="public-relations", + ), + ), + ], + ) + home_tab_view.validate_json() + + def test_submit_in_home_tab(self): + modal_view = View( + type="home", + callback_id="home-tab-id", + submit=PlainTextObject(text="Submit"), + blocks=[DividerBlock()], + ) + with self.assertRaises(SlackObjectFormationError): + modal_view.validate_json() + + def test_close_in_home_tab(self): + modal_view = View( + type="home", + callback_id="home-tab-id", + close=PlainTextObject(text="Cancel"), + blocks=[DividerBlock()], + ) + with self.assertRaises(SlackObjectFormationError): + modal_view.validate_json() + + def test_load_home_tab_view_001(self): + with open("tests/data/view_home_001.json") as file: + self.verify_loaded_view_object(file) + + def test_load_home_tab_view_002(self): + with open("tests/data/view_home_002.json") as file: + self.verify_loaded_view_object(file) + + def test_load_home_tab_view_003(self): + with open("tests/data/view_home_003.json") as file: + self.verify_loaded_view_object(file) + + def test_load_home_tab_view_004(self): + with open("tests/data/view_home_004.json") as file: + self.verify_loaded_view_object(file) + + def test_load_home_tab_view_005(self): + with open("tests/data/view_home_005.json") as file: + self.verify_loaded_view_object(file) + + def test_load_home_tab_view_006(self): + with open("tests/data/view_home_006.json") as file: + self.verify_loaded_view_object(file) diff --git a/tests/web/mock_web_api_handler.py b/tests/web/mock_web_api_handler.py new file mode 100644 index 000000000..f58aa36ec --- /dev/null +++ b/tests/web/mock_web_api_handler.py @@ -0,0 +1,186 @@ +import json +import logging +import re +import time +from http import HTTPStatus +from http.server import SimpleHTTPRequestHandler +from urllib.parse import parse_qs, urlparse + + +class MockHandler(SimpleHTTPRequestHandler): + protocol_version = "HTTP/1.1" + default_request_version = "HTTP/1.1" + logger = logging.getLogger(__name__) + + pattern_for_language = re.compile("python/(\\S+)", re.IGNORECASE) + pattern_for_package_identifier = re.compile("slackclient/(\\S+)") + + html_response_body = '\n\n404 Not Found\n\n

Not Found

\n

The requested URL /api/team.info was not found on this server.

\n\n' + + error_html_response_body = '\n\n\n\t\n\tServer Error | Slack\n\t\n\t\n\n\n\t\n\t
\n\t\t
\n\t\t\t

\n\t\t\t\t\n\t\t\t\tServer Error\n\t\t\t

\n\t\t\t
\n\t\t\t\t

It seems like there’s a problem connecting to our servers, and we’re investigating the issue.

\n\t\t\t\t

Please check our Status page for updates.

\n\t\t\t
\n\t\t
\n\t
\n\t\n\n' + + error_html_response_body = '\n\n\n\t\n\tServer Error | Slack\n\t\n\t\n\n\n\t\n\t
\n\t\t
\n\t\t\t

\n\t\t\t\t\n\t\t\t\tServer Error\n\t\t\t

\n\t\t\t
\n\t\t\t\t

It seems like there’s a problem connecting to our servers, and we’re investigating the issue.

\n\t\t\t\t

Please check our Status page for updates.

\n\t\t\t
\n\t\t
\n\t
\n\t\n\n' + + def is_valid_user_agent(self): + user_agent = self.headers["User-Agent"] + return self.pattern_for_language.search(user_agent) and self.pattern_for_package_identifier.search(user_agent) + + def is_valid_token(self): + if self.path.startswith("oauth"): + return True + return "Authorization" in self.headers and str(self.headers["Authorization"]).startswith("Bearer xoxb-") + + def set_common_headers(self): + self.send_header("content-type", "application/json;charset=utf-8") + self.send_header("connection", "close") + self.end_headers() + + invalid_auth = { + "ok": False, + "error": "invalid_auth", + } + + not_found = { + "ok": False, + "error": "test_data_not_found", + } + + def _handle(self): + try: + # put_nowait is common between Queue & asyncio.Queue, it does not need to be awaited + self.server.queue.put_nowait(self.path) + + if self.path in {"/oauth.access", "/oauth.v2.access"}: + self.send_response(200) + self.set_common_headers() + if self.headers["authorization"] == "Basic MTExLjIyMjpzZWNyZXQ=": + self.wfile.write("""{"ok":true}""".encode("utf-8")) + return + else: + self.wfile.write("""{"ok":false, "error":"invalid"}""".encode("utf-8")) + return + + if self.is_valid_token() and self.is_valid_user_agent(): + parsed_path = urlparse(self.path) + + len_header = self.headers.get("Content-Length") or 0 + content_len = int(len_header) + post_body = self.rfile.read(content_len) + request_body = None + if post_body: + try: + post_body = post_body.decode("utf-8") + if post_body.startswith("{"): + request_body = json.loads(post_body) + else: + request_body = {k: v[0] for k, v in parse_qs(post_body).items()} + except UnicodeDecodeError: + pass + else: + if parsed_path and parsed_path.query: + request_body = {k: v[0] for k, v in parse_qs(parsed_path.query).items()} + + header = self.headers["authorization"] + pattern = str(header).split("xoxb-", 1)[1] + if pattern.isnumeric(): + self.send_response(int(pattern)) + self.set_common_headers() + self.wfile.write("""{"ok":false}""".encode("utf-8")) + return + if pattern == "ratelimited": + self.send_response(429) + self.send_header("retry-after", 1) + self.set_common_headers() + self.wfile.write("""{"ok":false,"error":"ratelimited"}""".encode("utf-8")) + self.wfile.close() + return + + if pattern == "timeout": + time.sleep(2) + self.send_response(200) + self.wfile.write("""{"ok":true}""".encode("utf-8")) + self.wfile.close() + return + + if pattern == "html_response": + self.send_response(404) + self.send_header("content-type", "text/html;charset=utf-8") + self.send_header("connection", "close") + self.end_headers() + self.wfile.write(self.html_response_body.encode("utf-8")) + self.wfile.close() + return + + if pattern == "error_html_response": + self.send_response(503) + # no charset here is intentional for testing + self.send_header("content-type", "text/html") + self.send_header("connection", "close") + self.end_headers() + self.wfile.write(self.error_html_response_body.encode("utf-8")) + self.wfile.close() + return + + if pattern.startswith("user-agent"): + elements = pattern.split(" ") + prefix, suffix = elements[1], elements[-1] + ua: str = self.headers["User-Agent"] + if ua.startswith(prefix) and ua.endswith(suffix): + self.send_response(200) + self.set_common_headers() + self.wfile.write("""{"ok":true}""".encode("utf-8")) + self.wfile.close() + return + else: + self.send_response(400) + self.set_common_headers() + self.wfile.write("""{"ok":false, "error":"invalid_user_agent"}""".encode("utf-8")) + self.wfile.close() + return + + if request_body and "cursor" in request_body: + page = request_body["cursor"] + pattern = f"{pattern}_{page}" + if pattern == "coverage": + if self.path.startswith("/calls."): + for k, v in request_body.items(): + if k == "users": + users = json.loads(v) + for u in users: + if "slack_id" not in u and "external_id" not in u: + raise Exception(f"User ({u}) is invalid value") + else: + ids = ["channels", "users", "channel_ids"] + if request_body: + for k, v in request_body.items(): + if k in ids: + if not re.compile(r"^[^,\[\]]+?,[^,\[\]]+$").match(v): + raise Exception(f"The parameter {k} is not a comma-separated string value: {v}") + body = {"ok": True, "method": parsed_path.path.replace("/", "")} + else: + with open(f"tests/data/web_response_{pattern}.json") as file: + body = json.load(file) + + if self.path == "/api.test" and request_body: + body["args"] = request_body + + else: + body = self.invalid_auth + + if not body: + body = self.not_found + + self.send_response(HTTPStatus.OK) + self.set_common_headers() + self.wfile.write(json.dumps(body).encode("utf-8")) + self.wfile.close() + + except Exception as e: + self.logger.error(str(e), exc_info=True) + raise + + def do_GET(self): + self._handle() + + def do_POST(self): + self._handle() diff --git a/tests/web/test_async_web_client.py b/tests/web/test_async_web_client.py new file mode 100644 index 000000000..151a6ae55 --- /dev/null +++ b/tests/web/test_async_web_client.py @@ -0,0 +1,158 @@ +import io +import re +import unittest + +import slack.errors as err +from slack import AsyncWebClient +from tests.helpers import async_test +from tests.web.mock_web_api_handler import MockHandler +from tests.mock_web_api_server import setup_mock_web_api_server_async, cleanup_mock_web_api_server_async + + +class TestAsyncWebClient(unittest.TestCase): + def setUp(self): + setup_mock_web_api_server_async(self, MockHandler) + self.client = AsyncWebClient( + token="xoxp-1234", + base_url="http://localhost:8888", + ) + + def tearDown(self): + cleanup_mock_web_api_server_async(self) + + pattern_for_language = re.compile("python/(\\S+)", re.IGNORECASE) + pattern_for_package_identifier = re.compile("slackclient/(\\S+)") + + @async_test + async def test_api_calls_return_a_future(self): + self.client.token = "xoxb-api_test" + resp = await self.client.api_test() + self.assertEqual(200, resp.status_code) + self.assertTrue(resp["ok"]) + + @async_test + async def test_requests_can_be_paginated(self): + self.client.token = "xoxb-users_list_pagination" + users = [] + async for page in await self.client.users_list(limit=2): + users = users + page["members"] + self.assertTrue(len(users) == 4) + + @async_test + async def test_request_pagination_stops_when_next_cursor_is_missing(self): + self.client.token = "xoxb-users_list_pagination_1" + users = [] + async for page in await self.client.users_list(limit=2): + users = users + page["members"] + self.assertTrue(len(users) == 2) + + @async_test + async def test_json_can_only_be_sent_with_post_requests(self): + with self.assertRaises(err.SlackRequestError): + await self.client.api_call("fake.method", http_verb="GET", json={}) + + @async_test + async def test_slack_api_error_is_raised_on_unsuccessful_responses(self): + self.client.token = "xoxb-api_test_false" + with self.assertRaises(err.SlackApiError): + await self.client.api_test() + self.client.token = "xoxb-500" + with self.assertRaises(err.SlackApiError): + await self.client.api_test() + + @async_test + async def test_slack_api_rate_limiting_exception_returns_retry_after(self): + self.client.token = "xoxb-ratelimited" + try: + await self.client.api_test() + except err.SlackApiError as slack_api_error: + self.assertFalse(slack_api_error.response["ok"]) + self.assertEqual(429, slack_api_error.response.status_code) + self.assertEqual(1, int(slack_api_error.response.headers["retry-after"])) + self.assertEqual(1, int(slack_api_error.response.headers["Retry-After"])) + + @async_test + async def test_the_api_call_files_argument_creates_the_expected_data(self): + self.client.token = "xoxb-users_setPhoto" + resp = await self.client.users_setPhoto(image="tests/data/slack_logo.png") + self.assertEqual(200, resp.status_code) + + @async_test + async def test_issue_560_bool_in_params_sync(self): + self.client.token = "xoxb-conversations_list" + await self.client.conversations_list(exclude_archived=1) # ok + await self.client.conversations_list(exclude_archived="true") # ok + await self.client.conversations_list(exclude_archived=True) # ok + + @async_test + async def test_issue_690_oauth_v2_access_async(self): + self.client.token = "" + resp = await self.client.oauth_v2_access( + client_id="111.222", + client_secret="secret", + code="codeeeeeeeeee", + ) + self.assertIsNone(resp["error"]) + with self.assertRaises(err.SlackApiError): + await self.client.oauth_v2_access( + client_id="999.999", + client_secret="secret", + code="codeeeeeeeeee", + ) + + @async_test + async def test_issue_690_oauth_access_async(self): + self.client.token = "" + resp = await self.client.oauth_access(client_id="111.222", client_secret="secret", code="codeeeeeeeeee") + self.assertIsNone(resp["error"]) + with self.assertRaises(err.SlackApiError): + await self.client.oauth_access(client_id="999.999", client_secret="secret", code="codeeeeeeeeee") + + @async_test + async def test_token_param_async(self): + with self.assertRaises(err.SlackApiError): + await self.client.users_list() + resp = await self.client.users_list(token="xoxb-users_list_pagination") + self.assertIsNone(resp["error"]) + with self.assertRaises(err.SlackApiError): + await self.client.users_list() + + @async_test + async def test_timeout_issue_712_async(self): + with self.assertRaises(Exception): + await self.client.users_list(token="xoxb-timeout") + + @async_test + async def test_html_response_body_issue_718_async(self): + try: + await self.client.users_list(token="xoxb-html_response") + self.fail("SlackApiError expected here") + except err.SlackApiError as e: + self.assertEqual( + "The request to the Slack API failed. (url: http://localhost:8888/users.list, status: 404)\n" + "The server responded with: {}", + str(e), + ) + + @async_test + async def test_user_agent_customization_issue_769_async(self): + client = AsyncWebClient( + token="xoxb-user-agent this_is test", + base_url="http://localhost:8888", + user_agent_prefix="this_is", + user_agent_suffix="test", + ) + resp = await client.api_test() + self.assertTrue(resp["ok"]) + + @async_test + async def test_issue_809_filename_for_IOBase(self): + self.client.token = "xoxb-api_test" + file = io.BytesIO(b"here is my data but not sure what is wrong.......") + resp = await self.client.files_upload(file=file) + self.assertIsNone(resp["error"]) + # if file: + # if "filename" not in kwargs: + # # use the local filename if filename is missing + # > kwargs["filename"] = file.split(os.path.sep)[-1] + # E AttributeError: '_io.BytesIO' object has no attribute 'split' diff --git a/tests/web/test_slack_response.py b/tests/web/test_slack_response.py new file mode 100644 index 000000000..e305fbfbf --- /dev/null +++ b/tests/web/test_slack_response.py @@ -0,0 +1,28 @@ +import unittest + +from slack import WebClient +from slack.web.slack_response import SlackResponse + + +class TestSlackResponse(unittest.TestCase): + def setUp(self): + pass + + def tearDown(self): + pass + + # https://github.com/slackapi/python-slackclient/issues/559 + def test_issue_559(self): + response = SlackResponse( + client=WebClient(token="xoxb-dummy"), + http_verb="POST", + api_url="http://localhost:3000/api.test", + req_args={}, + data={"ok": True, "args": {"hello": "world"}}, + headers={}, + status_code=200, + ) + + self.assertTrue("ok" in response.data) + self.assertTrue("args" in response.data) + self.assertFalse("error" in response.data) diff --git a/tests/web/test_web_client.py b/tests/web/test_web_client.py new file mode 100644 index 000000000..71c8b6287 --- /dev/null +++ b/tests/web/test_web_client.py @@ -0,0 +1,314 @@ +import asyncio +import io +import re +import socket +import unittest + +import slack.errors as err +from slack import WebClient +from tests.helpers import async_test +from tests.web.mock_web_api_handler import MockHandler +from tests.mock_web_api_server import setup_mock_web_api_server_async, cleanup_mock_web_api_server_async + + +class TestWebClient(unittest.TestCase): + def setUp(self): + setup_mock_web_api_server_async(self, MockHandler) + self.client = WebClient( + token="xoxp-1234", + base_url="http://localhost:8888", + ) + self.async_client = WebClient( + token="xoxp-1234", + run_async=True, + base_url="http://localhost:8888", + ) + + def tearDown(self): + cleanup_mock_web_api_server_async(self) + + pattern_for_language = re.compile("python/(\\S+)", re.IGNORECASE) + pattern_for_package_identifier = re.compile("slackclient/(\\S+)") + + def test_api_calls_return_a_response_when_run_in_sync_mode(self): + self.client.token = "xoxb-api_test" + resp = self.client.api_test() + self.assertFalse(asyncio.isfuture(resp)) + self.assertTrue(resp["ok"]) + + def test_api_calls_include_user_agent(self): + self.client.token = "xoxb-api_test" + resp = self.client.api_test() + self.assertEqual(200, resp.status_code) + + @async_test + async def test_api_calls_return_a_future_when_run_in_async_mode(self): + self.client.token = "xoxb-api_test" + self.client.run_async = True + future = self.client.api_test() + self.assertTrue(asyncio.isfuture(future)) + resp = await future + self.assertEqual(200, resp.status_code) + self.assertTrue(resp["ok"]) + + def test_builtin_api_methods_send_json(self): + self.client.token = "xoxb-api_test" + resp = self.client.api_test(msg="bye") + self.assertEqual(200, resp.status_code) + self.assertEqual("bye", resp["args"]["msg"]) + + def test_requests_can_be_paginated(self): + self.client.token = "xoxb-users_list_pagination" + users = [] + for page in self.client.users_list(limit=2): + users = users + page["members"] + self.assertTrue(len(users) == 4) + + def test_response_can_be_paginated_multiple_times(self): + self.client.token = "xoxb-channels_list_pagination" + # This test suite verifies the changes in #521 work as expected + response = self.client.channels_list(limit=1) + ids = [] + for page in response: + ids.append(page["channels"][0]["id"]) + self.assertEqual(ids, ["C1", "C2", "C3"]) + + # The second iteration starting with page 2 + # (page1 is already cached in `response`) + self.client.token = "xoxb-channels_list_pagination2" + ids = [] + for page in response: + ids.append(page["channels"][0]["id"]) + self.assertEqual(ids, ["C1", "C2", "C3"]) + + def test_request_pagination_stops_when_next_cursor_is_missing(self): + self.client.token = "xoxb-users_list_pagination_1" + users = [] + for page in self.client.users_list(limit=2): + users = users + page["members"] + self.assertTrue(len(users) == 2) + + def test_response_can_be_paginated_multiple_times_use_sync_aiohttp(self): + self.client = WebClient( + token="xoxp-1234", + base_url="http://localhost:8888", + run_async=False, + use_sync_aiohttp=True, + ) + self.client.token = "xoxb-channels_list_pagination" + # This test suite verifies the changes in #521 work as expected + response = self.client.channels_list(limit=1) + ids = [] + for page in response: + ids.append(page["channels"][0]["id"]) + self.assertEqual(ids, ["C1", "C2", "C3"]) + + # The second iteration starting with page 2 + # (page1 is already cached in `response`) + self.client.token = "xoxb-channels_list_pagination2" + ids = [] + for page in response: + ids.append(page["channels"][0]["id"]) + self.assertEqual(ids, ["C1", "C2", "C3"]) + + def test_json_can_only_be_sent_with_post_requests(self): + with self.assertRaises(err.SlackRequestError): + self.client.api_call("fake.method", http_verb="GET", json={}) + + def test_slack_api_error_is_raised_on_unsuccessful_responses(self): + self.client.token = "xoxb-api_test_false" + with self.assertRaises(err.SlackApiError): + self.client.api_test() + self.client.token = "xoxb-500" + with self.assertRaises(err.SlackApiError): + self.client.api_test() + + def test_slack_api_rate_limiting_exception_returns_retry_after(self): + self.client.token = "xoxb-ratelimited" + try: + self.client.api_test() + except err.SlackApiError as slack_api_error: + self.assertFalse(slack_api_error.response["ok"]) + self.assertEqual(429, slack_api_error.response.status_code) + self.assertEqual(1, int(slack_api_error.response.headers["retry-after"])) + self.assertEqual(1, int(slack_api_error.response.headers["Retry-After"])) + + def test_the_api_call_files_argument_creates_the_expected_data(self): + self.client.token = "xoxb-users_setPhoto" + resp = self.client.users_setPhoto(image="tests/data/slack_logo.png") + self.assertEqual(200, resp.status_code) + + def test_issue_560_bool_in_params_sync(self): + self.client.token = "xoxb-conversations_list" + self.client.conversations_list(exclude_archived=1) # ok + self.client.conversations_list(exclude_archived="true") # ok + self.client.conversations_list(exclude_archived=True) # ok + + @async_test + async def test_issue_560_bool_in_params_async(self): + self.async_client.token = "xoxb-conversations_list" + await self.async_client.conversations_list(exclude_archived=1) # ok + await self.async_client.conversations_list(exclude_archived="true") # ok + await self.async_client.conversations_list(exclude_archived=True) # TypeError + + def test_issue_690_oauth_v2_access(self): + self.client.token = "" + resp = self.client.oauth_v2_access(client_id="111.222", client_secret="secret", code="codeeeeeeeeee") + self.assertIsNone(resp["error"]) + with self.assertRaises(err.SlackApiError): + self.client.oauth_v2_access(client_id="999.999", client_secret="secret", code="codeeeeeeeeee") + + @async_test + async def test_issue_690_oauth_v2_access_async(self): + self.async_client.token = "" + resp = await self.async_client.oauth_v2_access( + client_id="111.222", + client_secret="secret", + code="codeeeeeeeeee", + ) + self.assertIsNone(resp["error"]) + with self.assertRaises(err.SlackApiError): + await self.async_client.oauth_v2_access( + client_id="999.999", + client_secret="secret", + code="codeeeeeeeeee", + ) + + def test_issue_690_oauth_access(self): + self.client.token = "" + resp = self.client.oauth_access(client_id="111.222", client_secret="secret", code="codeeeeeeeeee") + self.assertIsNone(resp["error"]) + with self.assertRaises(err.SlackApiError): + self.client.oauth_access(client_id="999.999", client_secret="secret", code="codeeeeeeeeee") + + @async_test + async def test_issue_690_oauth_access_async(self): + self.async_client.token = "" + resp = await self.async_client.oauth_access(client_id="111.222", client_secret="secret", code="codeeeeeeeeee") + self.assertIsNone(resp["error"]) + with self.assertRaises(err.SlackApiError): + await self.async_client.oauth_access(client_id="999.999", client_secret="secret", code="codeeeeeeeeee") + + def test_issue_705_no_param_request_pagination(self): + self.client.token = "xoxb-users_list_pagination" + users = [] + for page in self.client.users_list(): + users = users + page["members"] + self.assertTrue(len(users) == 4) + + def test_token_param(self): + client = WebClient(base_url="http://localhost:8888") + with self.assertRaises(err.SlackApiError): + client.users_list() + resp = client.users_list(token="xoxb-users_list_pagination") + self.assertIsNone(resp["error"]) + with self.assertRaises(err.SlackApiError): + client.users_list() + + @async_test + async def test_token_param_async(self): + client = WebClient(base_url="http://localhost:8888", run_async=True) + with self.assertRaises(err.SlackApiError): + await client.users_list() + resp = await client.users_list(token="xoxb-users_list_pagination") + self.assertIsNone(resp["error"]) + with self.assertRaises(err.SlackApiError): + await client.users_list() + + def test_timeout_issue_712(self): + client = WebClient(base_url="http://localhost:8888", timeout=1) + with self.assertRaises(socket.timeout): + client.users_list(token="xoxb-timeout") + + @async_test + async def test_timeout_issue_712_async(self): + client = WebClient(base_url="http://localhost:8888", timeout=1, run_async=True) + with self.assertRaises(asyncio.TimeoutError): + await client.users_list(token="xoxb-timeout") + + # NOTE: This test may be unstable in GitHub Actions environment. + # As we no longer recommend using this LegacyWebClient, + # let us disable this test to avoid noises in CI builds. + # --------------------- + # def test_unclosed_client_session_issue_645_in_async_mode(self): + # def exception_handler(_, context): + # nonlocal session_unclosed + # if context["message"] == "Unclosed client session": + # session_unclosed = True + # + # async def issue_645(): + # client = WebClient( + # base_url="http://localhost:8888", timeout=1, run_async=True + # ) + # try: + # await client.users_list(token="xoxb-timeout") + # except asyncio.TimeoutError: + # pass + # + # session_unclosed = False + # loop = asyncio.get_event_loop() + # loop.set_exception_handler(exception_handler) + # loop.run_until_complete(issue_645()) + # gc.collect() # force Python to gc unclosed client session + # self.assertFalse(session_unclosed, "Unclosed client session") + + def test_html_response_body_issue_718(self): + client = WebClient(base_url="http://localhost:8888") + try: + client.users_list(token="xoxb-html_response") + self.fail("SlackApiError expected here") + except err.SlackApiError as e: + self.assertTrue( + str(e).startswith("Received a response in a non-JSON format: "), + e, + ) + + @async_test + async def test_html_response_body_issue_718_async(self): + client = WebClient(base_url="http://localhost:8888", run_async=True) + try: + await client.users_list(token="xoxb-html_response") + self.fail("SlackApiError expected here") + except err.SlackApiError as e: + self.assertEqual( + "The request to the Slack API failed.\n" "The server responded with: {}", + str(e), + ) + + def test_user_agent_customization_issue_769(self): + client = WebClient( + base_url="http://localhost:8888", + token="xoxb-user-agent this_is test", + user_agent_prefix="this_is", + user_agent_suffix="test", + ) + resp = client.api_test() + self.assertTrue(resp["ok"]) + + @async_test + async def test_user_agent_customization_issue_769_async(self): + client = WebClient( + run_async=True, + base_url="http://localhost:8888", + token="xoxb-user-agent this_is test", + user_agent_prefix="this_is", + user_agent_suffix="test", + ) + resp = await client.api_test() + self.assertTrue(resp["ok"]) + + def test_issue_809_filename_for_IOBase(self): + self.client.token = "xoxb-api_test" + file = io.BytesIO(b"here is my data but not sure what is wrong.......") + resp = self.client.files_upload(file=file) + self.assertIsNone(resp["error"]) + # if file: + # if "filename" not in kwargs: + # # use the local filename if filename is missing + # > kwargs["filename"] = file.split(os.path.sep)[-1] + # E AttributeError: '_io.BytesIO' object has no attribute 'split' + + def test_default_team_id(self): + client = WebClient(base_url="http://localhost:8888", team_id="T_DEFAULT") + resp = client.users_list(token="xoxb-users_list_pagination") + self.assertIsNone(resp["error"]) diff --git a/tests/web/test_web_client_coverage.py b/tests/web/test_web_client_coverage.py new file mode 100644 index 000000000..70a4788ab --- /dev/null +++ b/tests/web/test_web_client_coverage.py @@ -0,0 +1,2 @@ +# We no longer maintain this test. +# Add new method tests to slack_sdk_tests_async/web/test_web_client_coverage.py diff --git a/tests/web/test_web_client_functional.py b/tests/web/test_web_client_functional.py new file mode 100644 index 000000000..c2ece8de7 --- /dev/null +++ b/tests/web/test_web_client_functional.py @@ -0,0 +1,25 @@ +import unittest + +import slack +from tests.web.mock_web_api_handler import MockHandler +from tests.mock_web_api_server import setup_mock_web_api_server, cleanup_mock_web_api_server + + +class TestWebClientFunctional(unittest.TestCase): + def setUp(self): + setup_mock_web_api_server(self, MockHandler) + self.client = slack.WebClient(token="xoxb-api_test", base_url="http://localhost:8888") + + def tearDown(self): + cleanup_mock_web_api_server(self) + + def test_requests_with_use_session_turned_off(self): + self.client.use_pooling = False + resp = self.client.api_test() + assert resp["ok"] + + def test_subsequent_requests_with_a_session_succeeds(self): + resp = self.client.api_test() + assert resp["ok"] + resp = self.client.api_test() + assert resp["ok"] diff --git a/tests/web/test_web_client_issue_829.py b/tests/web/test_web_client_issue_829.py new file mode 100644 index 000000000..d728b8404 --- /dev/null +++ b/tests/web/test_web_client_issue_829.py @@ -0,0 +1,47 @@ +import unittest + +import slack.errors as err +from slack import WebClient +from tests.helpers import async_test +from tests.web.mock_web_api_handler import MockHandler +from tests.mock_web_api_server import setup_mock_web_api_server_async, cleanup_mock_web_api_server_async + + +class TestWebClient_Issue_829(unittest.TestCase): + def setUp(self): + setup_mock_web_api_server_async(self, MockHandler) + self.client = WebClient( + token="xoxp-1234", + base_url="http://localhost:8888", + ) + self.async_client = WebClient( + token="xoxp-1234", + run_async=True, + base_url="http://localhost:8888", + ) + + def tearDown(self): + cleanup_mock_web_api_server_async(self) + + def test_html_response_body_issue_829(self): + client = WebClient(base_url="http://localhost:8888") + try: + client.users_list(token="xoxb-error_html_response") + self.fail("SlackApiError expected here") + except err.SlackApiError as e: + self.assertTrue( + str(e).startswith("Received a response in a non-JSON format: "), + e, + ) + + @async_test + async def test_html_response_body_issue_829_async(self): + client = WebClient(base_url="http://localhost:8888", run_async=True) + try: + await client.users_list(token="xoxb-error_html_response") + self.fail("SlackApiError expected here") + except err.SlackApiError as e: + self.assertEqual( + "The request to the Slack API failed.\n" "The server responded with: {}", + str(e), + ) diff --git a/tests/web/test_web_client_issue_921_custom_logger.py b/tests/web/test_web_client_issue_921_custom_logger.py new file mode 100644 index 000000000..1580b9b5c --- /dev/null +++ b/tests/web/test_web_client_issue_921_custom_logger.py @@ -0,0 +1,35 @@ +import unittest +from logging import Logger + +from slack.web import WebClient +from tests.slack_sdk.web.mock_web_api_handler import MockHandler +from tests.mock_web_api_server import setup_mock_web_api_server, cleanup_mock_web_api_server + + +class TestWebClient_Issue_921_CustomLogger(unittest.TestCase): + def setUp(self): + setup_mock_web_api_server(self, MockHandler) + + def tearDown(self): + cleanup_mock_web_api_server(self) + + def test_if_it_uses_custom_logger(self): + logger = CustomLogger("test-logger") + client = WebClient( + base_url="http://localhost:8888", + token="xoxb-api_test", + logger=logger, + ) + client.chat_postMessage(channel="C111", text="hello") + self.assertTrue(logger.called) + + +class CustomLogger(Logger): + called: bool + + def __init__(self, name, level="DEBUG"): + Logger.__init__(self, name, level) + self.called = False + + def debug(self, msg, *args, **kwargs): + self.called = True diff --git a/tests/web/test_web_client_msg_text_content_warnings.py b/tests/web/test_web_client_msg_text_content_warnings.py new file mode 100644 index 000000000..11ca100ac --- /dev/null +++ b/tests/web/test_web_client_msg_text_content_warnings.py @@ -0,0 +1,105 @@ +import unittest +import warnings + +from slack import WebClient +from tests.web.mock_web_api_handler import MockHandler +from tests.mock_web_api_server import setup_mock_web_api_server, cleanup_mock_web_api_server + + +class TestWebClientMessageTextContentWarnings(unittest.TestCase): + def setUp(self): + setup_mock_web_api_server(self, MockHandler) + + def tearDown(self): + cleanup_mock_web_api_server(self) + warnings.resetwarnings() + + def test_missing_text_warning_chat_postMessage(self): + client = WebClient(base_url="http://localhost:8888", token="xoxb-api_test") + with self.assertWarnsRegex(UserWarning, "`text` argument is missing"): + resp = client.chat_postMessage(channel="C111", blocks=[]) + self.assertIsNone(resp["error"]) + + def test_missing_text_warning_chat_postEphemeral(self): + client = WebClient(base_url="http://localhost:8888", token="xoxb-api_test") + with self.assertWarnsRegex(UserWarning, "`text` argument is missing"): + resp = client.chat_postEphemeral(channel="C111", user="U111", blocks=[]) + self.assertIsNone(resp["error"]) + + def test_missing_text_warning_chat_scheduleMessage(self): + client = WebClient(base_url="http://localhost:8888", token="xoxb-api_test") + with self.assertWarnsRegex(UserWarning, "`text` argument is missing"): + resp = client.chat_scheduleMessage(channel="C111", post_at="299876400", text="", blocks=[]) + self.assertIsNone(resp["error"]) + + def test_missing_text_warning_chat_update(self): + client = WebClient(base_url="http://localhost:8888", token="xoxb-api_test") + with self.assertWarnsRegex(UserWarning, "`text` argument is missing"): + resp = client.chat_update(channel="C111", ts="111.222", blocks=[]) + self.assertIsNone(resp["error"]) + + def test_missing_fallback_warning_chat_postMessage(self): + client = WebClient(base_url="http://localhost:8888", token="xoxb-api_test") + with self.assertWarnsRegex(UserWarning, "`fallback` argument is missing"): + resp = client.chat_postMessage(channel="C111", blocks=[], attachments=[{"text": "hi"}]) + self.assertIsNone(resp["error"]) + + def test_missing_fallback_warning_chat_postEphemeral(self): + client = WebClient(base_url="http://localhost:8888", token="xoxb-api_test") + with self.assertWarnsRegex(UserWarning, "`fallback` argument is missing"): + resp = client.chat_postEphemeral(channel="C111", user="U111", blocks=[], attachments=[{"text": "hi"}]) + self.assertIsNone(resp["error"]) + + def test_missing_fallback_warning_chat_scheduleMessage(self): + client = WebClient(base_url="http://localhost:8888", token="xoxb-api_test") + with self.assertWarnsRegex(UserWarning, "`fallback` argument is missing"): + resp = client.chat_scheduleMessage( + channel="C111", + post_at="299876400", + text="", + blocks=[], + attachments=[{"text": "hi"}], + ) + self.assertIsNone(resp["error"]) + + def test_missing_fallback_warning_chat_update(self): + client = WebClient(base_url="http://localhost:8888", token="xoxb-api_test") + with self.assertWarnsRegex(UserWarning, "`fallback` argument is missing"): + resp = client.chat_update(channel="C111", ts="111.222", blocks=[], attachments=[{"text": "hi"}]) + self.assertIsNone(resp["error"]) + + def test_no_warning_when_markdown_text_is_provided_chat_postMessage(self): + client = WebClient(base_url="http://localhost:8888", token="xoxb-api_test") + with warnings.catch_warnings(record=True) as warning_list: + warnings.simplefilter("always") + resp = client.chat_postMessage(channel="C111", markdown_text="# hello") + + self.assertEqual(warning_list, []) + self.assertIsNone(resp["error"]) + + def test_no_warning_when_markdown_text_is_provided_chat_postEphemeral(self): + client = WebClient(base_url="http://localhost:8888", token="xoxb-api_test") + with warnings.catch_warnings(record=True) as warning_list: + warnings.simplefilter("always") + resp = client.chat_postEphemeral(channel="C111", user="U111", markdown_text="# hello") + + self.assertEqual(warning_list, []) + self.assertIsNone(resp["error"]) + + def test_no_warning_when_markdown_text_is_provided_chat_scheduleMessage(self): + client = WebClient(base_url="http://localhost:8888", token="xoxb-api_test") + with warnings.catch_warnings(record=True) as warning_list: + warnings.simplefilter("always") + resp = client.chat_scheduleMessage(channel="C111", post_at="299876400", markdown_text="# hello") + + self.assertEqual(warning_list, []) + self.assertIsNone(resp["error"]) + + def test_no_warning_when_markdown_text_is_provided_chat_update(self): + client = WebClient(base_url="http://localhost:8888", token="xoxb-api_test") + with warnings.catch_warnings(record=True) as warning_list: + warnings.simplefilter("always") + resp = client.chat_update(channel="C111", ts="111.222", markdown_text="# hello") + + self.assertEqual(warning_list, []) + self.assertIsNone(resp["error"]) diff --git a/tests/webhook/mock_web_api_handler.py b/tests/webhook/mock_web_api_handler.py new file mode 100644 index 000000000..9de171fee --- /dev/null +++ b/tests/webhook/mock_web_api_handler.py @@ -0,0 +1,79 @@ +import json +import logging +import re +import time +from http import HTTPStatus +from http.server import SimpleHTTPRequestHandler + + +class MockHandler(SimpleHTTPRequestHandler): + protocol_version = "HTTP/1.1" + default_request_version = "HTTP/1.1" + logger = logging.getLogger(__name__) + + pattern_for_language = re.compile("python/(\\S+)", re.IGNORECASE) + pattern_for_package_identifier = re.compile("slackclient/(\\S+)") + + error_html_response_body = '\n\n\n\t\n\tServer Error | Slack\n\t\n\t\n\n\n\t\n\t
\n\t\t
\n\t\t\t

\n\t\t\t\t\n\t\t\t\tServer Error\n\t\t\t

\n\t\t\t
\n\t\t\t\t

It seems like there’s a problem connecting to our servers, and we’re investigating the issue.

\n\t\t\t\t

Please check our Status page for updates.

\n\t\t\t
\n\t\t
\n\t
\n\t\n\n' + + def is_valid_user_agent(self): + user_agent = self.headers["User-Agent"] + return self.pattern_for_language.search(user_agent) and self.pattern_for_package_identifier.search(user_agent) + + def set_common_headers(self): + self.send_header("content-type", "text/plain;charset=utf-8") + self.send_header("connection", "close") + self.end_headers() + + def do_GET(self): + if self.path == "/received_requests.json": + self.send_response(200) + self.set_common_headers() + self.wfile.write(json.dumps(self.received_requests).encode("utf-8")) + return + + def do_POST(self): + try: + # put_nowait is common between Queue & asyncio.Queue, it does not need to be awaited + self.server.queue.put_nowait(self.path) + if self.path == "/timeout": + time.sleep(2) + + # user-agent-this_is-test + if self.path.startswith("/user-agent-"): + elements = self.path.split("-") + prefix, suffix = elements[2], elements[-1] + ua: str = self.headers["User-Agent"] + if ua.startswith(prefix) and ua.endswith(suffix): + self.send_response(HTTPStatus.OK) + self.set_common_headers() + self.wfile.write("ok".encode("utf-8")) + self.wfile.close() + return + else: + self.send_response(HTTPStatus.BAD_REQUEST) + self.set_common_headers() + self.wfile.write("invalid user agent".encode("utf-8")) + self.wfile.close() + return + + if self.path == "/error": + self.send_response(HTTPStatus.INTERNAL_SERVER_ERROR) + # no charset here is intentional for testing + self.send_header("content-type", "text/html") + self.send_header("connection", "close") + self.end_headers() + self.wfile.write(self.error_html_response_body.encode("utf-8")) + self.wfile.close() + return + + body = "ok" + + self.send_response(HTTPStatus.OK) + self.set_common_headers() + self.wfile.write(body.encode("utf-8")) + self.wfile.close() + + except Exception as e: + self.logger.error(str(e), exc_info=True) + raise diff --git a/tests/webhook/test_async_webhook.py b/tests/webhook/test_async_webhook.py new file mode 100644 index 000000000..829ec0565 --- /dev/null +++ b/tests/webhook/test_async_webhook.py @@ -0,0 +1,184 @@ +from tests.helpers import async_test +import unittest + +from slack.web.classes.attachments import Attachment, AttachmentField +from slack.web.classes.blocks import SectionBlock, ImageBlock +from slack.webhook import AsyncWebhookClient, WebhookResponse +from tests.webhook.mock_web_api_handler import MockHandler +from tests.mock_web_api_server import setup_mock_web_api_server_async, cleanup_mock_web_api_server_async + + +class TestAsyncWebhook(unittest.TestCase): + def setUp(self): + setup_mock_web_api_server_async(self, MockHandler) + + def tearDown(self): + cleanup_mock_web_api_server_async(self) + + @async_test + async def test_send(self): + client = AsyncWebhookClient("http://localhost:8888") + + resp: WebhookResponse = await client.send(text="hello!") + self.assertEqual(200, resp.status_code) + self.assertEqual("ok", resp.body) + + resp = await client.send(text="hello!", response_type="in_channel") + self.assertEqual("ok", resp.body) + + @async_test + async def test_send_with_url_unfurl_opts_issue_1045(self): + client = AsyncWebhookClient("http://localhost:8888") + resp: WebhookResponse = await client.send( + text="", + unfurl_links=False, + unfurl_media=False, + ) + self.assertEqual(200, resp.status_code) + self.assertEqual("ok", resp.body) + + @async_test + async def test_send_blocks(self): + client = AsyncWebhookClient("http://localhost:8888") + + resp = await client.send( + text="hello!", + response_type="ephemeral", + blocks=[ + SectionBlock(text="Some text"), + ImageBlock(image_url="image.jpg", alt_text="an image"), + ], + ) + self.assertEqual("ok", resp.body) + + resp = await client.send( + text="hello!", + response_type="ephemeral", + blocks=[ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "This is a mrkdwn section block :ghost: *this is bold*, and ~this is crossed out~, and ", + }, + }, + {"type": "divider"}, + { + "type": "section", + "text": {"type": "mrkdwn", "text": "Pick a date for the deadline."}, + "accessory": { + "type": "datepicker", + "initial_date": "1990-04-28", + "placeholder": { + "type": "plain_text", + "text": "Select a date", + }, + }, + }, + ], + ) + self.assertEqual("ok", resp.body) + + resp = await client.send( + text="hello!", + response_type="ephemeral", + blocks=[ + SectionBlock(text="Some text"), + ImageBlock(image_url="image.jpg", alt_text="an image"), + ], + ) + self.assertEqual("ok", resp.body) + + @async_test + async def test_send_attachments(self): + client = AsyncWebhookClient("http://localhost:8888") + + resp = await client.send( + text="hello!", + response_type="ephemeral", + attachments=[ + { + "color": "#f2c744", + "blocks": [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "This is a mrkdwn section block :ghost: *this is bold*, and ~this is crossed out~, and ", + }, + }, + {"type": "divider"}, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "Pick a date for the deadline.", + }, + "accessory": { + "type": "datepicker", + "initial_date": "1990-04-28", + "placeholder": { + "type": "plain_text", + "text": "Select a date", + }, + }, + }, + ], + } + ], + ) + self.assertEqual("ok", resp.body) + + resp = await client.send( + text="hello!", + response_type="ephemeral", + attachments=[ + Attachment( + text="attachment text", + title="Attachment", + fallback="fallback_text", + pretext="some_pretext", + title_link="link in title", + fields=[AttachmentField(title=f"field_{i}_title", value=f"field_{i}_value") for i in range(5)], + color="#FFFF00", + author_name="John Doe", + author_link="http://johndoeisthebest.com", + author_icon="http://johndoeisthebest.com/avatar.jpg", + thumb_url="thumbnail URL", + footer="and a footer", + footer_icon="link to footer icon", + ts=123456789, + markdown_in=["fields"], + ) + ], + ) + self.assertEqual("ok", resp.body) + + @async_test + async def test_send_dict(self): + client = AsyncWebhookClient("http://localhost:8888") + resp: WebhookResponse = await client.send_dict({"text": "hello!"}) + self.assertEqual(200, resp.status_code) + self.assertEqual("ok", resp.body) + + @async_test + async def test_timeout_issue_712(self): + client = AsyncWebhookClient(url="http://localhost:8888/timeout", timeout=1) + with self.assertRaises(Exception): + await client.send_dict({"text": "hello!"}) + + @async_test + async def test_proxy_issue_714(self): + client = AsyncWebhookClient(url="http://localhost:8888", proxy="http://invalid-host:9999") + with self.assertRaises(Exception): + await client.send_dict({"text": "hello!"}) + + @async_test + async def test_user_agent_customization_issue_769(self): + client = AsyncWebhookClient( + url="http://localhost:8888/user-agent-this_is-test", + user_agent_prefix="this_is", + user_agent_suffix="test", + ) + resp = await client.send_dict({"text": "hi!"}) + self.assertEqual(resp.body, "ok") diff --git a/tests/webhook/test_webhook.py b/tests/webhook/test_webhook.py new file mode 100644 index 000000000..abd7f79fa --- /dev/null +++ b/tests/webhook/test_webhook.py @@ -0,0 +1,183 @@ +import unittest +import socket +import urllib + +from slack.web.classes.attachments import Attachment, AttachmentField +from slack.web.classes.blocks import SectionBlock, ImageBlock +from slack.webhook import WebhookClient, WebhookResponse +from tests.webhook.mock_web_api_handler import MockHandler +from tests.mock_web_api_server import setup_mock_web_api_server, cleanup_mock_web_api_server + + +class TestWebhook(unittest.TestCase): + def setUp(self): + setup_mock_web_api_server(self, MockHandler) + + def tearDown(self): + cleanup_mock_web_api_server(self) + + def test_send(self): + client = WebhookClient("http://localhost:8888") + + resp: WebhookResponse = client.send(text="hello!") + self.assertEqual(200, resp.status_code) + self.assertEqual("ok", resp.body) + + resp = client.send(text="hello!", response_type="in_channel") + self.assertEqual("ok", resp.body) + + def test_send_with_url_unfurl_opts_issue_1045(self): + client = WebhookClient("http://localhost:8888") + resp: WebhookResponse = client.send( + text="", + unfurl_links=False, + unfurl_media=False, + ) + self.assertEqual(200, resp.status_code) + self.assertEqual("ok", resp.body) + + def test_send_blocks(self): + client = WebhookClient("http://localhost:8888") + + resp = client.send( + text="hello!", + response_type="ephemeral", + blocks=[ + SectionBlock(text="Some text"), + ImageBlock(image_url="image.jpg", alt_text="an image"), + ], + ) + self.assertEqual("ok", resp.body) + + resp = client.send( + text="hello!", + response_type="ephemeral", + blocks=[ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "This is a mrkdwn section block :ghost: *this is bold*, and ~this is crossed out~, and ", + }, + }, + {"type": "divider"}, + { + "type": "section", + "text": {"type": "mrkdwn", "text": "Pick a date for the deadline."}, + "accessory": { + "type": "datepicker", + "initial_date": "1990-04-28", + "placeholder": { + "type": "plain_text", + "text": "Select a date", + }, + }, + }, + ], + ) + self.assertEqual("ok", resp.body) + + resp = client.send( + text="hello!", + response_type="ephemeral", + blocks=[ + SectionBlock(text="Some text"), + ImageBlock(image_url="image.jpg", alt_text="an image"), + ], + ) + self.assertEqual("ok", resp.body) + + def test_send_attachments(self): + client = WebhookClient("http://localhost:8888") + + resp = client.send( + text="hello!", + response_type="ephemeral", + attachments=[ + { + "color": "#f2c744", + "blocks": [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "This is a mrkdwn section block :ghost: *this is bold*, and ~this is crossed out~, and ", + }, + }, + {"type": "divider"}, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "Pick a date for the deadline.", + }, + "accessory": { + "type": "datepicker", + "initial_date": "1990-04-28", + "placeholder": { + "type": "plain_text", + "text": "Select a date", + }, + }, + }, + ], + } + ], + ) + self.assertEqual("ok", resp.body) + + resp = client.send( + text="hello!", + response_type="ephemeral", + attachments=[ + Attachment( + text="attachment text", + title="Attachment", + fallback="fallback_text", + pretext="some_pretext", + title_link="link in title", + fields=[AttachmentField(title=f"field_{i}_title", value=f"field_{i}_value") for i in range(5)], + color="#FFFF00", + author_name="John Doe", + author_link="http://johndoeisthebest.com", + author_icon="http://johndoeisthebest.com/avatar.jpg", + thumb_url="thumbnail URL", + footer="and a footer", + footer_icon="link to footer icon", + ts=123456789, + markdown_in=["fields"], + ) + ], + ) + self.assertEqual("ok", resp.body) + + def test_send_dict(self): + client = WebhookClient("http://localhost:8888") + resp: WebhookResponse = client.send_dict({"text": "hello!"}) + self.assertEqual(200, resp.status_code) + self.assertEqual("ok", resp.body) + + def test_timeout_issue_712(self): + client = WebhookClient(url="http://localhost:8888/timeout", timeout=1) + with self.assertRaises(socket.timeout): + client.send_dict({"text": "hello!"}) + + def test_error_response(self): + client = WebhookClient(url="http://localhost:8888/error") + resp: WebhookResponse = client.send_dict({"text": "hello!"}) + self.assertEqual(500, resp.status_code) + self.assertTrue(resp.body.startswith("")) + + def test_proxy_issue_714(self): + client = WebhookClient(url="http://localhost:8888", proxy="http://invalid-host:9999") + with self.assertRaises(urllib.error.URLError): + client.send_dict({"text": "hello!"}) + + def test_user_agent_customization_issue_769(self): + client = WebhookClient( + url="http://localhost:8888/user-agent-this_is-test", + user_agent_prefix="this_is", + user_agent_suffix="test", + ) + resp = client.send_dict({"text": "hi!"}) + self.assertEqual(resp.body, "ok") diff --git a/tutorial/01-creating-the-slack-app.md b/tutorial/01-creating-the-slack-app.md new file mode 100644 index 000000000..81e3de76b --- /dev/null +++ b/tutorial/01-creating-the-slack-app.md @@ -0,0 +1,48 @@ +# Create a Slack app + +> 💡 Build useful apps, internal tools, simplified workflows, or brilliant bots for just your team or Slack's millions of users. + +- To get started, create a new Slack App on [api.slack.com](https://api.slack.com/apps?new_granular_bot_app=1). + 1. Type in your app name. + 2. Select the workspace you'd like to build your app on. We recommend using a workspace where you won't disrupt real work getting done — [you can create one for free](https://slack.com/get-started#create). + Create-A-Slack-App + +### Give your app permissions + +[Scopes](https://docs.slack.dev/reference/scopes) give your app permission to do things (for example, post messages) in your development workspace. + +- Navigate to **OAuth & Permissions** on the sidebar to add scopes to your app + +OAuth and Permissions + +- Scroll down to the **Bot Token Scopes** section and click **Add an OAuth Scope**. + +For now, we'll only use one scope. + +- Add the [`chat:write` scope](https://docs.slack.dev/reference/scopes/chat.write/) to grant your app the permission to post messages in channels it's a member of. +- Add the [`im:write` scope](https://docs.slack.dev/reference/scopes/im.write/) to grant your app the permission to post messages in DMs. + +🎉 You should briefly see a success banner. + +_If you want to change your bot user's name, click on **App Home** in the left sidebar and modify the display name._ + +### Install the app in your workspace + +- Scroll up to the top of the **OAuth & Permissions** pages and click the green "Install App to Workspace" button. + +![Install Slack app to workspace](assets/oauth-installation.png) + +Next you'll need to authorize the app for the Bot User permissions. + +- Click the "Allow" button. + +![Authorize Slack app installation](assets/authorize-install.png) + +🏁 Finally copy and save your bot token. You'll need this to communicate with Slack's Platform. +![Copy bot token](assets/bot-token.png) + +--- + +**Next section: [02 - Building a message](02-building-a-message.md).** + +**Back to the [Table of contents](README.md#table-of-contents).** diff --git a/tutorial/02-building-a-message.md b/tutorial/02-building-a-message.md new file mode 100644 index 000000000..9f2481a84 --- /dev/null +++ b/tutorial/02-building-a-message.md @@ -0,0 +1,155 @@ +# Building a message + +The code for this step is available [here](PythOnBoardingBot/onboarding_tutorial.py). + +> 💡 **[Block Kit](https://docs.slack.dev/block-kit/)** is a UI framework for Slack apps that offers a balance of control and flexibility when building experiences in messages and other surfaces. Customize the order and appearance of information and guide users through your app's capabilities by composing, updating, sequencing, and stacking blocks — reusable components that work almost everywhere in Slack. You can experiment and prototype with Block Kit using the [Block Kit Builder](https://api.slack.com/tools/block-kit-builder). + +We're going to be using Block Kit to build our onboarding tutorial messages. + +With Block Kit, we can create a message in Slack that looks like this: +Onboarding Message + +By sending the following json payload: + +```Python +{ + "channel": "D0123456", + "username": "pythonboardingbot", + "icon_emoji": ":robot_face:", + "blocks": [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "Welcome to Slack! :wave: We're so glad you're here. :blush:\n\n*Get started by completing the steps below:*", + }, + }, + {"type": "divider"}, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": ":white_large_square: *Add an emoji reaction to this message* :thinking_face:\nYou can quickly respond to any message on Slack with an emoji reaction. Reactions can be used for any purpose: voting, checking off to-do items, showing excitement.", + }, + }, + { + "type": "context", + "elements": [ + { + "type": "mrkdwn", + "text": " :information_source: **", + } + ], + }, + {"type": "divider"}, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": ":white_large_square: *Pin this message* :round_pushpin:\nImportant messages and files can be pinned to the details pane in any channel or direct message, including group messages, for easy reference.", + }, + }, + { + "type": "context", + "elements": [ + { + "type": "mrkdwn", + "text": " :information_source: **", + } + ], + }, + ], +} +``` + +To make this simpler, more pleasant and more productive we'll create a class that's responsible for building it. We'll also store the state of which tasks were completed so that it's easy to update existing messages. + +- Create a file called `onboarding_tutorial.py`. +- 🏁Add the following code into it: + +```Python +class OnboardingTutorial: + """Constructs the onboarding message and stores the state of which tasks were completed.""" + + WELCOME_BLOCK = { + "type": "section", + "text": { + "type": "mrkdwn", + "text": ( + "Welcome to Slack! :wave: We're so glad you're here. :blush:\n\n" + "*Get started by completing the steps below:*" + ), + }, + } + DIVIDER_BLOCK = {"type": "divider"} + + def __init__(self, channel): + self.channel = channel + self.username = "pythonboardingbot" + self.icon_emoji = ":robot_face:" + self.timestamp = "" + self.reaction_task_completed = False + self.pin_task_completed = False + + def get_message_payload(self): + return { + "ts": self.timestamp, + "channel": self.channel, + "username": self.username, + "icon_emoji": self.icon_emoji, + "blocks": [ + self.WELCOME_BLOCK, + self.DIVIDER_BLOCK, + *self._get_reaction_block(), + self.DIVIDER_BLOCK, + *self._get_pin_block(), + ], + } + + def _get_reaction_block(self): + task_checkmark = self._get_checkmark(self.reaction_task_completed) + text = ( + f"{task_checkmark} *Add an emoji reaction to this message* :thinking_face:\n" + "You can quickly respond to any message on Slack with an emoji reaction." + "Reactions can be used for any purpose: voting, checking off to-do items, showing excitement." + ) + information = ( + ":information_source: **" + ) + return self._get_task_block(text, information) + + def _get_pin_block(self): + task_checkmark = self._get_checkmark(self.pin_task_completed) + text = ( + f"{task_checkmark} *Pin this message* :round_pushpin:\n" + "Important messages and files can be pinned to the details pane in any channel or" + " direct message, including group messages, for easy reference." + ) + information = ( + ":information_source: **" + ) + return self._get_task_block(text, information) + + @staticmethod + def _get_checkmark(task_completed: bool) -> str: + if task_completed: + return ":white_check_mark:" + return ":white_large_square:" + + @staticmethod + def _get_task_block(text, information): + return [ + {"type": "section", "text": {"type": "mrkdwn", "text": text}}, + {"type": "context", "elements": [{"type": "mrkdwn", "text": information}]}, + ] +``` + +--- + +**Next section: [03 - Responding to Slack events](03-responding-to-slack-events.md).** + +**Previous section: [01 - Creating the Slack app](01-creating-the-slack-app.md).** + +**Back to the [Table of contents](README.md#table-of-contents).** diff --git a/tutorial/03-responding-to-slack-events.md b/tutorial/03-responding-to-slack-events.md new file mode 100644 index 000000000..70c904e50 --- /dev/null +++ b/tutorial/03-responding-to-slack-events.md @@ -0,0 +1,213 @@ +# Responding to Slack events + +The code for this step is available [here](PythOnBoardingBot). + +## Install the dependencies + +> 💡 **["Requirements files"](https://pip.pypa.io/en/stable/user_guide/#id12)** are files containing a list of items to be installed using pip install. Details on the format of the files are here: [Requirements File Format](https://pip.pypa.io/en/stable/reference/pip_install/#requirements-file-format). + +- In the root directory create a "requirements.txt" file. +- Add the following contents to that file and save the file. + +``` +slack_sdk>=3.0 +slack_bolt>=1.6.1 +certifi +``` + +> 💡 **[Certifi](https://github.com/certifi/python-certifi)** is a carefully curated collection of Root Certificates for validating the trustworthiness of SSL certificates while verifying the identity of TLS hosts. It has been extracted from the Requests project. + +- Next you can install those dependencies by running the following command from your terminal: + +``` +$ pip3 install -r requirements.txt +-> Successfully installed slack_sdk-3.0.0 +``` + +## Creating the app + +- Create an `app.py` file to run the app. + +The first thing we'll need to do is import the code our app needs to run. + +- In `app.py` add the following code: + +```Python +import logging +from slack_bolt import App +from slack_sdk.web import WebClient +from onboarding_tutorial import OnboardingTutorial +``` + +- Next, create a Bolt for Python application. Add the following line to `app.py`: + +```Python +app = App() +``` + +Next we'll need our app to store some data. For simplicity we'll store our app data in-memory with the following data structure: `{"channel": {"user_id": OnboardingTutorial}}`. + +- Add the following line under the previous code: + +```Python +onboarding_tutorials_sent = {} +``` + +Let's add a function that's responsible for creating and sending the onboarding welcome message to new users. We'll also save the time stamp of the message when it's posted so we can update this message in the future. + +- Add the following lines of code to `app.py`: + +```Python +def start_onboarding(user_id: str, channel: str, client: WebClient): + # Create a new onboarding tutorial. + onboarding_tutorial = OnboardingTutorial(channel) + + # Get the onboarding message payload + message = onboarding_tutorial.get_message_payload() + + # Post the onboarding message in Slack + response = client.chat_postMessage(**message) + + # Capture the timestamp of the message we've just posted so + # we can use it to update the message after a user + # has completed an onboarding task. + onboarding_tutorial.timestamp = response["ts"] + + # Store the message sent in onboarding_tutorials_sent + if channel not in onboarding_tutorials_sent: + onboarding_tutorials_sent[channel] = {} + onboarding_tutorials_sent[channel][user_id] = onboarding_tutorial +``` + +### Responding to events in Slack + +When events occur in Slack there are two primary ways to be notified about them. We can send you an [HTTP Request through our Events API](https://docs.slack.dev/apis/events-api/), or you can stream events through a WebSocket connection with our [Socket Mode](https://docs.slack.dev/apis/events-api/using-socket-mode/) API. If you're behind a firewall and cannot receive incoming web requests from Slack, we recommend going with Socket Mode. + +In this tutorial we'll be using the Events API and the [Bolt for Python](https://github.com/slackapi/bolt-python). + +Back to our application, it's time to link our onboarding functionality to Slack events. + +- Add the following lines of code to `app.py`: + +```Python +# ================ Team Join Event =============== # +# When the user first joins a team, the type of the event will be 'team_join'. +# Here we'll link the onboarding_message callback to the 'team_join' event. + +# Note: Bolt provides a WebClient instance as an argument to the listener function +# we've defined here, which we then use to access Slack Web API methods like conversations_open. +# For more info, checkout: https://docs.slack.dev/tools/bolt-python/concepts/message-listening +@app.event("team_join") +def onboarding_message(event, client): + """Create and send an onboarding welcome message to new users. Save the + time stamp of this message so we can update this message in the future. + """ + # Get the id of the Slack user associated with the incoming event + user_id = event.get("user", {}).get("id") + + # Open a DM with the new user. + response = client.conversations_open(users=user_id) + channel = response["channel"]["id"] + + # Post the onboarding message. + start_onboarding(user_id, channel, client) + + +# ============= Reaction Added Events ============= # +# When a users adds an emoji reaction to the onboarding message, +# the type of the event will be 'reaction_added'. +# Here we'll link the update_emoji callback to the 'reaction_added' event. +@app.event("reaction_added") +def update_emoji(event, client): + """Update the onboarding welcome message after receiving a "reaction_added" + event from Slack. Update timestamp for welcome message as well. + """ + # Get the ids of the Slack user and channel associated with the incoming event + channel_id = event.get("item", {}).get("channel") + user_id = event.get("user") + + if channel_id not in onboarding_tutorials_sent: + return + + # Get the original tutorial sent. + onboarding_tutorial = onboarding_tutorials_sent[channel_id][user_id] + + # Mark the reaction task as completed. + onboarding_tutorial.reaction_task_completed = True + + # Get the new message payload + message = onboarding_tutorial.get_message_payload() + + # Post the updated message in Slack + updated_message = client.chat_update(**message) + + +# =============== Pin Added Events ================ # +# When a users pins a message the type of the event will be 'pin_added'. +# Here we'll link the update_pin callback to the 'pin_added' event. +@app.event("pin_added") +def update_pin(event, client): + """Update the onboarding welcome message after receiving a "pin_added" + event from Slack. Update timestamp for welcome message as well. + """ + # Get the ids of the Slack user and channel associated with the incoming event + channel_id = event.get("channel_id") + user_id = event.get("user") + + # Get the original tutorial sent. + onboarding_tutorial = onboarding_tutorials_sent[channel_id][user_id] + + # Mark the pin task as completed. + onboarding_tutorial.pin_task_completed = True + + # Get the new message payload + message = onboarding_tutorial.get_message_payload() + + # Post the updated message in Slack + updated_message = client.chat_update(**message) + + +# ============== Message Events ============= # +# When a user sends a DM, the event type will be 'message'. +# Here we'll link the message callback to the 'message' event. +@app.event("message") +def message(event, client): + """Display the onboarding welcome message after receiving a message + that contains "start". + """ + channel_id = event.get("channel") + user_id = event.get("user") + text = event.get("text") + + if text and text.lower() == "start": + return start_onboarding(user_id, channel_id, client) +``` + +Finally, we need to make our app runnable. + +- 🏁 Add the following lines of code to the end of `app.py`. + +```Python +if __name__ == "__main__": + logger = logging.getLogger() + logger.setLevel(logging.DEBUG) + logger.addHandler(logging.StreamHandler()) + app.start(3000) +``` + +**Note:** When running in a virtual environment you often need to specify the location of the SSL Certificate(`cacert.pem`). To make this easy we use Certifi's built-in `where()` function to locate the installed certificate authority (CA) bundle. + +```python +import ssl as ssl_lib +import certifi + +ssl_context = ssl_lib.create_default_context(cafile=certifi.where()) +``` + +--- + +**Next section: [04 - Running the app](04-running-the-app.md).** + +**Previous section: [02 - Building a message](02-building-a-message.md).** + +**Back to the [Table of contents](README.md#table-of-contents).** diff --git a/tutorial/04-running-the-app.md b/tutorial/04-running-the-app.md new file mode 100644 index 000000000..873a16f5a --- /dev/null +++ b/tutorial/04-running-the-app.md @@ -0,0 +1,54 @@ +# Running your app + +## Set App Credentials + +Before you can run your app you need to put your bot token into the environment. + +**Note:** This is the same token you copied at the end of [Step 1](/tutorial/01-creating-the-slack-app.md#add-a-bot-user). +![Copy bot token](assets/bot-token.png) + +- Add this token to your environment variables: + +``` +$ export SLACK_BOT_TOKEN='xoxb-XXXXXXXXXXXX-xxxxxxxxxxxx-XXXXXXXXXXXXXXXXXXXXXXXX' +``` + +- Navigate to your app's **Basic Information** page from the left sidebar and scroll down to **App Credentials** and copy the value for **Signing Secret**: + ![Copy signing secret](assets/signing-secret.png) + +``` +$ export SLACK_SIGNING_SECRET='xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' +``` + +- 🏁Run your app + +``` +$ python3 app.py +``` + +When running locally, you'll likely need to tunnel requests from a public URL to your machine. We recommend [ngrok](https://ngrok.com/) to set up a tunnel. Once you've started ngrok, you'll have a URL that you can set in the Event Subscriptions portion of the Slack app configuration. Append the URL from ngrok with /slack/events. For example, https://abcdef.ngrok.io/slack/events. + +## Subscribe to events + +In the previous section, we set up handlers for a couple of events, but we need to set up our app to listen to those events. Click on **Event Subscriptions** on the left hand sidebar of your app. + +- Toggle the switch to **Enable Events** + ![Enable events](assets/enable-events.png) + +- Enter your public URL (for example, in the last step, ours was https://abcdef.ngrok.io/slack/events) and, if your server is running, the domain should be verified. + +- Next, subscribe to the events we used in the previous step: `message.channels`, `team_join`, `pin_added`, and `reaction_added`. + +![Subscribe to events](assets/subscribe-events.png). + +When you click the green **Save Changes** button, you'll need to reinstall your app. + +### 🎉 That's it. Congratulations! You've just built a Slack app. 🤖 + +To demo the app, simply invite your bot to a public channel and send a message that says "start". And then, the app will post a new message in the same channel. You can interact with the message by adding a reaction to it and adding the message to the pinned items. If the app is properly configured, you will see the modifications of the message accordingly. + +## ![Onboarding](https://user-images.githubusercontent.com/3329665/56870674-ab02b300-69c7-11e9-9101-eb823235f3c2.gif) + +**Previous section: [03 - Responding to Slack events](03-responding-to-slack-events.md).** + +**Back to the [Table of contents](README.md#table-of-contents).** diff --git a/tutorial/PythOnBoardingBot/app.py b/tutorial/PythOnBoardingBot/app.py new file mode 100644 index 000000000..7245c71ab --- /dev/null +++ b/tutorial/PythOnBoardingBot/app.py @@ -0,0 +1,132 @@ +import logging +from slack_bolt import App +from slack_sdk.web import WebClient +from onboarding_tutorial import OnboardingTutorial + +# Initialize a Bolt for Python app +app = App() + +# For simplicity we'll store our app data in-memory with the following data structure. +# onboarding_tutorials_sent = {"channel": {"user_id": OnboardingTutorial}} +onboarding_tutorials_sent = {} + + +def start_onboarding(user_id: str, channel: str, client: WebClient): + # Create a new onboarding tutorial. + onboarding_tutorial = OnboardingTutorial(channel) + + # Get the onboarding message payload + message = onboarding_tutorial.get_message_payload() + + # Post the onboarding message in Slack + response = client.chat_postMessage(**message) + + # Capture the timestamp of the message we've just posted so + # we can use it to update the message after a user + # has completed an onboarding task. + onboarding_tutorial.timestamp = response["ts"] + + # Store the message sent in onboarding_tutorials_sent + if channel not in onboarding_tutorials_sent: + onboarding_tutorials_sent[channel] = {} + onboarding_tutorials_sent[channel][user_id] = onboarding_tutorial + + +# ================ Team Join Event =============== # +# When the user first joins a team, the type of the event will be 'team_join'. +# Here we'll link the onboarding_message callback to the 'team_join' event. + +# Note: Bolt provides a WebClient instance as an argument to the listener function +# we've defined here, which we then use to access Slack Web API methods like conversations_open. +# For more info, checkout: https://docs.slack.dev/tools/bolt-python/concepts/message-listening +@app.event("team_join") +def onboarding_message(event, client): + """Create and send an onboarding welcome message to new users. Save the + time stamp of this message so we can update this message in the future. + """ + # Get the id of the Slack user associated with the incoming event + user_id = event.get("user", {}).get("id") + + # Open a DM with the new user. + response = client.conversations_open(users=user_id) + channel = response["channel"]["id"] + + # Post the onboarding message. + start_onboarding(user_id, channel, client) + + +# ============= Reaction Added Events ============= # +# When a users adds an emoji reaction to the onboarding message, +# the type of the event will be 'reaction_added'. +# Here we'll link the update_emoji callback to the 'reaction_added' event. +@app.event("reaction_added") +def update_emoji(event, client): + """Update the onboarding welcome message after receiving a "reaction_added" + event from Slack. Update timestamp for welcome message as well. + """ + # Get the ids of the Slack user and channel associated with the incoming event + channel_id = event.get("item", {}).get("channel") + user_id = event.get("user") + + if channel_id not in onboarding_tutorials_sent: + return + + # Get the original tutorial sent. + onboarding_tutorial = onboarding_tutorials_sent[channel_id][user_id] + + # Mark the reaction task as completed. + onboarding_tutorial.reaction_task_completed = True + + # Get the new message payload + message = onboarding_tutorial.get_message_payload() + + # Post the updated message in Slack + updated_message = client.chat_update(**message) + + +# =============== Pin Added Events ================ # +# When a users pins a message the type of the event will be 'pin_added'. +# Here we'll link the update_pin callback to the 'pin_added' event. +@app.event("pin_added") +def update_pin(event, client): + """Update the onboarding welcome message after receiving a "pin_added" + event from Slack. Update timestamp for welcome message as well. + """ + # Get the ids of the Slack user and channel associated with the incoming event + channel_id = event.get("channel_id") + user_id = event.get("user") + + # Get the original tutorial sent. + onboarding_tutorial = onboarding_tutorials_sent[channel_id][user_id] + + # Mark the pin task as completed. + onboarding_tutorial.pin_task_completed = True + + # Get the new message payload + message = onboarding_tutorial.get_message_payload() + + # Post the updated message in Slack + updated_message = client.chat_update(**message) + + +# ============== Message Events ============= # +# When a user sends a DM, the event type will be 'message'. +# Here we'll link the message callback to the 'message' event. +@app.event("message") +def message(event, client): + """Display the onboarding welcome message after receiving a message + that contains "start". + """ + channel_id = event.get("channel") + user_id = event.get("user") + text = event.get("text") + + if text and text.lower() == "start": + return start_onboarding(user_id, channel_id, client) + + +if __name__ == "__main__": + logger = logging.getLogger() + logger.setLevel(logging.DEBUG) + logger.addHandler(logging.StreamHandler()) + app.start(3000) diff --git a/tutorial/PythOnBoardingBot/onboarding_tutorial.py b/tutorial/PythOnBoardingBot/onboarding_tutorial.py new file mode 100644 index 000000000..8ae5ef2a2 --- /dev/null +++ b/tutorial/PythOnBoardingBot/onboarding_tutorial.py @@ -0,0 +1,76 @@ +class OnboardingTutorial: + """Constructs the onboarding message and stores the state of which tasks were completed.""" + + WELCOME_BLOCK = { + "type": "section", + "text": { + "type": "mrkdwn", + "text": ( + "Welcome to Slack! :wave: We're so glad you're here. :blush:\n\n" + "*Get started by completing the steps below:*" + ), + }, + } + DIVIDER_BLOCK = {"type": "divider"} + + def __init__(self, channel): + self.channel = channel + self.username = "pythonboardingbot" + self.icon_emoji = ":robot_face:" + self.timestamp = "" + self.reaction_task_completed = False + self.pin_task_completed = False + + def get_message_payload(self): + return { + "ts": self.timestamp, + "channel": self.channel, + "username": self.username, + "icon_emoji": self.icon_emoji, + "blocks": [ + self.WELCOME_BLOCK, + self.DIVIDER_BLOCK, + *self._get_reaction_block(), + self.DIVIDER_BLOCK, + *self._get_pin_block(), + ], + } + + def _get_reaction_block(self): + task_checkmark = self._get_checkmark(self.reaction_task_completed) + text = ( + f"{task_checkmark} *Add an emoji reaction to this message* :thinking_face:\n" + "You can quickly respond to any message on Slack with an emoji reaction." + "Reactions can be used for any purpose: voting, checking off to-do items, showing excitement." + ) + information = ( + ":information_source: **" + ) + return self._get_task_block(text, information) + + def _get_pin_block(self): + task_checkmark = self._get_checkmark(self.pin_task_completed) + text = ( + f"{task_checkmark} *Pin this message* :round_pushpin:\n" + "Important messages and files can be pinned to the details pane in any channel or" + " direct message, including group messages, for easy reference." + ) + information = ( + ":information_source: **" + ) + return self._get_task_block(text, information) + + @staticmethod + def _get_checkmark(task_completed: bool) -> str: + if task_completed: + return ":white_check_mark:" + return ":white_large_square:" + + @staticmethod + def _get_task_block(text, information): + return [ + {"type": "section", "text": {"type": "mrkdwn", "text": text}}, + {"type": "context", "elements": [{"type": "mrkdwn", "text": information}]}, + ] diff --git a/tutorial/PythOnBoardingBot/requirements.txt b/tutorial/PythOnBoardingBot/requirements.txt new file mode 100644 index 000000000..b22c1e59c --- /dev/null +++ b/tutorial/PythOnBoardingBot/requirements.txt @@ -0,0 +1,3 @@ +slack_sdk>=3.0 +slack_bolt>=1.6.1 +certifi \ No newline at end of file diff --git a/tutorial/README.md b/tutorial/README.md new file mode 100644 index 000000000..12d99c89c --- /dev/null +++ b/tutorial/README.md @@ -0,0 +1,62 @@ +# Build a Slack app in less than 10 minutes! + +Welcome to the Slack app tutorial: **PythOnBoardingBot**. + +This tutorial serves as a walkthrough guide and example of the types of Slack apps you can build with Slack's Python SDK, python-slack-sdk. We'll cover all the basic steps you'll need to have a fully functioning app. + +## What is PythOnBoardingBot? + +PythOnBoardingBot is designed to greet new users on your team and introduce them to some nifty features in Slack. + +When a user first joins a team it'll send you a message with the following tasks that you must complete: +- Pin a message to the channel. +- React to a message. + +As you complete each task you'll see the message update with a green checkmark. + +![Onboarding](https://user-images.githubusercontent.com/3329665/56870674-ab02b300-69c7-11e9-9101-eb823235f3c2.gif) + +## What you'll need before you get started: + +1. A Slack team. +Before anything else you'll need a Slack team. You can [Sign into an existing Slack workspace](https://get.slack.help/hc/en-us/articles/212681477-Sign-in-to-Slack) or you can [create a new Slack workspace](https://get.slack.help/hc/en-us/articles/206845317-Create-a-Slack-workspace) to test your app first. + +2. A terminal with Python 3.7+ installed. +Check your installation by running the following command in your terminal: +``` +$ python3 --version +-> Python 3.7.17 +``` + +You'll need to install Python 3.7 if you receive the following error: +``` +-> bash: python3: command not found +``` + +Note: You should probably use pyenv to install Python 3. See [pyenv](https://github.com/pyenv/pyenv#installation) and [pyenv-install](https://github.com/pyenv/pyenv-installer) for details. + +Create a new project folder and a virtual environment. +``` +$ mkdir PythOnBoardingBot && cd PythOnBoardingBot +$ python3 -m venv env/ +$ source env/bin/activate +``` + +3. A text editor of your choice. +Open up your new project folder "PythOnBoardingBot" in your text editor. + +## Table of contents +- [01 - Creating the Slack app](01-creating-the-slack-app.md) +- [02 - Building a message](02-building-a-message.md) +- [03 - Responding to Slack events](03-responding-to-slack-events.md) +- [04 - Running the app](04-running-the-app.md) + +## Coming up next +- Add tests to your app. +- Add starring a message as an onboarding task. +- Creating a Slack "MessageBuilder" object. This would aid in the creation of complex messages. +- Running this app from the command line with [`$ click_`](https://click.palletsprojects.com/en/7.x/). +- How to run this app on multiple teams. + +## Credits +This tutorial app was originally built by @karishay . Thank you! :bow: diff --git a/tutorial/assets/authorize-install.png b/tutorial/assets/authorize-install.png new file mode 100644 index 000000000..7e0b5fedb Binary files /dev/null and b/tutorial/assets/authorize-install.png differ diff --git a/tutorial/assets/bot-token.png b/tutorial/assets/bot-token.png new file mode 100644 index 000000000..0d89c4cbb Binary files /dev/null and b/tutorial/assets/bot-token.png differ diff --git a/tutorial/assets/enable-events.png b/tutorial/assets/enable-events.png new file mode 100644 index 000000000..eda94630c Binary files /dev/null and b/tutorial/assets/enable-events.png differ diff --git a/tutorial/assets/oauth-installation.png b/tutorial/assets/oauth-installation.png new file mode 100644 index 000000000..bf45ececb Binary files /dev/null and b/tutorial/assets/oauth-installation.png differ diff --git a/tutorial/assets/oauth-permissions.png b/tutorial/assets/oauth-permissions.png new file mode 100644 index 000000000..5477be0c2 Binary files /dev/null and b/tutorial/assets/oauth-permissions.png differ diff --git a/tutorial/assets/signing-secret.png b/tutorial/assets/signing-secret.png new file mode 100644 index 000000000..8d736aa8e Binary files /dev/null and b/tutorial/assets/signing-secret.png differ diff --git a/tutorial/assets/subscribe-events.png b/tutorial/assets/subscribe-events.png new file mode 100644 index 000000000..832e18459 Binary files /dev/null and b/tutorial/assets/subscribe-events.png differ